锐英源软件
第一信赖

精通

英语

开源

擅长

开发

培训

胸怀四海 

第一信赖

当前位置:锐英源 / 开源技术 / C++行业技术开源 / SRDF-开发自己的调试器轻松创建恶意软件分析工具和防病毒工具
服务方向
人工智能数据处理
人工智能培训
kaldi数据准备
小语种语音识别
语音识别标注
语音识别系统
语音识别转文字
kaldi开发技术服务
软件开发
运动控制卡上位机
机械加工软件
量化预测
股票离线分析软件
软件开发培训
Java 安卓移动开发
VC++
C#软件
汇编和破解
驱动开发
联系方式
固话:0371-63888850
手机:138-0381-0136
Q Q:396806883
微信:ryysoft

SRDF-开发自己的调试器轻松创建恶意软件分析工具和防病毒工具


以前研究过文件驱动源代码,也用过APIMonitor,对于这方面的机制有兴趣,看到有个例子能够进行文件系统接口过滤,非常有兴趣,共享给大家,共同提高。

 

1. 简介:
是否要编写自己的调试器?...您是否有新技术,并看到 OllyDbg 或 IDA Pro 等已知产品没有这项技术?…您是否在 OllyDbg 和 IDA Pro 中编写插件,但需要将其转换为单独的应用程序?…这篇文章是给你的。
在本文中,我将教您如何使用安全研究与开发框架 (SRDF) 编写一个功能齐全的调试器...如何反汇编说明、收集过程信息和使用 PE 文件...以及如何设置断点和使用调试器。
2. 为什么要调试?
调试通常用于检测应用程序错误并跟踪其执行情况......而且,当您没有此应用程序的源代码时,它用于逆向工程和分析应用程序。
逆向工程主要用于检测漏洞、分析恶意软件或破解应用程序。
我们不会在本文中讨论如何使用调试器来实现这些目标......但我们将介绍如何使用 SRDF 编写调试器...以及如何基于它实施您的想法。
3. 安全研发框架:
这是一个免费的开源开发框架,旨在支持编写安全工具和恶意软件分析工具。并将安全研究和思想从理论方法转化为实际实施。
此开发框架主要是为了支持恶意软件领域,无需重新发明轮子即可轻松创建恶意软件分析工具和防病毒工具,并激发创新者在该领域撰写研究成果并使用 SRDF 实施这些研究。
在用户模式部分,SRDF 为您提供了许多有用的工具...他们是:
汇编器和反汇编器
x86 仿真器
调试器
PE 解析器、ELF 解析器、PDF 和 Android 解析器
进程分析器(加载的 DLL、内存映射...等)
MD5、SSDeep 和野生名单扫描程序 (YARA)
API Hooker、IAT Hooker 和进程注入
后端数据库、XML 序列化程序
Pcap File Anaylzer
数据包分析
协议分析,如:TCP、UDP、ICMP、HTTP、DNS 等
网络流分析和网络分离
以及更多
在 Kernel-Mode 部分中,它尝试轻松编写自己的筛选器设备驱动程序 (而不是使用 WDF 和回调) ,并提供一个简单的、面向对象的 (尽可能多的) 开发框架,其中包含以下功能:
面向对象且易于使用的开发框架
Easy IRP 调度机制
SSDT Hooker
分层设备筛选
TDI 防火墙
文件和注册表管理器
内核模式易于使用的 Internet 套接字
Filesystem 过滤器
Kernel-Mode 仍在开发中,许多功能将在不久的将来添加。
查看其网站: www.security-framework.com 在 Twitter
上关注我们: @winSRDF
4. 收集有关流程的信息:
如果您决定调试正在运行的应用程序,或者启动应用程序进行调试。您需要收集有关要调试的此进程的信息,例如:
进程内分配的内存区域
应用程序在其内存中的位置以及应用程序在内存中的大小
在应用程序内存中加载的 DLL
读取内存中的特定位置
此外,如果您需要附加到已在运行的进程...您还需要知道此应用程序的 Process Filename 和命令行
4.1. 开始过程分析:
要收集有关内存中进程的信息,您应该创建一个 cProcess 类的对象,给定您需要分析的进程的 ProcessId。
C
cProcess myProc(792);
如果您只有进程名称而没有进程 ID,则可以从 SRDF 中的 ProcessScanner 获取进程 ID,如下所示:
C
cProcessScanner ProcScan;
然后从 cProcessSanner 类内的 ProcessList 字段中获取进程名称和 ID 的哈希值...而这个项目是 cHash 类的对象。
cHash 类是一个创建来表示 key 和 value 的哈希值的类......它们之间的关系是一对多的......因此,每个 key 可以有多个值。
在我们的例子中,键是进程名称,值是进程 ID。您可能会看到系统上正在运行多个同名进程。例如,要获取进程 "Explorer.exe" 的第一个 ProcessId...您将执行此操作:
C
ProcScan.ProcessList["explorer.exe"]
这将返回一个 cString 值,其中包括进程的 ProcessId。要将其转换为整数,您将使用 atoi() 函数...喜欢这个:
C
atoi(ProcScan.ProcessList["explorer.exe"])
4.2. 获取分配的内存:
要获取分配的内存区域,有一个名为 MemoryMap 的内存区域列表,此 Item 的类型为 cList。
cList 是一个类,用于表示具有固定大小的缓冲区列表或特定结构的数组。它有一个名为 GetNumberOfItems 的函数,此函数获取列表中的项目数。在下面的代码中,我们将了解如何使用 cList 函数获取内存区域列表。
C
for(int i=0; i<(int)(myProc->MemoryMap.GetNumberOfItems()) ;i++)
{
cout<<"Memory Address "<< ((MEMORY_MAP*)myProc->MemoryMap.GetItem(i))->Address;
cout << " Size: "<<hex<<((memory_map*)myproc->MemoryMap.GetItem(i))->Size << endl;
}
结构体 MEMORY_MAP 描述进程内的内存区域 ...它是:
C
struct MEMORY_MAP
{
DWORD Address;
DWORD Size;
DWORD Protection;
};
在前面的代码中,我们遍历 MemoryMap List 的项,并得到每个内存区域的地址和大小。
4.3. 获取应用程序信息:
要在内存中放置应用程序...您只需在 cProcess 类中获取 Imagebase 和 SizeOfImage 字段,如下所示:
C
cout<<"Process: "<< myProc->processName<< endl;
cout<<"Process Parent ID: "<< myProc->ParentID << endl;
cout<< "Process Command Line: "<< myProc->CommandLine << endl;

