Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F4324582
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Size
23 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/trunk/datareporterengine.cpp b/trunk/datareporterengine.cpp
index 465b6e4..6046fdb 100644
--- a/trunk/datareporterengine.cpp
+++ b/trunk/datareporterengine.cpp
@@ -1,363 +1,369 @@
/*******************************************************************************
* fred Copyright (c) 2011-2012 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>
#include <stdio.h>
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);
// GetRegistryKeyModTime
QScriptValue func_get_key_modt=this->newFunction(this->GetRegistryKeyModTime,
2);
func_get_key_modt.setData(this->newQObject(this->p_registry_hive));
this->globalObject().setProperty("GetRegistryKeyModTime",func_get_key_modt);
// 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);
}
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();
}
//qDebug(QString("P: %1 A: %2").arg(context->argument(0).toString()).arg(keys.count()).toAscii().constData());
// 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()) {
// Get error message to clear error state
p_hive->GetErrorMsg();
// printf("\nError: %s\n",p_hive->GetErrorMsg().toAscii().constData());
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=-1;
+ bool little_endian=true;
QByteArray key_value;
QString format="";
QString ret="";
// This function needs at least two arguments, key value and variant type,
- // and may have two optional arguments, offset and length
- if(context->argumentCount()<2 || context->argumentCount()>4) {
+ // and may have three optional arguments, offset, length and little_endian
+ if(context->argumentCount()<2 || context->argumentCount()>5) {
return engine->undefinedValue();
}
if(context->argumentCount()==3) {
offset=context->argument(2).toInt32();
}
if(context->argumentCount()==4) {
offset=context->argument(2).toInt32();
length=context->argument(3).toInt32();
}
+ if(context->argumentCount()==5) {
+ offset=context->argument(2).toInt32();
+ length=context->argument(3).toInt32();
+ little_endian=(context->argument(4).toInt32()==1);
+ }
// Cast ByteArray argument to QByteArray
key_value=qvariant_cast<QByteArray>(context->argument(0).data().toVariant());
format=context->argument(1).toString();
- ret=RegistryHive::KeyValueToString(key_value,format,offset,length);
+ ret=RegistryHive::KeyValueToString(key_value,format,offset,length,little_endian);
return engine->newVariant(ret);
}
QScriptValue DataReporterEngine::RegistryKeyTypeToString(
QScriptContext *context,
QScriptEngine *engine)
{
QString ret="";
- // This function needs one arguments, key type
+ // This function needs one argument, key type
if(context->argumentCount()!=1) return engine->undefinedValue();
ret=RegistryHive::KeyTypeToString(context->argument(0).toInt32());
return engine->newVariant(ret);
}
QScriptValue DataReporterEngine::GetRegistryKeyModTime(
QScriptContext *context,
QScriptEngine *engine)
{
QScriptValue calleeData;
RegistryHive *p_hive;
int64_t mod_time=0;
// This function needs two argument, 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());
mod_time=p_hive->GetKeyModTime(context->argument(0).toString(),
context->argument(1).toString());
if(p_hive->Error()) {
// Get error message to clear error state
p_hive->GetErrorMsg();
return engine->undefinedValue();
}
QDateTime date_time;
date_time.setTimeSpec(Qt::UTC);
date_time.setTime_t(RegistryHive::FiletimeToUnixtime(mod_time));
return engine->newVariant(date_time.toString("yyyy/MM/dd hh:mm:ss"));
}
diff --git a/trunk/report_templates/NTUSER_LaunchedApplications.qs b/trunk/report_templates/NTUSER_LaunchedApplications.qs
new file mode 100644
index 0000000..92a51a3
--- /dev/null
+++ b/trunk/report_templates/NTUSER_LaunchedApplications.qs
@@ -0,0 +1,101 @@
+function IsValid(val) {
+ if(typeof val !== 'undefined') return true;
+ else return false;
+}
+
+function PrintTableRow(cell01,cell02,cell03) {
+ println(" <tr><td style=\"white-space:nowrap\">",cell01,"</td><td style=\"padding:2px; white-space:nowrap\">",cell02,"</td><td style=\"padding:2px; white-space:nowrap\">",cell03,"</td></tr>");
+}
+
+function Rot13Decode(val) {
+ var ret="";
+
+ for(var i=0;i<val.length;i++) {
+ var decoded=val.charCodeAt(i);
+ if((decoded>64 && decoded<91) || (decoded>96 && decoded<123)) {
+ if((decoded-13)<65 || (decoded>96 && (decoded-13)<97)) {
+ decoded=(decoded-13)+26;
+ } else {
+ if(decoded>96 && (decoded-13)<97) {
+ decoded+=13;
+ } else {
+ decoded-=13;
+ }
+ }
+ ret+=String.fromCharCode(decoded);
+ } else {
+ ret+=val[i];
+ }
+ }
+
+ return ret;
+}
+
+function PrintUserAssistEntry(key,val,os) {
+ var run_count;
+ var last_run;
+
+ switch(os) {
+ case "winxp":
+ run_count=RegistryKeyValueToVariant(val.value,"uint32",4);
+
+ break;
+ case "win7":
+ run_count=RegistryKeyValueToVariant(val.value,"uint32",4,0,1);
+ last_run=RegistryKeyValueToVariant(val.value,"filetime",60);
+ break;
+ }
+
+ PrintTableRow(key,run_count,last_run);
+}
+
+println("<html>");
+println(" <head><title>Launched Applications</title></head>");
+println(" <body style=\"font-size:12\">");
+println(" <h2>Launched applications</h2>");
+
+// First, we need to find the correct GUID for the current Windows version
+var path;
+var apps;
+var os;
+
+// Windows XP
+os="winxp";
+path="\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{5E6AB780-7743-11CF-A12B-00AA004AE837}\\Count";
+apps=GetRegistryKeys(path);
+
+// TODO: Determine GUIDs for Vista / Win8
+
+if(!IsValid(apps)) {
+ // Windows 7
+ os="win7";
+ path="\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA}\\Count";
+ apps=GetRegistryKeys(path);
+}
+
+
+
+
+if(IsValid(apps)) {
+ if(apps.length!=0) {
+ println(" <p style=\"font-size:12\">");
+ println(" <table style=\"margin-left:20px; font-size:12\">");
+ println(" <tr><td><b>Application</b></td><td style=\"padding:2px\"><b>Run count</b></td><td style=\"padding:2px\"><b>Last run</b></td></tr>");
+
+ for(var i=0;i<apps.length;i++) {
+ var val=GetRegistryKeyValue(path,apps[i]);
+ PrintUserAssistEntry(Rot13Decode(apps[i]),val,os);
+ }
+
+ println(" </table>");
+ println(" </p>");
+ } else {
+ println(" <p><font color='red'>");
+ println(" The list of launched applications is empty.");
+ println(" </font></p>");
+ }
+} else {
+ println(" <p><font color='red'>");
+ println(" This registry hive does not contain a list of launched applications!");
+ println(" </font></p>");
+}
diff --git a/trunk/report_templates/NTUSER_RecentDocs.qs b/trunk/report_templates/NTUSER_RecentDocs.qs
index c4359a5..112669d 100644
--- a/trunk/report_templates/NTUSER_RecentDocs.qs
+++ b/trunk/report_templates/NTUSER_RecentDocs.qs
@@ -1,36 +1,41 @@
function IsValid(val) {
if(typeof val !== 'undefined') return true;
else return false;
}
println("<html>");
println(" <head><title>Recent Documents</title></head>");
println(" <body style=\"font-size:12\">");
println(" <h2>Recent documents</h2>");
// Get list of recent docs
var recent_docs=GetRegistryKeyValue("\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RecentDocs","MRUListEx");
if(IsValid(recent_docs)) {
- println(" <p style=\"font-size:12\">");
- println(" <table style=\"margin-left:20px; font-size:12\">");
- println(" <tr><td><b>Document</b></td></tr>");
-
// Iterate over all recent docs
var i=0;
var runlist=RegistryKeyValueToVariant(recent_docs.value,"uint32",i);
- while(Number(runlist)!=0xffffffff) {
- var entry=GetRegistryKeyValue("\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RecentDocs",runlist.toString(10));
- println(" <tr><td>",RegistryKeyValueToVariant(entry.value,"utf16",0),"</td></tr>");
- i+=4;
- runlist=RegistryKeyValueToVariant(recent_docs.value,"uint32",i);
- }
+ if(Number(runlist)!=0xffffffff) {
+ println(" <p style=\"font-size:12\">");
+ println(" <table style=\"margin-left:20px; font-size:12\">");
- println(" </table>");
- println(" </p>");
+ while(Number(runlist)!=0xffffffff) {
+ var entry=GetRegistryKeyValue("\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RecentDocs",runlist.toString(10));
+ println(" <tr><td style=\"white-space:nowrap\">",RegistryKeyValueToVariant(entry.value,"utf16",0),"</td></tr>");
+ i+=4;
+ runlist=RegistryKeyValueToVariant(recent_docs.value,"uint32",i);
+ }
+
+ println(" </table>");
+ println(" </p>");
+ } else {
+ println(" <p><font color='red'>");
+ println(" The list of recent documents is empty.");
+ println(" </font></p>");
+ }
} else {
println(" <p><font color='red'>");
println(" This registry hive does not contain a list of recent documents!");
println(" </font></p>");
}
println("</html>");
diff --git a/trunk/report_templates/NTUSER_TypedUrls.qs b/trunk/report_templates/NTUSER_TypedUrls.qs
index a94144d..b581da7 100644
--- a/trunk/report_templates/NTUSER_TypedUrls.qs
+++ b/trunk/report_templates/NTUSER_TypedUrls.qs
@@ -1,31 +1,36 @@
function IsValid(val) {
if(typeof val !== 'undefined') return true;
else return false;
}
println("<html>");
println(" <head><title>Typed Urls</title></head>");
println(" <body style=\"font-size:12\">");
println(" <h2>Typed urls</h2>");
// Iterate over all typed urls
var typed_urls=GetRegistryKeys("\\Software\\Microsoft\\Internet Explorer\\TypedURLs");
if(IsValid(typed_urls)) {
- println(" <p style=\"font-size:12\">");
- println(" <table style=\"margin-left:20px; font-size:12\">");
- println(" <tr><td><b>Url</b></td></tr>");
+ if(typed_urls.length!=0) {
+ println(" <p style=\"font-size:12\">");
+ println(" <table style=\"margin-left:20px; font-size:12\">");
- for(var i=0;i<typed_urls.length;i++) {
- var val=GetRegistryKeyValue("\\Software\\Microsoft\\Internet Explorer\\TypedURLs",typed_urls[i]);
- println(" <tr><td>",RegistryKeyValueToString(val.value,val.type),"</td></tr>");
- }
+ for(var i=0;i<typed_urls.length;i++) {
+ var val=GetRegistryKeyValue("\\Software\\Microsoft\\Internet Explorer\\TypedURLs",typed_urls[i]);
+ println(" <tr><td style=\"white-space:nowrap\">",RegistryKeyValueToString(val.value,val.type),"</td></tr>");
+ }
- println(" </table>");
- println(" </p>");
+ println(" </table>");
+ println(" </p>");
+ } else {
+ println(" <p><font color='red'>");
+ println(" The list of typed urls is empty.");
+ println(" </font></p>");
+ }
} else {
println(" <p><font color='red'>");
println(" This registry hive does not contain a list of typed urls!");
println(" </font></p>");
}
println("</html>");
diff --git a/trunk/report_templates/NTUSER_Windows7_SearchKeywords.qs b/trunk/report_templates/NTUSER_Windows7_SearchKeywords.qs
new file mode 100644
index 0000000..effaa23
--- /dev/null
+++ b/trunk/report_templates/NTUSER_Windows7_SearchKeywords.qs
@@ -0,0 +1,41 @@
+function IsValid(val) {
+ if(typeof val !== 'undefined') return true;
+ else return false;
+}
+
+println("<html>");
+println(" <head><title>Document And Folder Search Keywords</title></head>");
+println(" <body style=\"font-size:12\">");
+println(" <h2>Document and folder search keywords</h2>");
+
+// Get list of search keys
+var mrulist=GetRegistryKeyValue("\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\WordWheelQuery","MRUListEx");
+if(IsValid(mrulist)) {
+ // Iterate over all items
+ var i=0;
+ var runlist=RegistryKeyValueToVariant(mrulist.value,"uint32",i);
+ if(Number(runlist)!=0xffffffff) {
+ println(" <p style=\"font-size:12\">");
+ println(" <table style=\"margin-left:20px; font-size:12\">");
+
+ while(Number(runlist)!=0xffffffff) {
+ var entry=GetRegistryKeyValue("\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\WordWheelQuery",runlist.toString(10));
+ println(" <tr><td style=\"white-space:nowrap\">",RegistryKeyValueToVariant(entry.value,"utf16",0),"</td></tr>");
+ i+=4;
+ runlist=RegistryKeyValueToVariant(mrulist.value,"uint32",i);
+ }
+
+ println(" </table>");
+ println(" </p>");
+ } else {
+ println(" <p><font color='red'>");
+ println(" The list of document and search keywords is empty.");
+ println(" </font></p>");
+ }
+} else {
+ println(" <p><font color='red'>");
+ println(" This registry hive does not contain a list of document and folder search keywords!");
+ println(" </font></p>");
+}
+
+println("</html>");
diff --git a/trunk/report_templates/NTUSER_Windows7_TypedPaths.qs b/trunk/report_templates/NTUSER_Windows7_TypedPaths.qs
new file mode 100644
index 0000000..4411897
--- /dev/null
+++ b/trunk/report_templates/NTUSER_Windows7_TypedPaths.qs
@@ -0,0 +1,36 @@
+function IsValid(val) {
+ if(typeof val !== 'undefined') return true;
+ else return false;
+}
+
+println("<html>");
+println(" <head><title>Typed Paths</title></head>");
+println(" <body style=\"font-size:12\">");
+println(" <h2>Typed paths</h2>");
+
+// Iterate over all typed paths
+var urls=GetRegistryKeys("\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\TypedPaths");
+if(IsValid(urls)) {
+ if(urls.length!=0) {
+ println(" <p style=\"font-size:12\">");
+ println(" <table style=\"margin-left:20px; font-size:12\">");
+
+ for(var i=0;i<urls.length;i++) {
+ var val=GetRegistryKeyValue("\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\TypedPaths",urls[i]);
+ println(" <tr><td style=\"white-space:nowrap\">",RegistryKeyValueToString(val.value,val.type),"</td></tr>");
+ }
+
+ println(" </table>");
+ println(" </p>");
+ } else {
+ println(" <p><font color='red'>");
+ println(" The list of typed paths is empty.");
+ println(" </font></p>");
+ }
+} else {
+ println(" <p><font color='red'>");
+ println(" This registry hive does not contain a list of typed paths!");
+ println(" </font></p>");
+}
+
+println("</html>");
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Dec 24, 3:07 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1176828
Default Alt Text
(23 KB)
Attached To
Mode
rFRED fred
Attached
Detach File
Event Timeline
Log In to Comment