Page MenuHomePhabricator

No OneTemporary

Size
25 KB
Referenced Files
None
Subscribers
None
diff --git a/trunk/datareporterengine.cpp b/trunk/datareporterengine.cpp
index a3965ad..2e1aad0 100644
--- a/trunk/datareporterengine.cpp
+++ b/trunk/datareporterengine.cpp
@@ -1,373 +1,383 @@
/*******************************************************************************
* fred Copyright (c) 2011 by Gillen Daniel <gillen.dan@pinguin.lu> *
* *
* Forensic Registry EDitor (fred) is a cross-platform M$ registry hive editor *
* with special feautures useful during forensic analysis. *
* *
* This program is free software: you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the Free *
* Software Foundation, either version 3 of the License, or (at your option) *
* any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
* more details. *
* *
* You should have received a copy of the GNU General Public License along with *
* this program. If not, see <http://www.gnu.org/licenses/>. *
*******************************************************************************/
#include "datareporterengine.h"
#include <QString>
#include <QMap>
#include <QMapIterator>
#include <QStringList>
#include <QDateTime>
DataReporterEngine::DataReporterEngine(RegistryHive *p_hive) : QScriptEngine() {
// Init vars
this->p_registry_hive=p_hive;
this->report_content="";
// Add our types to engine
qScriptRegisterMetaType<s_RegistryKeyValue>(this,
this->RegistryKeyValueToScript,
this->RegistryKeyValueFromScript);
this->p_type_byte_array=new ByteArray(this);
this->globalObject().setProperty("ByteArray",
this->p_type_byte_array->constructor());
// Add our functions
// print
QScriptValue func_print=this->newFunction(this->Print);
this->globalObject().setProperty("print",func_print);
// println
QScriptValue func_println=this->newFunction(this->PrintLn);
this->globalObject().setProperty("println",func_println);
// GetRegistryNodes
QScriptValue func_get_nodes=this->newFunction(this->GetRegistryNodes,1);
func_get_nodes.setData(this->newQObject(this->p_registry_hive));
this->globalObject().setProperty("GetRegistryNodes",func_get_nodes);
// GetRegistryKeys
QScriptValue func_get_keys=this->newFunction(this->GetRegistryKeys,1);
func_get_keys.setData(this->newQObject(this->p_registry_hive));
this->globalObject().setProperty("GetRegistryKeys",func_get_keys);
// GetRegistryKeyValue
QScriptValue func_get_key_value=this->newFunction(this->GetRegistryKeyValue,
2);
func_get_key_value.setData(this->newQObject(this->p_registry_hive));
this->globalObject().setProperty("GetRegistryKeyValue",func_get_key_value);
// RegistryKeyValueToString
QScriptValue func_value_to_string=
this->newFunction(this->RegistryKeyValueToString,2);
this->globalObject().setProperty("RegistryKeyValueToString",
func_value_to_string);
// RegistryKeyValueToVariant
QScriptValue func_value_to_variant=
this->newFunction(this->RegistryKeyValueToVariant);
this->globalObject().setProperty("RegistryKeyValueToVariant",
func_value_to_variant);
// RegistryKeyTypeToString
QScriptValue func_type_to_string=
this->newFunction(this->RegistryKeyTypeToString,1);
this->globalObject().setProperty("RegistryKeyTypeToString",
func_type_to_string);
/*
// Add RegistryHive object
QScriptValue obj_registry_hive=this->newQObject(this->p_registry_hive);
this->globalObject().setProperty("RegistryHive",obj_registry_hive);
*/
}
DataReporterEngine::~DataReporterEngine() {
delete this->p_type_byte_array;
}
QScriptValue DataReporterEngine::Print(QScriptContext *context,
QScriptEngine *engine)
{
int i;
QString content;
// Append all arguments to content
for(i=0;i<context->argumentCount();++i) {
//if(i>0) content.append(" ");
content.append(context->argument(i).toString());
}
//QScriptValue calleeData=context->callee().data();
//DataReporterEngine *engine=
// qobject_cast<DataReporterEngine*>(calleeData.toQObject());
qobject_cast<DataReporterEngine*>(engine)->report_content.append(content);
return engine->undefinedValue();
}
QScriptValue DataReporterEngine::PrintLn(QScriptContext *context,
QScriptEngine *engine)
{
int i;
QString content;
// Append all arguments to content
for(i=0;i<context->argumentCount();++i) {
//if(i>0) content.append(" ");
content.append(context->argument(i).toString());
}
qobject_cast<DataReporterEngine*>(engine)->
report_content.append(content).append("\n");
return engine->undefinedValue();
}
/*
* GetRegistryNodes
*/
QScriptValue DataReporterEngine::GetRegistryNodes(QScriptContext *context,
QScriptEngine *engine)
{
QScriptValue calleeData;
RegistryHive *p_hive;
QMap<QString,int> nodes;
QScriptValue ret_nodes;
int ii=0;
// This function needs one argument, parent node path
if(context->argumentCount()!=1) return engine->undefinedValue();
// Get calle data (Pointer to RegistryHive class)
calleeData=context->callee().data();
p_hive=qobject_cast<RegistryHive*>(calleeData.toQObject());
// Get nodes
nodes=p_hive->GetNodes(context->argument(0).toString());
if(p_hive->Error()) {
// Clear error state
p_hive->GetErrorMsg();
return engine->undefinedValue();
}
// Build script array
ret_nodes=engine->newArray(nodes.count());
QMapIterator<QString,int> i(nodes);
while(i.hasNext()) {
i.next();
ret_nodes.setProperty(ii++,QScriptValue(i.key()));
}
return ret_nodes;
}
/*
* GetRegistryKeys
*/
QScriptValue DataReporterEngine::GetRegistryKeys(QScriptContext *context,
QScriptEngine *engine)
{
QScriptValue calleeData;
RegistryHive *p_hive;
QMap<QString,int> keys;
QScriptValue ret_keys;
int ii=0;
// This function needs one argument, parent node path
if(context->argumentCount()!=1) return engine->undefinedValue();
// Get calle data (Pointer to RegistryHive class)
calleeData=context->callee().data();
p_hive=qobject_cast<RegistryHive*>(calleeData.toQObject());
// Get keys
keys=p_hive->GetKeys(context->argument(0).toString());
if(p_hive->Error()) {
// Clear error state
p_hive->GetErrorMsg();
return engine->undefinedValue();
}
// Build script array
ret_keys=engine->newArray(keys.count());
QMapIterator<QString,int> i(keys);
while(i.hasNext()) {
i.next();
ret_keys.setProperty(ii++,QScriptValue(i.key()));
}
return ret_keys;
}
/*
* RegistryKeyValueToScript
*/
QScriptValue DataReporterEngine::RegistryKeyValueToScript(QScriptEngine *engine,
const
s_RegistryKeyValue
&s)
{
QScriptValue obj=engine->newObject();
obj.setProperty("type",s.type);
obj.setProperty("length",s.length);
ByteArray *p_byte_array=new ByteArray(engine);
obj.setProperty("value",p_byte_array->newInstance(s.value));
return obj;
}
/*
* RegistryKeyValueFromScriptValue
*/
void DataReporterEngine::RegistryKeyValueFromScript(const QScriptValue &obj,
s_RegistryKeyValue &s)
{
s.type=obj.property("type").toInt32();
s.length=obj.property("length").toInt32();
// TODO: Don't know if this works, but it probably does ;)
s.value=qvariant_cast<QByteArray>(obj.property("value").data().toVariant());
}
QScriptValue DataReporterEngine::GetRegistryKeyValue(QScriptContext *context,
QScriptEngine *engine)
{
QScriptValue calleeData;
RegistryHive *p_hive;
QByteArray key_value;
int key_type=0;
size_t key_length=0;
s_RegistryKeyValue script_key_value;
// This function needs two arguments, key path and key name
if(context->argumentCount()!=2) return engine->undefinedValue();
// Get calle data (Pointer to RegistryHive class)
calleeData=context->callee().data();
p_hive=qobject_cast<RegistryHive*>(calleeData.toQObject());
// Get key value
key_value=p_hive->GetKeyValue(context->argument(0).toString(),
context->argument(1).toString(),
&key_type,
&key_length);
if(p_hive->Error() || key_length==-1) {
// Get error message ro clear error state
p_hive->GetErrorMsg();
return engine->undefinedValue();
}
// Save key value to s_RegistryKeyValue struct
script_key_value.type=key_type;
script_key_value.length=key_length;
script_key_value.value=key_value;
return DataReporterEngine::RegistryKeyValueToScript(engine,script_key_value);
}
QScriptValue DataReporterEngine::RegistryKeyValueToString(
QScriptContext *context,
QScriptEngine *engine)
{
QByteArray key_value;
QString ret="";
// This function needs two arguments, key value and value type
if(context->argumentCount()!=2) return engine->undefinedValue();
// Cast ByteArray argument to QByteArray and convert
key_value=qvariant_cast<QByteArray>(context->argument(0).data().toVariant());
ret=RegistryHive::KeyValueToString(key_value,
context->argument(1).toInt32());
return engine->newVariant(ret);
}
QScriptValue DataReporterEngine::RegistryKeyValueToVariant(
QScriptContext *context,
QScriptEngine *engine)
{
int offset=0;
+ int length=0;
QByteArray key_value;
QString variant_type;
int remaining_data_len;
const char *p_data;
QString ret="";
// This function needs at least two arguments, key value and variant type,
- // and may have an optional third argument specifying an offset
- if(context->argumentCount()<2 || context->argumentCount()>3)
+ // and may have two optional arguments, offset and length
+ if(context->argumentCount()<2 || context->argumentCount()>4) {
return engine->undefinedValue();
- if(context->argumentCount()==3) offset=context->argument(2).toInt32();
+ }
+ if(context->argumentCount()==3) {
+ offset=context->argument(2).toInt32();
+ length=-1;
+ }
+ if(context->argumentCount()==4) {
+ offset=context->argument(2).toInt32();
+ length=context->argument(3).toInt32();
+ }
+
// Cast ByteArray argument to QByteArray
key_value=qvariant_cast<QByteArray>(context->argument(0).data().toVariant());
variant_type=context->argument(1).toString();
// Calculate how many bytes are remainig after specified offset
remaining_data_len=key_value.size()-offset;
if(!remaining_data_len>0) {
// Nothing to show
return engine->undefinedValue();
}
// Get pointer to data at specified offset
p_data=key_value.constData();
p_data+=offset;
- // Convert
+ // ConvertFull name
if(variant_type=="int8" && remaining_data_len>=1) {
ret=QString().sprintf("%d",*(int8_t*)p_data);
} else if(variant_type=="uint8" && remaining_data_len>=1) {
ret=QString().sprintf("%u",*(uint8_t*)p_data);
} else if(variant_type=="int16" && remaining_data_len>=2) {
ret=QString().sprintf("%d",*(int16_t*)p_data);
} else if(variant_type=="uint16" && remaining_data_len>=2) {
ret=QString().sprintf("%u",*(uint16_t*)p_data);
} else if(variant_type=="int32" && remaining_data_len>=4) {
ret=QString().sprintf("%d",*(int32_t*)p_data);
} else if(variant_type=="uint32" && remaining_data_len>=4) {
ret=QString().sprintf("%d",*(uint32_t*)p_data);
} else if(variant_type=="unixtime" && remaining_data_len>=4) {
if(*(uint32_t*)p_data==0) {
ret="n/a";
} else {
QDateTime date_time;
date_time.setTime_t(*(uint32_t*)p_data);
ret=date_time.toString("yyyy/MM/dd hh:mm:ss");
}
} else if(variant_type=="filetime" && remaining_data_len>=8) {
if(*(uint64_t*)p_data==0) {
ret="n/a";
} else {
// TODO: Warn if >32bit
QDateTime date_time;
date_time.setTime_t((*(uint64_t*)p_data-116444736000000000)/10000000);
ret=date_time.toString("yyyy/MM/dd hh:mm:ss");
}
} else if(variant_type=="ascii") {
// TODO: This fails bad if the string is not null terminated!! It might be
// wise checking for a null char here
- ret=QString().fromAscii((char*)p_data);
+ ret=QString().fromAscii((char*)p_data,length);
} else if(variant_type=="utf16" && remaining_data_len>=2) {
- ret=QString().fromUtf16((ushort*)p_data);
+ ret=QString().fromUtf16((ushort*)p_data,length);
} else {
// Unknown variant type or another error
return engine->undefinedValue();
}
return engine->newVariant(ret);
}
QScriptValue DataReporterEngine::RegistryKeyTypeToString(
QScriptContext *context,
QScriptEngine *engine)
{
QString ret="";
// This function needs one arguments, key type
if(context->argumentCount()!=1) return engine->undefinedValue();
ret=RegistryHive::KeyTypeToString(context->argument(0).toInt32());
return engine->newVariant(ret);
}
diff --git a/trunk/qtscript_types/bytearray.cpp b/trunk/qtscript_types/bytearray.cpp
index e6dc920..37df935 100644
--- a/trunk/qtscript_types/bytearray.cpp
+++ b/trunk/qtscript_types/bytearray.cpp
@@ -1,193 +1,181 @@
/*******************************************************************************
* Copyright (c) 2011 by Gillen Daniel <gillen.dan@pinguin.lu> *
* *
* Derived from code by Nokia Corporation and/or its subsidiary(-ies) under a *
* compatible license: *
* *
* Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). *
* All rights reserved. *
* *
* This program is free software: you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the Free *
* Software Foundation, either version 3 of the License, or (at your option) *
* any later version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
* more details. *
* *
* You should have received a copy of the GNU General Public License along with *
* this program. If not, see <http://www.gnu.org/licenses/>. *
*******************************************************************************/
#include <QtScript/QScriptEngine>
#include "bytearray.h"
#include "bytearrayiterator.h"
#include "bytearrayprototype.h"
#include <stdlib.h>
Q_DECLARE_METATYPE(QByteArray*)
Q_DECLARE_METATYPE(ByteArray*)
-/*
-static qint32 toArrayIndex(const QString &str) {
- QByteArray bytes = str.toUtf8();
- char *eptr;
-
- quint32 pos = strtoul(bytes.constData(), &eptr, 10);
- if((eptr == bytes.constData() + bytes.size()) &&
- (QByteArray::number(pos) == bytes))
- {
- return pos;
- }
-
- return -1;
-}
-*/
-
ByteArray::ByteArray(QScriptEngine *engine)
: QObject(engine), QScriptClass(engine)
{
qScriptRegisterMetaType<QByteArray>(engine,
this->toScriptValue,
this->fromScriptValue);
this->length=engine->toStringHandle(QLatin1String("length"));
this->proto=engine->newQObject(new ByteArrayPrototype(this),
QScriptEngine::QtOwnership,
QScriptEngine::SkipMethodsInEnumeration |
QScriptEngine::ExcludeSuperClassMethods |
QScriptEngine::ExcludeSuperClassProperties);
QScriptValue global=engine->globalObject();
proto.setPrototype(global.property("Object").property("prototype"));
this->ctor=engine->newFunction(this->construct,this->proto);
this->ctor.setData(qScriptValueFromValue(engine,this));
}
ByteArray::~ByteArray() {}
QScriptClass::QueryFlags ByteArray::queryProperty(const QScriptValue &object,
const QScriptString &name,
QueryFlags flags,
uint *id)
{
QByteArray *ba=qscriptvalue_cast<QByteArray*>(object.data());
if(!ba) return 0;
if(name!=this->length) {
bool is_array_index;
qint32 pos=name.toArrayIndex(&is_array_index);
if(!is_array_index) return 0;
*id=pos;
if((flags & HandlesReadAccess) && (pos>=ba->size()))
flags &= ~HandlesReadAccess;
}
return flags;
}
QScriptValue ByteArray::property(const QScriptValue &object,
const QScriptString &name,
uint id)
{
QByteArray *ba=qscriptvalue_cast<QByteArray*>(object.data());
if(!ba) return QScriptValue();
if(name==length) return ba->length();
else {
qint32 pos=id;
if((pos < 0) || (pos >= ba->size())) return QScriptValue();
return uint(ba->at(pos)) & 255;
}
return QScriptValue();
}
void ByteArray::setProperty(QScriptValue &object,
const QScriptString &name,
uint id,
const QScriptValue &value)
{
QByteArray *ba=qscriptvalue_cast<QByteArray*>(object.data());
if(!ba) return;
if(name==length) this->resize(*ba,value.toInt32());
else {
qint32 pos=id;
if(pos<0) return;
if(ba->size()<=pos) this->resize(*ba,pos + 1);
(*ba)[pos]=char(value.toInt32());
}
}
QScriptValue::PropertyFlags ByteArray::propertyFlags(const QScriptValue &object,
const QScriptString &name,
uint id)
{
Q_UNUSED(object);
Q_UNUSED(id);
if(name==length) {
return QScriptValue::Undeletable | QScriptValue::SkipInEnumeration;
}
return QScriptValue::Undeletable;
}
QScriptClassPropertyIterator *ByteArray::newIterator(const QScriptValue &object)
{
return new ByteArrayIterator(object);
}
QString ByteArray::name() const {
return QLatin1String("ByteArray");
}
QScriptValue ByteArray::prototype() const {
return proto;
}
QScriptValue ByteArray::constructor() {
return ctor;
}
QScriptValue ByteArray::newInstance(int size) {
+#if QT_VERSION >= 0x040700
this->engine()->reportAdditionalMemoryCost(size);
+#endif
return newInstance(QByteArray(size,0));
}
QScriptValue ByteArray::newInstance(const QByteArray &ba) {
QScriptValue data=engine()->newVariant(qVariantFromValue(ba));
return engine()->newObject(this,data);
}
QScriptValue ByteArray::construct(QScriptContext *ctx, QScriptEngine *) {
ByteArray *cls=qscriptvalue_cast<ByteArray*>(ctx->callee().data());
if(!cls) return QScriptValue();
QScriptValue arg=ctx->argument(0);
if(arg.instanceOf(ctx->callee()))
return cls->newInstance(qscriptvalue_cast<QByteArray>(arg));
int size=arg.toInt32();
return cls->newInstance(size);
}
QScriptValue ByteArray::toScriptValue(QScriptEngine *eng, const QByteArray &ba)
{
QScriptValue ctor=eng->globalObject().property("ByteArray");
ByteArray *cls=qscriptvalue_cast<ByteArray*>(ctor.data());
if(!cls) return eng->newVariant(qVariantFromValue(ba));
return cls->newInstance(ba);
}
void ByteArray::fromScriptValue(const QScriptValue &obj, QByteArray &ba) {
ba=qvariant_cast<QByteArray>(obj.data().toVariant());
}
void ByteArray::resize(QByteArray &ba, int newSize) {
int oldSize=ba.size();
ba.resize(newSize);
+#if QT_VERSION >= 0x040700
if(newSize>oldSize)
this->engine()->reportAdditionalMemoryCost(newSize-oldSize);
+#endif
}
diff --git a/trunk/report_templates/SAM_UserAccounts.qs b/trunk/report_templates/SAM_UserAccounts.qs
index ba6d384..0f77c9c 100644
--- a/trunk/report_templates/SAM_UserAccounts.qs
+++ b/trunk/report_templates/SAM_UserAccounts.qs
@@ -1,64 +1,75 @@
// See http://windowsir.blogspot.com/2006/08/getting-user-info-from-image.html
+function print_table_row(cell01,cell02) {
+ println(" <tr><td>",cell01,"</td><td>",cell02,"</td></tr>");
+}
+
+function print_v_info(v_key_value,info_name,str_off) {
+ var offset=Number(RegistryKeyValueToVariant(v_key_value,"uint16",str_off))+0x0cc;
+ var len=Number(RegistryKeyValueToVariant(v_key_value,"uint16",str_off+4))/2;
+ if(len>0) print_table_row(info_name,RegistryKeyValueToVariant(v_key_value,"utf16",offset,len));
+}
+
println("<html>");
println(" <head><title>User Accounts</title></head>");
println(" <body>");
println(" <h2>User accounts</h2>");
// Iterate over all user names
var user_names=GetRegistryNodes("\\SAM\\Domains\\Account\\Users\\Names");
for(var i=0;i<user_names.length;i++) {
println(" <p>");
// Print user name
println(" ",user_names[i],"<br />");
println(" <table style=\"margin-left:20px\">");
// Get user rid stored in "default" key
var user_rid=GetRegistryKeyValue(String().concat("\\SAM\\Domains\\Account\\Users\\Names\\",user_names[i]),"");
user_rid=RegistryKeyTypeToString(user_rid.type);
- println("<tr><td>RID:</td><td>",Number(user_rid).toString(10)," (",user_rid,")","</td></td>");
+ println(" <tr><td>RID:</td><td>",Number(user_rid).toString(10)," (",user_rid,")","</td></tr>");
// RegistryKeyTypeToString returns the rid prepended with "0x". We have to remove that for further processing
user_rid=String(user_rid).substr(2);
// Get user's V key and print various infos
var v_key=GetRegistryKeyValue(String().concat("\\SAM\\Domains\\Account\\Users\\",user_rid),"V");
+ print_v_info(v_key.value,"Full name:",0x18);
+ print_v_info(v_key.value,"Comment:",0x24);
+ print_v_info(v_key.value,"Home directory:",0x48);
+ print_v_info(v_key.value,"Home directory drive:",0x54);
+ print_v_info(v_key.value,"Logon script path:",0x60);
+ print_v_info(v_key.value,"Profile path:",0x6c);
// Get user's F key and print various infos
var f_key=GetRegistryKeyValue(String().concat("\\SAM\\Domains\\Account\\Users\\",user_rid),"F");
- println("<tr><td>Last login time:</td><td>",RegistryKeyValueToVariant(f_key.value,"filetime",8),"</td></td>");
- println("<tr><td>Last pw change:</td><td>",RegistryKeyValueToVariant(f_key.value,"filetime",24),"</td></td>");
- println("<tr><td>Last failed login:</td><td>",RegistryKeyValueToVariant(f_key.value,"filetime",40),"</td></td>");
- println("<tr><td>Account expires:</td><td>",RegistryKeyValueToVariant(f_key.value,"filetime",32),"</td></td>");
-
- println("<tr><td>Total logins:</td><td>",RegistryKeyValueToVariant(f_key.value,"uint16",66),"</td></td>");
- println("<tr><td>Failed logins:</td><td>",RegistryKeyValueToVariant(f_key.value,"uint16",64),"</td></td>");
-
+ print_table_row("Last login time:",RegistryKeyValueToVariant(f_key.value,"filetime",8));
+ print_table_row("Last pw change:",RegistryKeyValueToVariant(f_key.value,"filetime",24));
+ print_table_row("Last failed login:",RegistryKeyValueToVariant(f_key.value,"filetime",40));
+ print_table_row("Account expires:",RegistryKeyValueToVariant(f_key.value,"filetime",32));
+ print_table_row("Total logins:",RegistryKeyValueToVariant(f_key.value,"uint16",66));
+ print_table_row("Failed logins:",RegistryKeyValueToVariant(f_key.value,"uint16",64));
var acc_flags=Number(RegistryKeyValueToVariant(f_key.value,"uint16",56));
- print("<tr><td>Account flags:</td><td>");
+ print(" <tr><td>Account flags:</td><td>");
if(acc_flags&0x0001) print("Disabled ");
if(acc_flags&0x0002) print("HomeDirReq ");
if(acc_flags&0x0004) print("PwNotReq ");
if(acc_flags&0x0008) print("TempDupAcc ");
- // Don't think this would be useful to show
+ // I don't think this would be useful to show
//if(acc_flags&0x0010) print("NormUserAcc ");
if(acc_flags&0x0020) print("MnsAcc ");
if(acc_flags&0x0040) print("DomTrustAcc ");
if(acc_flags&0x0080) print("WksTrustAcc ");
if(acc_flags&0x0100) print("SrvTrustAcc ");
if(acc_flags&0x0200) print("NoPwExpiry ");
if(acc_flags&0x0400) print("AccAutoLock ");
- println("</td></td>");
-
-
-
-
- println(" </table>");
+ println("</td></tr>");
+ // TODO: User group membership
+ println(" </table>");
println(" </p>");
}
println("</html>");

File Metadata

Mime Type
text/x-diff
Expires
Tue, Dec 24, 3:03 AM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1176962
Default Alt Text
(25 KB)

Event Timeline