cout<<"Process PEB:\t"<< myProc->ppeb<< endl;
cout<<"Process ImageBase:\t"<<hex<<>ImageBase<< endl;
cout<<"Process SizeOfImageBase:\t"<<dec<<>SizeOfImage<< " bytes"<< endl;
</dec<<>
如你所见,我们获得了有关进程及其在内存中的位置 (Imagebase) 及其在内存中的大小 (SizeOfImage) 的最重要信息。
4.4. 加载的 DLL 和模块:
加载的 Modules 是 cProcess 类中的一个 cList,名称为 "modulesList",它表示一个结构体 "MODULE_INFO" 数组,如下所示:
C
struct MODULE_INFO
{
DWORD moduleImageBase;
DWORD moduleSizeOfImage;
cString* moduleName;
cString* modulePath;
};
若要获取进程内加载的 DLL,此代码表示如何获取加载的 DLL:
C
for (int i=0 ; i<(int)( myProc->modulesList.GetNumberOfItems()) ;i++)
{
cout<<"Module "<< ((MODULE_INFO*)myProc->modulesList.GetItem(i))->moduleName->GetChar();
cout <<" ImageBase: "<< hex <<((MODULE_INFO*)myProc->modulesList.GetItem(i))->moduleImageBase << endl;
}
4.5. 进程的读取、写入和执行:
为了读取此进程的内存中的位置,cProcess 类为您提供一个名为 Read(...) 的函数,该函数在内存中分配一个空间,然后读取此进程内存中的特定位置并将其复制到内存中(内存中新分配的位置)。
C
DWORD Read(DWORD startAddress,DWORD size)
对于写入进程,您有另一个函数名称 Write ,如下所示:
C
DWORD Write (DWORD startAddressToWrite ,DWORD buffer ,DWORD sizeToWrite)
此函数将代替您要写入的位置,即进程中包含要写入的数据的缓冲区以及缓冲区的大小。
如果 startAddressToWrite 为 null ...Write() 函数将在内存中分配一个要写入的位置,并将指针返回给该位置。
要在进程内仅分配一个空间...你可以使用 Allocate() 函数在进程内分配内存,就像这样:
C
Allocate(DWORD preferedAddress,DWORD size)
您还可以选择通过在进程内创建新线程来在此进程内执行代码,或者使用这些函数在进程内注入 DLL
C
DWORD DllInject(cString DLLFilename)
DWORD CreateThread (DWORD addressToFunction , DWORD addressToParameter)
这些函数返回新创建的线程的 ThreadId。
5.调试应用程序:
要编写成功的调试器,您需要在调试器中包含以下功能:
可以附加到正在运行的进程或打开 EXE 文件并对其进行调试
可以收集寄存器值并对其进行修改
可以在特定地址上设置 Int3 断点
可以设置硬件断点(读取、写入或执行时)
可以设置内存断点(在内存中的特定页面上读取、写入或执行时)
可以在运行时暂停应用程序
可以处理异常、加载或卸载 dll 或者创建或终止线程等事件。
在这一部分中,我们将介绍如何使用 SRDF 的 Debugger Library 轻松地完成所有这些操作。
5.1. 打开 exe 文件并调试 ...或 附加到进程:
要打开 EXE 文件并对其进行调试,请执行以下操作:
C
cDebugger* Debugger = new cDebugger("C:\\upx01.exe");
或者使用命令行:
C
cDebugger* Debugger = new cDebugger("C:\\upx01.exe","xxxx");
如果文件成功打开,您将看到 cDebugger 类中的 IsFound 变量设置为 TRUE。如果发生任何问题(找不到文件或其他任何内容),您将看到它等于 FALSE。在继续之前,请务必检查此字段。
如果要调试正在运行的进程...您将使用所需的 ProcessId 创建一个 cProcess 类,然后将调试器附加到该类:
C
cDebugger* Debugger = new cDebugger(myProc);
要开始运行应用程序...您将像这样使用函数 Run():
C
Debugger->Run();
或者你只能使用函数 Step() 运行一条指令,如下所示:
C
Debugger->Step();
此函数返回以下输出之一(到目前为止,可以扩展):
DBG_STATUS_STEP
DBG_STATUS_HARDWARE_BP
DBG_STATUS_MEM_BREAKPOINT
DBG_STATUS_BREAKPOINT
DBG_STATUS_EXITPROCESS
DBG_STATUS_ERROR
DBG_STATUS_INTERNAL_ERROR
如果返回 DBG_STATUS_ERROR,您可以检查 ExceptionCode 字段和 debug_event 字段以获取更多信息。
5.2. 获取和修改寄存器:
要从调试器获取寄存器...你有 cDebugger 类中的所有寄存器,如下所示:
Reg [0 -> 7]
Eip
EFlags
DebugStatus -> DR7 用于硬件断点
要更新它们,您可以修改这些变量,然后在修改后使用函数 "UpdateRegisters()" 以使其生效。
5.3. 设置 Int3 断点:
调试器的主要断点是指令 "int3",它以二进制(或本机)形式转换为字节 "0xCC"。调试器在他们需要中断的指令的开头写入 int3 字节。
之后,当执行到达此指令时,应用程序将停止并返回调试器,但异常为 STATUS_BREAKPOINT。
要设置 Int3 断点,调试器具有一个名为 SetBreakpoint(...) 的函数,如下所示:
C
Debugger->SetBreakpoint(0x004064AF);
您可以像这样为断点设置 UserData:
C
DBG_BREAKPOINT* Breakpoint = GetBreakpoint(DWORD Address);
断点结构体是这样的:
C
struct DBG_BREAKPOINT
{
DWORD Address;
DWORD UserData;
BYTE OriginalByte;
BOOL IsActive;
WORD wReserved;
};
所以,你可以为自己设置一个 UserData ...比如指向另一个结构体或其他东西的指针,并为每个断点设置它。
当调试器的 Run() 函数返回 "DBG_STATUS_BREAKPOINT" 时,您可以通过 Eip 获取断点结构 "DBG_BREAKPOINT" 并从内部获取 UserData...并操纵有关此断点的信息。
此外,您可以通过在 cDebugger 类中使用名为"LastBreakpoint"的 Variable 来获取最后一个断点,如下所示:
C
cout << "LastBp: " << Debugger->LastBreakpoint << "\n";
要停用断点,您可以使用函数 RemoveBreakpoint(...),如下所示:
C
Debugger->RemoveBreakpoint(0x004064AF);
5.4. 设置硬件断点:
硬件断点是基于 CPU 中调试寄存器的断点。这些断点可以在访问或写入内存中的某个位置时停止,也可以在对地址执行时停止。而且你只有 4 个可用的断点。如果需要添加更多,则必须删除一个。
这些断点不会修改应用程序的二进制文件来设置断点,因为它们不会将 int3 字节添加到地址以在其上停止。因此,它们可用于在打包的代码上设置一个断点,以便在解压缩时中断。
要将硬件断点设置为内存中的某个位置(用于访问、写入或执行),您可以像这样设置它:
C
Debugger->SetHardwareBreakpoint(0x00401000,DBG_BP_TYPE_WRITE,DBG_BP_SIZE_2);
Debugger->SetHardwareBreakpoint(0x00401000,DBG_BP_TYPE_CODE,DBG_BP_SIZE_4);
Debugger->SetHardwareBreakpoint(0x00401000, DBG_BP_TYPE_READWRITE,DBG_BP_SIZE_1);
仅对于代码,请使用 DBG_BP_SIZE_1 代码。但是其他的,你可以使用等于 1 字节、2 字节或 4 字节的大小。
如果您没有用于断点的空闲位置,则此函数返回 false。因此,您必须为此删除一个断点。
要删除此断点,您将使用函数 RemoveHardwareBreakpoint(...),如下所示:
C
Debugger->RemoveHardwareBreakpoint(0x004064AF);
5.5. 设置内存断点:
内存断点是很少看到的断点。它们并不完全在 OllyDbg 或 IDA Pro 中,但它们是很好的断点。它类似于 OllyBone。
这些断点基于内存保护。如果您在写入时设置了断点,它们会将 read/write place in memory 设置为只读。或者,如果您设置了读/写断点等,则将内存中的位置设置为 no access。
这种类型的断点没有限制,但它在内存页上设置一个大小为 0x1000 字节的断点。所以,它并不总是准确的。并且您只有 Access 上的断点和 write 上的断点 要设置断点,您可以这样做:
C
Debugger->SetMemoryBreakpoint(0x00401000,0x2000,DBG_BP_TYPE_WRITE);
当 Run() 函数返回 DBG_STATUS_MEM_BREAKPOINT 时,将触发 Memory Breakpoint。您可以使用 cDebugger 类变量获取访问的内存位置(确切地):"LastMemoryBreakpoint"
您还可以通过使用 GetMemoryBreakpoint(...) 和内存内设置断点的任何指针(从 Address 到 (Address + Size))来设置类似于 Int3 断点的 UserData。它返回一个指向结构体 "" 的指针,该结构体描述内存断点,你可以在其中添加用户数据
C
struct DBG_MEMORY_BREAKPOINT
{
DWORD Address;
DWORD UserData;
DWORD OldProtection;
DWORD NewProtection;
DWORD Size;
BOOL IsActive;
CHAR cReserved; //they are written for padding
WORD wReserved;
};
你可以看到里面真正的内存保护,并且可以在断点内设置你的用户数据。
要删除断点,您可以使用 RemoveMemoryBreakpoint(Address) 删除断点。
5.6. 暂停应用程序:
要在运行时暂停应用程序,您需要在执行 Run() 函数之前创建另一个线程。此线程将调用 Pause() 函数来暂停应用程序。此函数将调用 SuspendThread 以暂停 debuggee 进程 (正在调试的进程) 内的调试线程。
要再次恢复,您应该调用 Resume(),然后再次调用 Run()。
您还可以通过调用 Terminate() 函数来终止调试对象进程。或者,如果需要退出调试器并让调试对象进程继续,则可以使用 Exit() 函数分离调试器。
5.7. 处理事件:
要处理调试器事件(加载新 DLL、卸载新 DLL、创建新线程等),您有 5 个函数来接收这些事件的通知,它们是:
DLLLoadedNotify例程
DLLUnloadedNotify例程
ThreadCreatedNotify例程
ThreadExitNotify例程
ProcessExitNotify例程
您需要从 cDebugger 类继承并覆盖这些函数以获取通知。
要获取有关 Event 的信息,您可以从 debug_event 变量中获取信息
6. PE 文件格式:
DOS MZ 接头
DOS 存根
PE 接头
Section表
Section 1
Section 2
Section 3
Section n

