一、代码节空白区添加代码
1.MessageBox函数说明
-
MessageBox()
函数:功能是弹出一个标准的Windows对话框;它不是C函数库的标准函数,而是一个API,我们可以用C语言调用API函数。可以理解成我们在C中使用MessageBox函数就表示调用系统提供的API函数–MessageBoxA
。包含在头文件windows.h
;如果一个程序中包含user32.dll,则此程序就有MessageBoxA
API函数 -
函数原型:
int MessageBox( HWND hWnd,LPCTSTR lpText, LPCTSTR lpCaption = NULL, UINT nType = MB_OK );
-
四个参数说明:
- hWnd:表示窗口句柄,指定该对话框的所有者窗口;如果该参数为空(0/NULL),则该对话框不属于任何窗口
- lpText:字符串,指显示在对话框中的内容
- lpCaption:字符串,指对话框的标题;如果此参数为空,则默认使用“错误”作为标题
- nType:指定显示按钮的数目及形式,表名使用的图标样式、缺省按钮是什么、以及消息框的强制回应等
2.call与jmp指令的硬编码
-
由于我们现在还没有系统学习过硬编码知识,所以只能先自己编写C看看:我们任意在VC中写一个函数再调用它,编译进入到反汇编查看,右键打开
code bytes
显示硬编码,可以看到每条指令的左边都是它对应的硬编码,即二进制数(这里用十六进制显示) -
call指令的硬编码:E8 后面跟了4个字节(转换后的地址)
注意:硬编码为E8的call,后面跟的地址是相对地址;还有硬编码为FF15的call,后面跟的是绝对地址,即ImageBase + RVA后的地址值
-
jmp指令的硬编码:E9 后面跟了4个字节(转换后的地址)
-
push指令的硬编码:6A 参数的值(传入函数参数的值是什么对应的参数硬编码就是什么)
-
由于call指令和jmp指令后面跟的是我们想要调用的函数地址或者跳转的地址,那E8和E9后面跟的4个字节是不是就是这个地址本身呢?不是!E8和E9后面跟的地址数据是需要转换得到的
-
E8 和 E9 后面4字节地址X的转换公式:X = 真正要跳转的地址 - E8这条指令的下一行地址(相当于==相对地址),因为
call 地址
的硬编码固定一共占5字节,所以E8指令的下一行地址 = E8当前地址 + 5;所以公式最终为:X = 要跳转的地址 - (E8的地址 + 该指令长度)==注意:这些公式中所用到的地址值全部是可执行文件在4GB虚拟内存中的地址值,不是文件地址!
-
举例:
-
构造一个需要调用函数的C代码:
#include "stdafx.h" void Function(int a,int b,int c,int d){ } int main(int argc,char* argv[]){ Function(1,2,3,4); return 0; }
-
查看反汇编:
-
计算E8后面跟的4字节地址
-
因为E8的当前地址为0x00401190,真正要跳转的Function函数的地址为0x00401014,所有根据公式:X = 要跳转的地址 - (E8的地址 + 该指令长度) = 0x00401014 - (0x00401190 + 5)= 0xFFFFFE7F,由于内存是低地址向高地址存储,所以为
7F FE FF FF
,所以最后call指令的硬编码为E8 7F FE FF FF
jmp指令的硬编码同理
-
-
3.添加代码效果说明
- 我们将一段MessageBox函数添加到一个可执行文件中,当我们双击此文件时先弹出我们写的弹框窗口,点击确定按钮后再重新正常执行文件
- 所以我们的目的就是将此可执行文件的入口改成我们添加的代码位置,执行完代码后再jmp到此文件的原来的入口地址
- 注意在代码节空白区添加代码相当于给在硬盘上的文件中添加数据,添加完后再运行文件,所以这个过程可以理解为文件注入;但是我们平时说的注入,则是文件在运行时我把代码添加进去,这个过程可以理解为内存注入
4.添加思路
-
我们知道程序其实就是一堆二进制数据组成的,所以我们不可能直接将MessageBox函数加到一个可执行文件中,而是要想办法得到这个函数对应的二进制数据,将这些数据添加到文件中
-
但是我们没必要去自己再去写一个MessageBox函数的二进制,因为只要是user32的可执行文件中都会调用系统API函数–MessageBoxA,所以我们只要找到此文件中的MessageBox函数的地址,使用
call 这个地址
,在call之前先push需要的4个参数就等于使用了弹框的功能。因为最终我们要添加的是二进制数据,所以我们要知道传入4个参数的指令硬编码和call MessageBox地址
指令对应的硬编码,最后再加上一个**jmp 程序原来的入口地址
对应的硬编码**即可但是这个思路有一个小问题,换一个机器就用不了了,后面学习会知道原因
-
如何知道运行时MessageBoxAPI函数的地址?
- 使用OD打开可执行文件,在左下角的命令栏输入:
bp MessageBoxA
回车,表示在此函数起始位置设置断点,接着我们点击上方栏中的B
按钮,表示查看断点,接着双击我们刚设置的断点会跳转到断点所在位置,这个地址就是MessageBoxA函数的起始地址
- 使用OD打开可执行文件,在左下角的命令栏输入:
-
还要记下来此程序本来的入口地址:通过可选PE头中的AddressOfEntryPoint字段值可以知道程序入口地址的偏移量,再通过可选PE头中的ImageBase字段知道文件装载到内存中的起始地址,根据ImageBase + AddressOfEntryPoint可以得出来程序在4GB虚拟内存中的真正的入口地址
-
按照要求,我们只需要构造这样一个功能的代码:①pushMessageBox函数需要的4个参数;②call MessageBox函数地址;③jmp 程序原来的入口地址。
-
先初步判断空白区的长度是否能存放得下添加的硬编码(使用节对应的节表中的Misc.VirtualSize - SizeOfRawData计算空白区的大小)。因为push4个参数,一个push硬编码长度为2字节;call和jmp硬编码长度为5字节;加起来一共是18字节。
-
接着将这个功能的代码转换成对应的硬编码:其中需要计算E8 和 E9后面的地址,所以要用公式
X = 要跳转的地址 - (E8的地址 + 5)
进行计算,如果我们使用UE编辑器打开可执行文件进行编辑修改,由于UE编辑器打开的文件是在硬盘上的状态,即我们如果在UE或者winhex中改数据是改的FileBuffer中的数据,不是ImageBuffer中的,则公式的地址都是需要从文件地址转换成内存地址后再计算的!如果是ipmsg.exe,由于在文件对齐和内存对齐是一样的,所以文件在硬盘上的数据格式和加载到内存中的格式是一样的,那使用UE打开上面显式的地址是什么就用就行,因为文件地址此时等于内存地址
但是如果是notepad.exe等,文件对齐和内存对齐不一样,从硬盘加载到内存是需要"拉伸"的,所以如果此时用UE添加硬编码计算E8 E9后面的地址值时需要使用内存地址,所以多了一个FOA转化成RVA的过程
-
定位到任意一个节后面的空白区,将这段硬编码加到任意节的空白区中,但是最好是代码节中(节的名字一般默认为.text),因为代码区的属性一般是可执行的,所以加到代码节中不用修改节的characteristic属性。记下来这段添加的硬编码的起始偏移地址
-
我们如果写程序来模拟这个过程,添加硬编码在"拉伸"后的文件中添加
- 为什么要在拉伸后的文件中添加:因为我们添加的硬编码中有call和jmp指令后面的地址,这个地址是要转化能得到的—
X = 要跳转的地址 - (E8的地址 + 5)
,转换完才得到了添加的代码的完整硬编码,而地址转化公式中使用的E8当前的地址 + 5,这个E8当前的地址值为文件运行时装入4GB内存(拉伸后)后的值;而且公式中要跳转的地址也是文件装入内存后的MessageBoxA函数的地址。综上我们应该将要添加的硬编码添加到拉伸后的文件任意节的空白区中,这样更方便计算E8 和 E9后面跟的地址值。如果我们想在文件在硬盘上的状态中添加也可以,但是计算E8 和 E9 后面跟的地址值使用公式前,还要自己先把文件地址转换成内存地址,就比较麻烦
- 为什么要在拉伸后的文件中添加:因为我们添加的硬编码中有call和jmp指令后面的地址,这个地址是要转化能得到的—
-
最后找到程序的AddressOfEntryPoint字段所在地址,将此字段的值改为添加的硬编码的偏移地址
二、作业
1.在代码空白区添加代码(手动)
1)方法一:在FileBuffer中添加代码
-
打开复制一份notepad.exe,命名为newpad.exe
-
使用OD打开newpad.exe,OD会模拟newpad.exe运行时装入内存中的状态,输入
bp MessageBoxA
,找到MessageBoxA函数的地址0x77D36476 -
使用winhex打开newpad.exe,现在开始所有的地址、数据都是FileBuffer中的,即文件在硬盘上时状态的数据
-
我们先定位到newpad.exe的代码区(一般名为.text):DOS头最后4字节为0xE8,找到0xE8往后数24字节即为PE签名 + 标准PE头,标准PE头的倒数第四个字节取WORD宽的数据为0xE0,表示可选PE头的大小为0xE0,那么接着往后数0xE0字节,即可定位到第一个节表开始;找到一个节表看前8字节,ASCII码转换成字符串为.text,表示这个节表中的信息就是描述.text节的,即我们所要找的代码区。再接着找到此节表的PointerToRawData字段值为0x400,表示.text节起始地址为0x400(相对于文件起始地址的偏移地址)
-
定位到0x400,我们再通过节表的Misc.VirtualSize字段的值为0x6D72,通过0x400 + 0x6D72可以大致确定.text节的没有对齐的自身的结束位置0x7172。再通过下一个节表的PointerToRawData字段值为0x7200,综合0x7172可以判断出.text节的空白区大小为0x7200 - 0x7172 = 0x8E。所以我们可以将代码添加在这个空白区的任意大小合适的位置
-
接着就是构造ShellCode:
-
我们就添加一个最简单的弹框,即MessageBox(0,0,0,0)。汇编代码为:
push 00 push 00 push 00 push 00 call 程序运行时的MessageBoxA的地址(即ImageBuffer中的地址)
-
我们知道当执行完MessageBox函数最后还要重新让程序运行,所以还要加一个汇编指令:
jmp 程序原来的程序入口地址(ImageBase + AddressOfEntryPoint)
-
我们要将这些指令转化成对应的硬编码:
push 00
:硬编码为6A 00
call
:硬编码为E8
,后面跟的MessageBoxA地址需要转化,但是长度是确定的5字节jmp
:硬编码为E9
,后面跟的原来的程序入口地址也需要转化,但是长度也是确定的5字节
-
所以先初步估算一下:这段要添加的代码对应的硬编码长度为18字节,明显小于.text空白区的长度0x8E,所以可以添加到代码区的空白区处,下面就开始计算E8和E9后面跟的地址为多少?
-
-
计算E8 和 E9 后面的地址值:
-
我们先把已经确定的shellcode在代码区的空白区写好,
6A 00 6A 00 6A 00 6A 00
传入MessageBoxA函数的四个参数0的硬编码是确定的,所以写好;后面call的硬编码是E8确定的,但是后面四字节的call地址还不确定,先把位置空出来;然后jmp的硬编码也是确定的E9,同样后面的4字节地址还没算出来,所以先空下来。 -
接着为了将地址补全,需要用公式
X = 要跳转的地址 - (E8当前的内存地址 + 5)
,由于现在加的位置是文件在硬盘上的地址,不是加载到ImageBuffer中的地址,所以要做地址转换:E8文件地址为0x717A,相对于代码区.text开始地址的偏移量为0x717A - 0x400 = 0x6D7A;再找.text节在内存中的起始地址(ImageBase + VirtualAddress)0x1000000 + 0x1000 = 0x1001000;接着用0x1001000 + 0x6D7A = 0x1007D7A就得到了E8的内存地址;因为开始已经确认过MessageBoxA函数的内存中地址为0x77D36476
,所以X = 0x77D36476 - (0x1007D7A + 5) = 0x76D2E6F7 -
算出了上面,就可以得到E9的内存地址为0x1007D7A + 5 = 0x1007D7F,由于原来的OEP的值为0x6AE0,这是偏移量,加上ImageBase后才是内存地址,所以0x1000000 + 0x6AE0 = 0x1006AE0,所以E9后面的值X = 0x1006AE0 - (0x1007D7F + 5) = 0xFFFFED5C
-
-
现在完整的shellcode硬编码为:
6A 00 6A 00 6A 00 6A 00 E8 F7 E6 D2 76 E9 5C ED FF FF
-
最后我们手动的把程序的AddressOfEntryPoint字段的值改为这段shellcode硬编码在ImageBuffer的偏移起始地址0x7D72(因为0x7172 - 0x400 + 0x1000 = 0x7D72)
-
接着按保存,我们再运行一下newpad.exe,发现先弹出我们添加的对话框,然后点击确定,程序又正常执行了
-
我们如果还想验证一下我们加的那段代码在程序运行时,即装入ImageBuffer时在什么位置,我们双击运行一下newpad.exe,接着打开winhex–Tools–open ram–找到newpad–展开后找到newpad.exe双击,接着此时的数据就是此时正在运行时的newpad.exe的数据,那么我们找到.text节的空白区,可以发现此时我们添加的代码就在这里:
2)方法二:在ImageBuffer中添加代码
和上述过程类似,但是直接在ImageBuffer中改我们计算E8和E9后面的值就可以不用再将文件地址转为内存地址了,在哪里添加硬编码,直接取这个地址代到公式中进行计算即可,更方便一些,但是winhex工具目前好像无法实现,虽然可以修改,但是最后没办法保存。所以这种直接在imageBuffer中修改数据,只适合于我们自己编程的时候,因为最后改完还可以用编程C把ImageBuffer再编程newbuffer,day30写过这个代码。手动修改直接用方法一即可,下面简单看一看思路即可
-
我们再复制一个notepad.exe的副本,取名为notepad_v2.exe,我们双击运行起来,接着使用winhex–tools–open ram–找到notepad_v2展开–找到notepad_v2.exe双击,此时winhex中显式的数据就是文件加载到内存(imagebuffer)中的状态,旁边的地址就是内存地址
-
我们先通过e_lfanew字段值0xE8找到PE开始,标准PE头倒数第二个字段为SizeOfOptionalHeader,值为0xE0,所以从标准PE往后数0xE0字节,找到节表起始地址0x100001E0,我们找到Misc.VirtualSize字段值为0x6D72,再找VirtualAddress字段值为0x1000,由于文件起始地址为0x1000000(imagebase/或者直接看开头地址),那么.text节的起始地址为0x1001000,没对齐之前节本身结尾为0x1001000 + 0x6D72 = 0x1007D72,再根据下一个节表的VirtualAddress值为0x8000,可以知道,从0x1007D72到0x1008000为.text的空白区
-
我们直接将shellcode的硬编码加到.text的空白区即可,同样还是先把确定的硬编码写好:
6A 00 6A 00 6A 00 6A 00 E8 ? ? ? ? E9 ? ? ? ?
-
接着我们将这里E8的起始地址和MessageBoxA函数的内存地址代入公式
X = 要跳转的地址 - (E8当前的内存地址 + 5)
,为X = 0x77D36476 - (0x1007D7A + 5) = 0x76D2E6F7;同理将E9的起始地址和程序原来的程序入口地址0x1000000 + 0x6AE0 = 0x1006AE0代入公式得E9后面跟的值为:X = 0x1006AE0 - (0x1007D7F + 5) = 0xFFFFED5C这里就不用再进行文件地址到内存地址的换算了,直接看E8在哪个地址,这个地址就是E8的内存地址
-
所以完整的要添加的硬编码为:
6A 00 6A 00 6A 00 6A 00 E8 F7 E6 D2 76 E9 5C ED FF FF
-
最后将程序的EOP字段的值改为此段硬编码的起始内存地址相对于imagebase的偏移量0x7D72,保存即可
-
此时会发现没法保存,即使复制全部数据到新的文件中,也是将拉伸过的文件数据保存到了硬盘上,文件是无法运行的,所以现实中还是使用方法一比较可行,只是稍微麻烦一点,计算E8和E9后面的值是需要将文件地址转换成内存地址而已;但是如果用编码实现的话,由于我们可以写代码将文件拉伸,还可以再还原,所以写代码使用方法二更合适