逆向工程核心原理-第28章-使用汇编语言编写注入代码
28.1 目标
借助OD的汇编功能, 使用汇编语言编写注入代码(ThreadProc()函数), 汇编语言能够生成比C语言更自由, 更灵活的代码(如: 直接访问栈, 寄存器的功能等), 然后将汇编语言编写的ThreadProc()函数注入notepad.exe进程
28.2 汇编编程
常用开发工具:
- MASM
- TASM
- FASM
- OD的汇编功能
也可以使用OD的汇编功能进行编程, 我们下面将使用OD
28.3 OD的汇编命令
我们首先使用OD打开asmtest.exe(该程序没有实现任何功能)
然后从顶部开始(滚轮直接往上滑), 使用New origin here(此处为新EIP)将EIP设置到最顶端, 快捷键是(Ctrl + Gray*, Gray就是小键盘, Gray就是小键盘上的键)
然后在该处进行汇编(快捷键: Space)
注意, 要取消勾选”使用 NOP 填充”, 这个的功能是当我们输入的代码短于原先代码的时候, 多出来的字节使用NOP来填充
28.3.1 编写 ThreadProc() 函数
在401033(紧跟在上面汇编的后面)地址处使用Edit选项(Ctrl + E)开始编写字符串
注意后面一定要有一个’\0’
编辑完成后
可以看到OD把这个字符串识别成了代码, 因为这里是代码段, 所以识别为代码非常正常
选中字符串开头后, 执行Analysis命令(快捷键: Ctrl + A)得到下图
这时在401033地址处可以清晰看到我们的字符串”ReverseCore”
但是上面从401000地址处的代码却分析错误, 我们再使用右键 → 分析 → 从模块中删除分析(Remove analysis from module)
又恢复原来的样子了, 我们接着上面的字符串, 继续编写汇编指令
下面感觉识别错误, 导致开头对不齐, 所以直接使用的编辑
下面是编辑完成后的结果
28.3.2 保存文件
保存为asmtest_patch.exe
使用OD打开asmtest_patch.exe, 再内存窗口中跳转至401000处
选中该区域右键
经过处理以后我们得到了机器码序列
然后就是照着书上的进行编写
1 |
|
书上没有给出主函数, 但是我们可以参考上一章的主函数进行编写, 然后就是书上省略了对异常情况的处理这里我们添加上即可(g_InjectionCode中间有一个数0x14写成了0x15, 汇编代码一注入程序直接关闭了, 找了好久才发现)
然后我们生成exe, 注意要使用Release和x86选项生成的exe才能成功, 试了其他的选项都不行
检验一下
首先打开notepad.exe
然后使用ProcessExplorer查看其PID
得到PID = 13456
然后在CodeInjection2.exe的文件中以管理员身份打开终端
输入参数运行我们的CodeInjection2.exe
可以看到弹出了我们想要的窗口
28.5 调试练习
28.5.1 调试notepad.exe
使用OD工具打开notepad.exe, F9使其处于运行中状态
28.5.2 设置OllDbg选项
这样当我们的CodeInjection2.exe产生新线程时, 我们就是停下来调试了
28.5.3 运行CodeInjection2.exe
使用ProcessExplorer查看notepad的进程PID
然后以进程PID作为参数, 运行CodeInjection2.exe
28.5.4 线程起始代码
当运行CodeInjection2.exe时, 我们来到OD界面, 可以看到停在了我们的线程函数部分
28.6 详细分析汇编指令
28.6.1 生成栈帧
PUSH EBP
MOV EBP, ESP
以上两句为生成栈帧指令
28.6.2 THREAD_PARAM 结构体指针
MOV ESI, DWORD PTR [EBP+8]
生成栈帧以后, [EBP + 8] 是传入函数的第一个参数, 这里指THREAD_PARAM结构体指针. 为什么是EBP + 8我们看看栈图即可知道(此时执行完了MOV EBP, ESP
指令)
参数结构体总共有8个字节, 前面四个字节保存的是LoadLibraryA()函数的指针, 后面四个字节保存的是GetProcAddress()函数的指针.
28.6.3 “User32.dll” 字符串
PUSH 6C6C
“\0\0ll” 逆序
PUSH 642E3233
“d.23” 逆序
PUSH 72657375
“resu” 逆序
上面三行代码将”User32.dll”字符串压入栈中, 这种独特的方法仅用于汇编语言编写的程序, 采用的是逆序的方式压入, 因为栈是从高地址向低地址排列, 且整个数据是以小端排序, 当一整个数据块压入的时候, 会以字节为单位逆序存储, 所以”resu”压入栈中是排列其实是”user”
这样将所需字符串压入栈中的方式, 注入代码时就不需要另外注入数据, 直接使用栈即可.
我们再看看运行完上述语句后的寄存器情况
可以看到此时的ESP就相当于我们的字符串指针
28.6.4 压入”user32.dll”字符串参数
PUSH ESP
LoadLibraryA()API需要一个参数, 用来接收一个字符串的地址, 该字符串为需要加载的DLL文件的名称. 所以我们在上面已经知道了ESP此时相当于”user32.dll”的字符串指针, 所以这里PUSH了ESP来作为下面调用的参数
28.6.5 调用LoadLibraryA()(”user32.dll”)
CALL DWORD PTR [ESI]
前面已经将参数结构体的地址给了ESI, 而ESI所指向的内存则保存着LoadLibraryA的地址, 所以这里调用了LoadLibraryA()函数, 而其参数就是ESP(”user32.dll”的字符串指针)
在调用完成后, 查看其返回值, 就是该模块的句柄
验证一下
可以看到user32.dll的基址就是EAX中的76380000
28.6.6 “MessageBoxA” 字符串
PUSH 41786F “\0Axo”
PUSH 42656761 “Bega”
PUSH 7373654D “sseM”
跟28.6.3同理也是通过把字符串压入栈中, 然后得到了ESP就是该字符串的指针
28.6.7 调用GetProcAddress(hMod, “MessageBoxA” )
PUSH ESP
PUSH EAX
CALL DWORD PTR DS : [ESI+4]
我们要知道汇编中的参数时逆序要入栈中的, 所以我们先PUSH”MessageBoxA”的指针, 然后再PUSH模块的句柄. 首先, PUSH”MessageBoxA”的字符串指针, 就是PUSH了ESP; 其次, 我们前面调用了LoadLibraryA()函数的返回值就是hMod模块句柄, 存储在EAX当中, 所以这里PUSH了EAX.
随后调用了[ESI + 4]即是结构体中的第二个成员GetProcAddress()函数的地址.
而它的返回值就是MessageBoxA的函数地址, 存放再了EAX当中
28.6.8 压入MessageBoxA()函数的参数 1 - MB_OK
紧接着就开始了调用MessageBoxA()的参数准备, 参数是逆序压入的, 所以首先是压入MB_OK参数
PUSH 0
压入的时MessageBoxA的第四个参数
28.6.9 压入MessageBoxA()函数的参数2 - “ReverseCore”
0041002E E8 0C000000 call 0041003F 00410033 52 push edx 00410034 65:76 65 jbe short 0041009c 00410037 72 73 jb short 004100AC 00410039 65:43 inc ebx 0041003B 6f outs dx, dword ptr [esi] 0041003C 72 65 jb short 004100A3 0041003E 00E8 add al, ch
很明显, 410033 ~ 41003E地址处为我们的字符串”ReverseCore”, 只不过被识别成了代码. 也就是说, “ReverseCore”字符串的首地址为410033, 它被用作MessageBoxA()的第三个参数
将字符串作为参数传递给函数前, 要先把字符串的首地址压入栈中
下面, 我们将介绍”使用CALL指令将包含再代码键的字符串数据地址压入栈中”的方法.
根据途中的介绍我们可以知道字符串的首地址被压入了栈中, 也就代表着第三个参数被成功压入了栈中, 同时通过CALL指令我们的EIP变成了43003F, 跳过了中间的字符串来到了接下去的代码段
1 | 个人的理解: 这个方法起始就是把CALL指令拆分成了JMP和PUSH两个指令, 首先使用JMP跳转到下一个 |
28.6.10 压入MessageBoxA()函数的参数 3 - “www.reversecore.com”
0043003E 00E8 add al, ch 00430040 14 00 adc al, 0 00430042 0000 add byte ptr [eax], al 00430044 77 77 ja short 004300BD 00430046 77 2E ja short 00430076 00430048 72 65 jb short 004300AF 0043004A 76 65 jbe short 004300B1 0043004C 72 73 jb short 004300C1 0043004E 65:636F 72 arpl word ptr gs:[edi+72], bp 00430052 65 gs: 00430053 2E:636F 6D arpl word ptr cs:[edi+6D], bp 00430057 006A 00 add byte ptr [edx], ch
注意, 第一条指令, 由于我的OD分析偏差, 所以跟上面”ReverseCore”结尾的’\0’是连在一起的
实际上的第一条指令机器码为E8 14
,意思是CALL 当前位置 + 0x14
, 原理跟上面是一样的
28.6.11 压入MessageBoxA()函数的参数 4 - NULL
00430057 006A 00 add byte ptr [edx], ch
这里同样是识别错误, 跟上面”www.reversecore.com”结尾的’\0’连在了一起
实际上的机器码为6A 00
, 汇编为: PUSH 00
,压入了第一个参数NULL
28.6.13 调用了MessageBoxA()
0043005A FFD0 call eax
我们可能忘记了EAX是从哪里来的(反正我是忘了), 看看汇编窗口
你可能感到奇怪的就是中间我们压入那两个字符串的时候不是也调用了CALL吗, 为什么不会影响EAX?
因为使用的这两个CALL指令根本就没有返回, 一直执行到了最后的RETN程序就结束了, 从头到尾都没有返回.
回到正题, 我们可以知道CALL EAX
, 就是调用了MessageBoxA()函数
28.6.13 设置ThreadProc()函数的返回值
0043005C 33C0 xor eax, eax
该汇编代码完成之前, 我们还需要做一些准备工作, XOR指令将线程函数的返回值设置为0
28.6.14 删除栈帧及函数返回
0043005E 8BE5 mov esp, ebp 00430060 5D pop ebp 00430061 C3 retn
删除ThradProc()函数一开始生成的栈帧, 然后RETN
至此我们完成了整个调试过程