我们将介绍 PE 标头(EXE 标头),以及如何从它和 SRDF 中的 cPEFile 类(PE 解析器)中获取信息。
EXE 文件以"MZ"字符和 DOS 标头(名为 MZ 标头)开头。此 DOS 标头用于 EXE 文件开头的 DOS 应用程序。
如果这个 DOS 应用程序在 DOS 上运行,则创建它是为了说"它不是 win32 应用程序"。
MZ Header 包含偏移量(从 File 的开头)到 PE Header 的开头。PE 标头是 Win32 应用程序的 Real 标头

DOS PE Header
Signature: PE,0,0
File Header
Optional Header
Data Directory

它以签名"PE"和 2 个 null 字节开头,然后是 2 个标头:文件标头和可选标头。
为了在 Debugger 中获取 PE 标头,cPEFile 类包含指向它的指针(在 Process Application File 的 Memory Mapped File 中),如下所示:
C
cPEFile* PEFile = new cPEFile(argv[1]);
image_header* PEHeader = PEFile->PEHeader;
File Header 包含节数(将进行描述)并包含此应用程序应运行的 CPU 架构和型号......如 Intel x86 32 位等。
此外,它还包括可选标头 (下一个标头) 的大小,并包括应用程序的特征 (EXE 文件或 DLL) 。

