写入可执行代码这部分我们借助OD来完成(这部分比较考验汇编语言)
首先认识一下MessageBoxA这个API:
更详细的解释自己可以在编译器里面去搜索
在反汇编代码中可以看到他的参数传递方式同声明时的正好相反
认识程序中参数的传递,将2.exe载入到OD中。
按照编译器的习惯我们将”Hello word”字符串的地址放入eax寄存器中。我们知道Hello Word这个字符串被放入了.data段的开始部分,所以他的地址也就是.data的起始地址,.data段是我们手写PE的最后一个区段,他的RVA为3000h Va=Imagebase+Rva=400000h+3000h=403000h双击第一行写入:mov eax,403000
按照反向弹入堆栈的规则MessageBoxA的第四个参数 :
UINT uType //对话框的样式
最先弹入堆栈,他的值有很多种可以参考msdn去填写,我们这里显示一个简单的样式这个值填写为0。双击第二行写入push 0
接着弹入MessageBoxA的第三个参数 :
LPCTSTR lpCaption, //消息框的标题,如果为NULL,则标题为”错误”
压入堆栈,他的值填写为0。双击第三行写入push 0
接着弹入MessageBoxA的第二个参数 :
LPCTSTR lpText, //对话框上显示的空终止的字符串。
压入堆栈,他的值填写为Hello Word的地址,这个地址保存在eax寄存器中,双击第四行写入push eax
接着弹入MessageBoxA的第一个参数 :
HWND hWnd, //消息框所属的父窗口,如果为NULL这个消息框将没有所属的窗口
弹入堆栈,他的值填写为0,说明他没有所属的父窗口。双击第四行写入push 0
接着写调用MessageBoxA这个api的代码 :
这里涉及到了导入表,我们在讲这一部分知识时说过,函数的地址最终放在IAT里
而MessageBoxA的IAT恰好在.rdata的段起始,所以他的RVA是2000h。Va=Imagebase+Rva=400000h+2000h=402000h
按照call dword ptr ds:[xxxxxx]这个格式写入:call dword ptr ds:[402000]
这个时候已经能够弹出对话框了,我们可以复制到可执行文件试一下。程序运行弹出对话框后就崩溃了
哇咔咔~~~~猜到什么原因了嘛???没错,罪魁祸首是调用弹窗函数之后的那句add byte ptr ds:[eax],al,这条语句应该在od中很常见,因为它的字节码是0000,如果我们什么代码都没写,OD就会把它识别为这条语句
下面我们来修正崩溃问题:
程序在结束后应该正确的返回,返回值放在eax寄存器中,一般的这个值为0,写入:xor eax,eax(清空eax)然后程序返回写入: ret复制到可执行文件后测试。
以前经常经常有人问我,retn后面的参数是什么意思,为啥是retn10而不直接写retn呢?我只能说这是汇编的基础知识,这个不懂就说明汇编的很多知识都不知道,真的要去系统性的学习一下汇编语言了。不废话,我来解释吧~~~~~
先拆分一下retn的结构
RETN等价于一条指令:POP eip
而带有操作数的RETN指令则是在POP之后,执行ESP=ESP+操作数1
我们刚才压入了16字节的数据,占用了堆栈16个字节的空间,为了保持堆栈平衡,必须要释放堆栈的空间(你用了多少,用完就要还回去多少)
通常情况下,在被调用过程里,分配局部变量使用add esp -xx或着sub esp xx 这就是在分配局部变量,那么释放局部变量也有2种方式,第一种使用add esp -xx或着sub esp xx来移动堆栈指针,第二种就是retn xx
我们可以使用add esp 10来平衡堆栈,也可以使用retn10来平衡堆栈,为啥这里要使用retn10呢?因为编译器使用的retn10。
如果你还想问为啥是retn10而不是retn16,那你真的要从第一课开始看了
执行完retn10之后开始执行结束线程函数,这下一切都正常了
这就是我们纯手工把一个记事本文件变成了可执行程序的全过程,希望大家亲自动手完成。
至此手写PE文件部分就结束了,希望大家能够通过这部分的学习能够对PE结构有更深的了解。教程有理解不到位或者错误的地方,请联系QQ1481645902一起探讨学习,欢迎大家来纠正我的错误。