逆向工程课程小结,授课老师:孙聪
一些基础概念
什么是逆向工程?软件逆向工程?具体阶段? 1
2
3
4
5
6答:从对象中提取知识或设计信息,并重建对象或基于对象信息的任意事物的过程
一种分析目标系统的过程,其目的是识别出系统的各个组件以及他们之间的关系,并在较高的抽象层次上以
另一种形式创建对系统的表示,从而理解目标系统的结构和行为。
主要有两个阶段:收集信息和抽象信息阶段。前者涉及有解释器、调试器等,后者主要用于构造可理解的高层模型(动态或静态)
逆向工程主要的活动? 1
答:反汇编(撤销汇编过程)、分析、可能的修改
逆向工程中恶意代码分析的静态方法和动态方法区别? 1
2答:静态方法:分析但不运行代码;相比较动态方法更为安全;使用如IDA Pro等的反汇编器
动态方法:检查进程执行过程中寄存器、内存值的实时变化;允许操作操作进程;要使用到调试器
举几个软件逆向工程的主要应用? 1
答:恶意代码分析、闭源软件的漏洞分析、闭源软件的互操作性分析、衡量编译器性能
说说逆向工程的困难性? 1
答:编译过程中造成的信息损失、由于编译属于多对多操作,所以可能反编译的源文件有偏差、不同编程语言的反编译器不同
什么是x86系统?什么是x64系统? 1
2答:x86是基于Intel 8086/8088处理器的一系列向后兼容的指令体系结构(ISA)的总称
x64又称为x86-64,是x86体系结构的扩展。是与x86(IA-32)兼容的64位CPU体系结构
小端序大端序? 1
2答:小端序是低位字节储存在内存中的低位地址,效率较高(Intel使用),而大端序是低位字节储存在内存中高位地址,符合一般的思维逻辑
RISC架构处理器使用(MIPS和PowerPC采用)
保护模式下用户程序运行在哪一层? 1
答:Ring3层,即最低权限级别,程序金科以读写系统设置的一个子集
IA-32内存模型有哪几种? 1
2答:平面内存模型(代码数据和栈在线性地址空间中,大小为0~2^31 - 1)
分段内存模型(代码、数据和栈在不同的段中)
什么是反汇编? 1
答:将二进制文件中的机器码转化为汇编代码或等价的中间语言代码
反汇编器分为? 1
2
3
4
5
6
7答:动态反汇编器和静态反汇编器
前者会执行二进制码,记录执行路径,并对记录执行路径进行解码
优点:精确,可对混淆的二进制进行反汇编
缺点:记录路径需要时间,且一次只能反汇编一条执行路径
后者不会执行文件,而是直接反汇编
优点:快,能覆盖多于一条执行路径
缺点:复杂,难以处理代码混淆
静态反汇编的挑战? 1
答:变长指令集、数据嵌入在代码中、间接跳转/调用
可执行程序中的元信息都可以有啥? 1
答:符号表(源代码的符号信息,由链接器和调试器使用)、重定位信息、调试信息(由调试器生成)
ELF文件由啥构成? 1
2
3
4
5答:ELF头:包含如指令集体系结构、32/64位格式、大端序/小端序、执行入口点、程序头表和节头表等信息
程序头表(PHT):用于执行视图,告诉系统如何在内存中创建一个进程
有以下几个段类型:PHDR(保存程序头表本身的位置和大小), LOAD(代表该类型的段会被装载到内存中), INTERP(指向可执行文件的动态连接器的指针), DYNAMIC(保存指向动态链接信息的指针), NOTE(保存操作系统相关的规范信息)
节头表(SHT):用于链接视图,包含名称和类型、请求的内存位置、权限
有下面这几个重要节类型:.interp(程序解释器的路径名), .text(程序代码), .data(已初始化的全局数据), .rodata(只读数据), .bss(未初始化的全局数据), .init(用于进程初始化的可执行指令序列), .fini(用于进程终止的可执行指令序列), .plt(保存过程链接表,其中包含动态连接器调用共享库函数的必须代码), .rel.<x>(节<x>的重定位信息), dynamic(动态链接信息)
ELF文件怎么加载到内存? 1
2答:1. 装载器使用程序头表,将所有LOAD类型的段映射到内存,并赋以权限位
2. 调用动态连接器(INTERP段内指定),传入可执行文件相关信息
段和节的区别? 1
2
3答:1. 段是程序执行的必要组成部分,每个段划分为不同节
2. 程序内存分布的描述是由程序头表来完成的
3. 每个ELF目标文件都有节,但不一定有节头(对程序执行非必须。主要用于链接和调试)
PE32文件由啥构成? 1
2
3答:大体分为PE头,PE体和NULL填充
前者由DOS头、Dos存根(可选)、NT头、节区头(各节区在文件或内存中的大小,位置,属性等)
后者由节区构成(包括.text, .data, .rsrc)
IA-32指令集
(待补充)
静态反汇编算法
1. 线性扫描算法
2. 递归遍历算法
线性扫描算法
主要原理:从代码节区的入口点开始扫描,顺序逐一解码指令,直到代码节区末尾;如果此过程中遇到解码出非法指令的情况,则停止.例:gdb, objdump都是使用的该算法。(ps:其实就是编译原理中的下推自动机那种)
算法伪代码:
1 | //code: 输入代码节区中的字节 |
优点:简单易实现 缺点:如果代码区有数据可能会误认为是代码
递归遍历算法
主要原理:沿着反汇编过程中生成的控制流图对指令进行反汇编 例:IDA
Pro使用该算法 伪代码 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15worlist = {0}; //可理解为待消费队列
processed = {}; //可理解为消费者队列
while(worklist <> {}){
offset = removeOneNode(Worklist); //取出一个工作量,记下偏移
processed = processed ∪ {offset}; //将该偏移加入工作队列
(instr, size) = decode(code, offset);//解码
switch (instr): //根据解码的内容进行对应的流程
case non-control-flow-inst : add(offset+size);
case unconditional-jmp(dest : add(dest));
case cond-jmp(dest1, dest2 : add(dest1 ; add(dest2)));
...
}
Procedure add (offset):
if (offs t ∉ processe ) then worklist = worklist ∪ {offset}
缺点:对于间接跳转/调用(indiret jumps/calls),很难决定控制流边到底指向哪里
常用Windows核心API
- CreateRemoteThread:创建一个在另一个进程的地址空间中运行的线程
1 | HANDLE CreateRemoteThread( |
- OpenProcess 返回现有进程对象的句柄
1 | HANDLE OpenProcess( |
- CreateThread 创建一个在调用进程的地址空间内执行的线程
1 | CreateThread( |
- VirtualAllocEx 翻转、提交或更改指定进程虚拟空间中一个内存区域的状态
1 | LPVOID VirtualAllocEx( |
- WriteProcessMemory 在指定的进程中写入内存。要写入的整个区域必须可访问,否则操作失败
1 | BOOL WriteProcessMemory( |
- GetProcAddress 返回指定的导出动态链接库(DLL)函数的地址
1 | FARPROC GetProcAddress( |
- WaitForSingleObject 等待指定的对象超时或被信号通知
1 | DWORD WaitForSingleObject( |
- GetModuleFileName 返回指定模块的的文件的完整路径(前提是已经加载)
1 | DWORD GetModuleFileName( |
- GetModuleHandle 返回指定模块文件的句柄
1 | HMODULE GetModuleHandle( |
- Closehandle 关闭一个句柄对象
1 | BOOL CloseHandle( |
- SetWindowsHookEx 将用户定义的钩子加入到钩子链中
1 | HHOOK SetWindowsHookEx( |
HHOOK WINAPI setWindowsHookEx(WH_KEYBOARD(钩取键盘消息), KeyBoardProc(回调函数), g_Instance(HINSTANCE类型的dll句柄), 0)
- CallNextHookEx 将钩子信息传递给当前钩子链中的下一个钩子过程
1 | LRESULT CallNextHookEx( |
- UnhookWindowsHookEx 移除由SetWindowsHookEx安装到钩子链中的钩子
1 | BOOL UnhookWindowsHookEx( |
DLL注入的基本概念和方法
DLL注入是指向运行中的其他进程强制插入特定的DLL文件,即,从进程的外部向目标发出条用LoadLibrary()函数的命令,装载我们的DLL文件,强制调用执行该DLL文件中的DllMain()函数
DLL文件中可以有DllMain()函数,当程序装载该Dll文件时会执行该函数,可以将希望执行的放入DllMain()函数中。 基本方法:
- 使用CreateRemoteThread()函数创建远程线程
- 修改注册表中
AppInit_DLLs
和LoadAppInit_DLLs
两个表项来实现 - 使用SetWindowsHookEx()函数进行消息钩取
方法一:CreateRemoteThread()
主要原理:通过获取要注入的进程的PID,使用OpenProcess()函数来返回目标进程对象的句柄。伪代码:
1 | BOOL InjectDll(DWORD dwPID, LPCTSTR srcDllPath){ |
如果写的再详细一点的话: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath){ //dwPID --- 要注入的进程的PID, szDllPath --- 要注入的DLL(你自己写的)路径
DWORD dwBufSize = (DWORD)(lstrlen(szDllPath) + 1) * sizeof(TCHAR); //计算要放入内存的内容的大小
LPTHREAD_START_ROUTINE pThreadProc; //要使用的线程函数指针
HANDLE hProcess = NULL, hThread = NULL;
LPVOID pRemoteBuf = NULL;
HMOUDLE hMod = NULL;
//先打开进程获取他的句柄:
hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, dwPID)
if (!(hProcess)){
return false;
}
//在该进程中分配空间:
pRemoteBuf = VirtualAlloceEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE)
if (!(pRemoteBuf){
retun false
}
//向新的内存空间内写入数据:使用一个任意类型指针指向LPCTSTR类型的、要加载的DLL名称,大小为上面开的dwBuSize
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
//获得LoadLibraryA函数的地址
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, L"LoadLibraryA");
//创建线程
pThreadProc = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hProcess);
CloseHandle(hThread);
return TRUE; //注入成功
}
方法二:修改注册表项
主要原理:注册表中有当某进程加载User32.dll时,可默认加载一个自定义DLL的表项。
具体为将HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
下的AppInit_DLLs
修改为你要注入的DLL的路径,LoadAppInit_DLLs
修改为1。
方法三:SetWindowsHookEx()
"消息钩子"就是子事件消息从操作系统向应用程序传递的过程中,获取、查看、修改和拦截这些消息的机制
主要原理:将自定义的钩子加入钩子链中。如钩取键盘消息,将SetWindowsHookEx()函数的第一个参数设置为WH_KEYBOARD。伪代码:
1 | //TestHook.c |
DLL卸载的基本原理
DLL的卸载只有通过强制目标进程调用FreeLibrary()
函数来实现,这是其基本原理
代码注入
主要原理:CreateRemoteThread
函数可以更一般化的直接将独立运行的代码插入目标进程中并使之运行,这一技术称为代码注入/线程注入技术。
和DLL注入的区别
1. DLL注入时,其程序代码所使用的数据存在于DLL的数据节区,所以注入时一同被注入进去;而代码注入使用的数据需要预先注入并告知目标代码
2. 相较于DLL注入,其优点是占用内存少且难以被检测到;缺点是复杂的多。
伪代码:
1 | //核心数据结构 |
调试方式的API钩取
一共有三种方式进行API的钩钩取:调试方式的钩取、修改IAT(Import Address Table)表、修改API代码
调试方式的钩取
下面函数实现的是当被调试函数触发了事件后,钩取writeFile函数的代码,并将notepad.exe要保存到文件的内容后移一个字母保存 一般步骤: 1. 将需要被钩取的进程设置为被调试者 2. 将被调试者的API的第一个字节改为0xCC 3. 在被调试者进程中该API调用时,被调试者挂起,控制权转入调试器 4. 调试器执行预定义操作 5. 脱钩(将第一字节值改回去),恢复被调用者进程,使其执行正常API功能 6. 调试器再次钩取该API,将第一个字节值改为0xCC 7. 转3
伪代码:
1 | BYTE INT3 = 0xCC |
下面是上述的代码的简单流程图:
修改IAT表
通过更改PE文件的IAT表,将API函数的地址修改为钩取函数的地址,自己定义的钩取函数中进行完要执行的操作后再调用kernel32.dll中的正常API功能
修改API代码
DLL映射进目标进程的地址空间后,直接查找目标API的实际地址,直接修改其代码