Optional Header 包含有关 PE 的重要信息,如下表所示:
田 意义
AddressOfEntryPoint 执行文件的开始
ImageBase 图像库 内存中 PE 文件的开头(默认)
SectionAlignment (分段对齐) 映射时内存中的分段对齐
FileAlignment 硬盘中的段对齐方式(~ 一个扇区)
MajorSubsystemVersion
MinorSubsystemVersion win32 子系统版本
SizeOfImage 内存中PE 文件的大小
SizeOfHeaders 所有标头大小的总和
Subsystem GUI、控制台、驱动程序或其他
DataDirectory 指向重要 Headers 的指针数组

要从 SRDF 中的 cPEFile 类获取此信息...类中有以下变量:
C
bool FileLoaded;
image_header* PEHeader;
DWORD Magic;
DWORD Subsystem;
DWORD Imagebase;
DWORD SizeOfImage;
DWORD Entrypoint;
DWORD FileAlignment;
DWORD SectionAlignment;
WORD DataDirectories;
short nSections;
DataDirectory 是指向其他 Headers 的指针数组(可选的 Headers ...或者指针可以为 null)和 Header 的大小。
它包括:
导入表 :从 DLL 导入 API
导出表 :将 API 导出到其他应用程序
资源表 :用于图标、图像等
Relocables Table :用于重新定位 PE 文件(将其加载到不同位置...与 Imagebase 不同)
我们包括 Import Table ...因为它包含所有导入的 DLL 和 API 的数组,如下所示:
C
cout << PEFile->ImportTable.nDLLs << "\n";
for (int i=0;i < PEFile->ImportTable.nDLLs;i++)
{
cout << PEFile->ImportTable.DLL[i].DLLName << "\n";
cout << PEFile->ImportTable.DLL[i].nAPIs << "\n";
for (int l=0;l<pefile->ImportTable.DLL[i].nAPIs;l++)
{
cout << PEFile->ImportTable.DLL[i].API[i].APIName << "\n";
cout <<pefile->ImportTable.DLL[i].API[i].APIAddressPlace << "\n";
}
}
</pefile->
在 Headers 之后,有部分 Headers。应用程序文件分为以下几个部分:代码部分、数据部分、资源(图像和图标)部分、导入表部分等。
部分是可扩展的 ...所以你可以看到它在硬盘(或文件)中的大小小于内存中的大小(作为一个进程加载时)......因此,下一部分的位置将与 Harddisk 和 memory 不同。
作为进程加载时,相对于内存中文件开头的部分地址命名为 RVA(相对虚拟地址)...并且该节相对于硬盘中文件开头的地址名为 Offset 或 PointerToRawData

,这是该节 Header 提供的信息:
成员 意义
Name 部分名称
VirtualAddress 部分的 RVA 地址
VirtualSize 节大小 (in Memory)
SizeOfRawData 节的大小(在硬盘中)
PointerToRawData 指向文件开头 (Harddisk) 的指针
Characteristics 内存保护(执行、读取、写入)

您可以像这样操作 cPEFile 类中的部分:
C
cout << PEFile->nSections << "\n";
for (int i=0;i< PEFile->nSections;i++)
{
cout << PEFile->Section[i].SectionName << "\n";
cout << PEFile->Section[i].VirtualAddress << "\n";
cout << PEFile->Section[i].VirtualSize << "\n";
cout << PEFile->Section[i].PointerToRawData << "\n";
cout << PEFile->Section[i].SizeOfRawData << "\n";
cout << PEFile->Section[i].RealAddr << "\n";
}
Real Address 是 Memory Mapped File 中此部分开头的地址。或者换句话说,在 Opened File(打开的文件)中。
要将 RVA 转换为 Offset 或 Offset 转换为 RVA ...您可以使用以下函数:
C
DWORD RVAToOffset(DWORD RVA);
DWORD OffsetToRVA(DWORD RawOffset);
7. 反汇编者:
要了解如何使用汇编程序和反汇编程序...您应该了解说明的形状等。
这是 x86 指令 Format:
图 2
前缀是保留字节,用于描述 Instruction 中的某些内容,例如:
0xF0:锁前缀 ...它用于同步
0xF2/0xF3: Repne/Rep ...字符串操作的 repeat 指令
0x66: 操作数覆盖 ...对于 16 位操作数,例如:MOV AX,4556
0x67:地址覆盖...用于 16 位 ModRM ...可以忽略
0x64: FS 的段覆盖...喜欢: mov eax, FS:[18]
操作码:
Opcode 对有关
操作类型,
操作
每个操作数的大小,包括直接操作数的大小
Like Add RM/R, Reg (8 bits) à Opcode: 0x00
Opcode 可以是 1 字节、2 或 3 字节
操作码可以使用 ModRM 中的"Reg"作为操作码扩展...,这名为 "Opcode Groups"
modrm:描述操作数(目标和源)。它描述了目标或源是寄存器、内存地址(例如:dword ptr [eax+ 1000])还是即时(数字)。
SIB:Modrm 的扩展 ...用于扩容内存地址,如:DWORD PTR [EAX*4 + ECX + 50]
位移: 方括号 [] ...就像 dword ptr [eax+0x1000] 一样,所以位移0x1000......它可以是 1 个字节、2 个字节或 4 个字节
Immediate:如果其中任何一个是像 (move ax,1000) 这样的数字,则它是源或目标的值......所以立即数是 1000

这就是 x86 指令 格式简介 ...您可以在 Intel 参考手册中找到更多详细信息。
要在 SRDF 中使用 PokasAsm 类进行组装和反汇编...您将创建一个新类并按如下方式使用它:
C
CPokasAsm* Asm = new CPokasAsm();
DWORD InsLength;
char* buff;
buff = Asm->Assemble("mov eax,dword ptr [ecx+ 00401000h]",InsLength);
cout << "The Length: " << InsLength << "\n";
cout << "Assembling mov eax,dword ptr [ecx+ 00401000h]\n\n";
for (DWORD i = 0;i < InsLength; i++)
{
cout << (int*)buff[i] << " ";
}
cout << "\n\n";
cout << "Disassembling the same Instruction Again\n\n";
cout << Asm->Disassemble(buff,InsLength) << " ... and the instruction length : " << InsLength << "\n\n";
输出:

The Length: 6
Assembling mov eax,dword ptr [ecx+ 00401000h]
FFFFFF8B FFFFFF81 00000000 00000010 00000040 00000000
Disassembling the same Instruction Again
mov eax ,dword ptr [ecx + 401000h] ... and the instruction length : 6

此外,我们还添加了一种检索指令信息的有效方法。我们创建了一个 disassemble 函数,该函数返回一个结构体,描述指令 "DISASM_INSTRUCTION",如下所示:
C
struct DISASM_INSTRUCTION
{
hde32sexport hde;
int entry;
string* opcode;
int ndest;
int nsrc;
int other;
struct
{
int length;
int items[3];
int flags[3];
} modrm;
int (*emu_func)(Thread&,DISASM_INSTRUCTION*);
int flags;
};
Disassemble 函数如下所示:
C
DISASM_INSTRUCTION* Disassemble(char* Buffer,DISASM_INSTRUCTION* ins);
它需要要反汇编的缓冲区的 Address 和函数将返回结构体的缓冲区
让我们解释一下这个结构:
hde:这是 Hacker Disassembler Engine 创建的一个结构体,描述了操作码......重要的字段包括:
莱恩: 指令的长度
opcode: 操作码字节 ...如果操作码为 2 字节,则另请参阅 opcode2
标志: 这是标志,它有一些重要的标志,如 "F_MODRM" 和 "F_ERROR_XXXX" (XXXX 在这里表示任何内容)
条目:未使用
Opcode:操作码字符串 ...使用类 "string" 而不是 "cString"
其他:用于 mul 以节省 imm ...除此之外......未使用
Modrm:它是一个结构,描述了 RM 内部的内容(如果有),例如"[eax*2 + ecx + 6]"......它看起来像:
Length: 内页物品数...赞 "[EAX+ 2000]" 包含 2 个项目
Flags[3]:描述 RM 中的每个项目,其最大值为 3 ...It's Flags 是:
RM_REG:该项是类似于 "[EAX ..." 的寄存器
RM_MUL2:此寄存器乘以 2
RM_MUL4:按 4
RM_MUL8:按 8
RM_DISP:它是类似于 "[0x401000 + ..." 的位移
RM_DISP8:随附 RM_DISP ...这意味着位移为 8 位
RM_DISP16:位移为 16 位
RM_DISP32:位移为 32 位
RM_ADDR16:这意味着 ...modrm 处于 16 位寻址模式
Items[3]:这给出了 modrm 中 item 的值......就像 Item 是一个寄存器一样......所以它包含这个收银机的编号(例如:ECX à item = 1)
如果该项目是位移 ...所以它包含位移值,如 "0x401000" 等。
emu_func:未使用
Flags:此标志描述指令 ...一些描述指令形状,一些描述目标,一些描述源......我看看
Instruction Shape:有一些标志描述指令,例如:
NO_SRCDEST:此指令没有像 "nop" 这样的 source 或 destination
SRC_NOSRC:此指令只有类似 "push dest" 的目的地
INS_UNDEFINED:此指令在反汇编器中未定义...但您仍然可以从 hde.len 获取它的长度
OP_FPU:此指令为 FPU 指令
FPU_NULL:表示此指令没有任何目标或源
FPU_DEST_ONLY:这意味着此指令只有一个目的地
FPU_SRCDEST:这意味着此指令具有 source 和 destination
FPU_BITS32:FPU 指令为 32 位
FPU_BITS16:表示 FPU 指令为 16 位
FPU_MODRM:表示该指令包含 ModRM 字节
目标形状:
DEST_REG:表示目标是寄存器
DEST_RM:表示目的地是 RM,如 "dword ptr [xxxx]"
DEST_IMM:目的地是立即的(仅带有 enter 指令)
DEST_BITS32:目标为 32 位
DEST_BITS16:目标是 16 位
DEST_BITS8:目标是 8 位
FPU_DEST_ST:表示目的地在仅 FPU 指令中为 "ST0"
FPU_DEST_STi:表示目的地是 "STx" 和 "ST1" 一样
FPU_DEST_RM:表示目的地为 RM
源形状:类似于目标 ...阅读上面的目标标志中的描述
SRC_REG
SRC_RM
SRC_IMM
SRC_BITS32
SRC_BITS16
SRC_BITS8
FPU_SRC_ST
FPU_SRC_STi
ndest:这包括与其类型相关的目标的值:
如果它是一个寄存器......所以它将包含这个 register 的索引
如果是立即......因此,它将具有 immediate value
如果是 RM ...所以它将为 null
nsrc:这包括与类型 ...请参阅上面的 NDEST

这只是反汇编程序。我们讨论了 debugger 的所有项目。我们讨论了 Process Analyzer、Debugger、PE Parser 和 Disassembler。我们现在应该把所有的东西放在一起
8. 把所有放在一起:
为了编写一个好的调试器,而且操作简单,我们决定创建一个交互式控制台应用程序(如 Metasploit 中的 msfconsole),它接受 run 或 bp(设置断点)等命令。
要创建交互式控制台应用程序,我们将使用 cConsoleApp 类来创建我们的控制台应用程序。我们将从它继承一个类并开始修改它的命令
C
class cDebuggerApp : public cConsoleApp
{
public:
cDebuggerApp(cString AppName);
~cDebuggerApp();
virtual void SetCustomSettings();
virtual int Run();
virtual int Exit();
};
And the Code:
cDebuggerApp::cDebuggerApp(cString AppName) : cConsoleApp(AppName)
{

}
cDebuggerApp::~cDebuggerApp()
{
((cApp*)this)->~cApp();
}

void cDebuggerApp::SetCustomSettings()
{
//Modify the intro of the application
Intro = "\
***********************************\n\
** Win32 Debugger **\n\
***********************************\n";

}
int cDebuggerApp::Run()
{
//write your code here for run
StartConsole();
return 0;
}
int cDebuggerApp::Exit()
{
//write your code here for exit
return 0;
}
如您在前面的代码中所见,我们实现了 3 个函数(虚拟函数),它们是:
SetCustomSettings:此函数用于修改应用程序的设置...就像修改应用程序的简介一样,包括一个日志文件,包括一个应用程序的注册表项,或者包括一个用于应用程序保存数据的数据库......如你所见,它用于编写介绍。
Run:调用该函数运行应用程序。您应该调用 StartConsole 以启动交互式控制台
Exit:当用户向控制台写入 "quit" 命令时,将调用该函数。
cConsoleApp 为您实现 2 个命令 "quit" 和 "help"。退出应用程序并帮助显示命令列表及其描述。要添加新命令,您应该调用以下函数:
C
AddCommand(char* Name,char* Description,char* Format,DWORD nArgs,PCmdFunc CommandFunc)
命令 Func 是用户输入此命令时将调用的函数...它应该采用以下格式:
C
void CmdFunc(cConsoleApp* App,int argc,char* argv[])
它类似于添加到其中的 main 函数 App 类。argv 是此函数的参数列表,argc 是参数的数量(始终等于您在添加命令中输入的 nArgs,因为它是保留的,因此可以忽略)。
要使用 AddCommand ...您可以像这样使用它:
C
AddCommand("dump","Dump a place in memory in hex","dump [address] [size]",2,&DumpFunc);
DumpFunc 是这样的:
C
void DumpFunc(cConsoleApp* App,int argc,char* argv[])
{
((cDebuggerApp*)App)->Dump(argc,argv);
};
当它调用继承自 cConsoleApp 类的 cDebuggerApp 类中的 Dump 函数时。
我们为应用程序添加了以下命令:
C
AddCommand("step","one Step through code","step",0,&StepFunc);
AddCommand("run","Run the application until the first breakpoint","run",0,&RunFunc);
AddCommand("regs","Show Registers","regs",0,&RegsFunc);
AddCommand("bp","Set an Int3 Breakpoint","bp [address]",1,&BpFunc);
AddCommand("hardbp","Set a Hardware Breakpoint","hardbp [address] [size (1,2,4)] [type .. 0 = access .. 1 = write .. 2 = execute]",3,&HardbpFunc);
AddCommand("membp","Set Memory Breakpoint","membp [address] [size] [type .. 0 = access .. 1 = write]",3,&MembpFunc);
AddCommand("dump","Dump a place in memory in hex","dump [address] [size]",2,&DumpFunc);
AddCommand("disasm","Disassemble a place in memory","disasm [address] [size]",2,&DisasmFunc);
AddCommand("string","Print string at a specific address","string [address] [max size]",2,&StringFunc);
AddCommand("removebp","Remove an Int3 Breakpoint","removebp [address]",1,&RemovebpFunc);
AddCommand("removehardbp","Remove a Hardware Breakpoint","removehardbp [address]",1,&RemovehardbpFunc);
AddCommand("removemembp","Remove Memory Breakpoint","removemembp [address]",1,&RemovemembpFunc);
对于 Run Function:
C
int cDebuggerApp::Run()
{

Debugger = new cDebugger(Request.GetValue("default"));
Asm = new CPokasAsm();
if (Debugger->IsDebugging)
{
Debugger->Run();
Prefix = Debugger->DebuggeeProcess->processName;
if (Debugger->IsDebugging)StartConsole();
}
else
{
cout << Intro << "\n\n";
cout << "Error: File not Found";
}
return 0;
}
如您所见,我们让应用程序在用户输入有效文件名时启动控制台,否则返回 error 并关闭应用程序。
我们不会描述所有命令,而是 难以实现的命令 .
C
void cDebuggerApp::Disassemble(int argc,char* argv[])
{
DWORD Address = 0;
DWORD Size = 0;
sscanf(argv[0], "%x", &Address);
sscanf(argv[1], "%x", &Size);
DWORD Buffer = Debugger->DebuggeeProcess->Read(Address,Size+16);
DWORD InsLength = 0;

for (DWORD InsBuff = Buffer;InsBuff < Buffer+ Size ;InsBuff+=InsLength)
{
cout << (int*)Address << ": " << Asm->Disassemble((char*)InsBuff,InsLength) << "\n";
Address+=InsLength;
}
}
此函数在开始时将参数从字符串(如用户输入时)转换为十六进制值。然后,它在 debugee 进程中读取您需要反汇编的内存。如您所见,我们添加了 16 个字节,以确保所有指令都能正确反汇编,即使其中一条指令超过了缓冲区的限制。
然后,我们开始循环反汇编过程,并按每条指令的长度增加地址,直到达到限制大小。
main 函数将调用一些函数来启动应用程序并运行它:
C
int _tmain(int argc, char* argv[])
{
cDebuggerApp* Debugger = new cDebuggerApp("Win32Debugger");
Debugger->SetCustomSettings();
Debugger->Initialize(argc,argv);
Debugger->Run();
return 0;
}
9. 结论:
在本文中,我们介绍了如何使用 SRDF 编写调试器...以及 SRDF 的易用性 .我们还介绍了如何分析 PE 文件以及反汇编指令的工作原理。

友情链接
版权所有 Copyright(c)2004-2024 锐英源软件
统一社会信用代码:91410105098562502G 豫ICP备08007559号 最佳分辨率 1440*900
地址:郑州市金水区文化路97号郑州大学北区院内南门附近