这个比赛的题看起来难,但是考察的知识点比较浅,属于那种不会是真的不会,会了秒起来很快的。。。而我刚好什么都不会,当时只做出来一道QAQ。还有一个安卓,一个CPP虚拟机和迷宫,不想复现了QAQ

TemporalParadox

进入主函数发现有花指令:

image-20250714103425955

去掉,然后成功反编译主函数:

image-20250714103823501

逻辑很清楚,time为时间戳,如果时间戳在1751990400和1752052051就使用哈希函数对时间加密并输出字符串类似于salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t=1751994277&r=101356418&a=1388848462&b=441975230&x=1469980073&y=290308156并输出最后的hex值,如果不在,则直接要求输入这个字符串并令其哈希后的hex结果与8a2fc1e9e2830c37f8a7f51572a640aa比较。

因此,可以通过idapython,爆破每步的时间戳,在时间戳处于1751990400和1752052051的情况下,程序本身就会打印字符串和hex结果,通过比较该结果即可获取正确字符串(先将时间比较patch掉,方便爆破)

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
import idaapi
import ida_dbg
import idc
import struct

OFFSET_VAR_120 = -0x40
OFFSET_VAR_140 = -0x60

# 以下内容需要根据具体地址设置
BP_TIME = 0x7FF7C16619A2
BP_CHECK = 0x7FF7C1661DB4
BP_BACK = 0x7FF7C1661D16

TIME_START = 1751990400
TIME_END = 1752052051
current_time = TIME_START

def read_c_string(ea):
"""从调试进程内存读取以 0 结尾的 C 字符串"""
s = b""
while True:
b = idaapi.dbg_read_memory(ea, 1)
if not b or b == b"\x00":
break
s += b
ea += 1
return s

class MyDbgHooks(idaapi.DBG_Hooks):
def dbg_bpt(self, tid, ea):
global current_time

if ea == BP_TIME:
if current_time > TIME_END:
print("[*] time 超出上限,重置为 START")
current_time = TIME_START

idc.set_reg_value(current_time, "RAX")
# RAX = current_time
print(f"[*] BP_TIME hit @0x{ea:X}, RAX ← {current_time}")
current_time += 1
ida_dbg.continue_process()
return 0
if ea == BP_CHECK:
rbp = idaapi.get_reg_val("RBP")

buf1 = idaapi.dbg_read_memory(rbp + OFFSET_VAR_120, 8)
buf2 = idaapi.dbg_read_memory(rbp + OFFSET_VAR_140, 8)
addr1 = struct.unpack("<Q", buf1)[0]
addr2 = struct.unpack("<Q", buf2)[0]

s1 = read_c_string(addr1)
s2 = read_c_string(addr2)
print(f"[*] BP_CHECK hit @0x{ea:X}\n str1 = {s1}\n str2 = {s2}")

if s1 == b"8a2fc1e9e2830c37f8a7f51572a640aa":
print("[+] 验证通过!", s2.decode(errors="ignore"))
import hashlib
print(f"L3HCTF{{{hashlib.sha1(s2).hexdigest()}}}")
ida_dbg.request_terminate_process()
idaapi.qexit(0)
else:
print("[-] 验证失败,跳回 0x%X" % BP_BACK)
idc.set_reg_value(BP_BACK, "RIP")
ida_dbg.continue_process()

return 0
return 1

hooks = MyDbgHooks()
hooks.hook()

idc.add_bpt(BP_TIME)
idc.add_bpt(BP_CHECK)

print(f"[*] 已设 BP_TIME @0x{BP_TIME:X}")
print(f"[*] 已设 BP_CHECK @0x{BP_CHECK:X}")

# salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t=1751994277&r=101356418&a=1388848462&b=441975230&x=1469980073&y=290308156
# L3HCTF{5cbbe37231ca99bd009f7eb67f49a98caae2bb0f}

snake

一个GO,很久没有做过go相关的了,go和c的调用约定不一样。将start函数的调用约定改为__golang

然后尝试找runtime_newproc函数,该函数第一个传参为runtime_main

image-20250718030454912

image-20250718030514095

点进这个off_3c3790就是runtime_main指针

image-20250718030600720

进入后找到检查双标志位的地方,里面就是main函数

image-20250718030638539

找到后,单步调试找到游戏主循环

image-20250718030929863

该处有很明显的校验逻辑,由于有基于时间的反调,所以先尝试直接改这里的判断吃没吃到。去掉if保存patch

image-20250718031115865

1
L3HCTF{ad4d5916-9697-4219-af06-014959c2f4c9}

obfuscate

使用signsrch可以在sub_1250找到RC5轮密钥,使用d810去混淆。同时有一堆反调试,找到对exit的引用然后force jmp所有的条件跳转。

动调发现1250这里是生成S盒

1
[0x122F2C9C, 0xE3BCCAE7, 0xD0FFC0F2, 0xD9A12544, 0x8A27992F, 0x55B1B935, 0x9110B161, 0x92811564, 0x5CE9B359, 0x77C79A51, 0x4265527A, 0x8AB57C4B, 0x11529FA4, 0x9D9F63FF, 0xA970B936, 0xC8EABA0D, 0x9A0EB4AA, 0xB0BC6E7F, 0x9784B100, 0x70DCD3AE, 0x6057A44E, 0x89187658, 0xE00098A8, 0x45773540, 0xF9374F1A, 0x913FA548]

找check函数可以调试得最后的数据

1
[0xF2A1BB1B, 0x21877CE9, 0x0AFD378A, 0xBC811A94, 0xAAE31E40, 0x3FD82E73, 0x4271B884, 0x398B35CC, 0xE9977D00, 0x00007FFD]

然后网上找个RC5脚本对着代码改一下:

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
#include <stdio.h>
typedef unsigned int WORD; /* Should be 32-bit = 4 bytes */
#define w 32 /* word size in bits */
#define r 12 /* number of rounds */
#define b 16 /* number of bytes in key */
#define c 4 /* number words in key */
#define t 26 /* size of table S = 2*(r+1) words */
WORD S[t]; /* expanded key table */
WORD P = 0xb7e15163, Q = 0x9e3779b9; /* magic constants */
/* Rotation operators. x must be unsigned, to get logical right shift */
#define ROTL(x, y) (((x) << (y & (w - 1))) | ((x) >> (w - (y & (w - 1)))))
#define ROTR(x, y) (((x) >> (y & (w - 1))) | ((x) << (w - (y & (w - 1)))))


void rc5_decrypt(unsigned char* cipher, unsigned char* plain)
{
WORD pt[2], ct[2];
for (int i = 0; i < 2; i++)
{
ct[i] = cipher[4 * i] + (cipher[4 * i + 1] << 8) + (cipher[4 * i + 2] << 16) + (cipher[4 * i + 3] << 24);
}
WORD B = ct[1], A = ct[0];
for (int i = r; i > 0; i--)
{
A = A ^ B;
B = ROTR(B - S[2 * i + 1], A) ^ A;
A = ROTR(A - S[2 * i], B) ^ B;
}
pt[1] = B - S[1];
pt[0] = A - S[0];
for (int i = 0; i < 2; i++)
{
plain[4 * i] = pt[i] & 0xFF;
plain[4 * i + 1] = (pt[i] >> 8) & 0xFF;
plain[4 * i + 2] = (pt[i] >> 16) & 0xFF;
plain[4 * i + 3] = (pt[i] >> 24) & 0xFF;
}
}


int main(int argc, char* argv[])
{
WORD p[] = { 0x122F2C9C, 0xE3BCCAE7, 0xD0FFC0F2, 0xD9A12544, 0x8A27992F, 0x55B1B935, 0x9110B161, 0x92811564, 0x5CE9B359, 0x77C79A51, 0x4265527A, 0x8AB57C4B, 0x11529FA4, 0x9D9F63FF, 0xA970B936, 0xC8EABA0D, 0x9A0EB4AA, 0xB0BC6E7F, 0x9784B100, 0x70DCD3AE, 0x6057A44E, 0x89187658, 0xE00098A8, 0x45773540, 0xF9374F1A, 0x913FA548 };
for (int i = 0; i < 26; i++)
{
S[i] = p[i];
}
WORD enc[] = { 0xF2A1BB1B, 0x21877CE9, 0x0AFD378A, 0xBC811A94, 0xAAE31E40, 0x3FD82E73, 0x4271B884, 0x398B35CC };
unsigned char pout[40] = {};
printf("L3HCTF{");
for (int i = 0; i < 4; ++i)
{
rc5_decrypt(&enc[i * 2], pout);
printf("%s", pout);
}
printf("}");
}
1
L3HCTF{5fd277be39046905ef6348ba89131922}

这个题要是像我当时比赛的时候一直跟混淆,那就废了。左边函数列表不多,可以先看看有没有加密函数之类的。

TheFinalGate

这个程序使用了raylib库,且在开始的时候,执行了

1
sub_7FF7D3CA1100(&v26, 0i64, byte_7FF7D3D13380);

这种函数,这是装载着色器片段,byte_7FF7D3D13380是字符串格式的代码,这个代码会交给GPU运行

调试到这里,然后dump,得到代码

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
#version 430 core

layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(std430, binding = 0) buffer OpCodes { int opcodes[]; };
layout(std430, binding = 2) buffer CoConsts { int co_consts[]; };
layout(std430, binding = 3) buffer Cipher { int cipher[16]; };
layout(std430, binding = 4) buffer Stack { int stack_data[256]; };
layout(std430, binding = 5) buffer Out { int verdict; };

const int MaxInstructionCount = 1000;

void main()
{
if (gl_GlobalInvocationID.x > 0) return;

uint ip = 0u;
int sp = 0;
verdict = -233;

while (ip < uint(MaxInstructionCount))
{
int opcode = opcodes[int(ip)];
int arg = opcodes[int(ip)+1];

switch (opcode)
{
case 2:
stack_data[sp++] = co_consts[arg];
break;
case 7:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = a + b;
break;
}
case 8:
{
int a = stack_data[--sp];
int b = stack_data[--sp];
stack_data[sp++] = a - b;
break;
}
case 14:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = a ^ b;
break;
}

case 15:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = int(a == b);
break;
}

case 16:
{
bool ok = true;
for (int i = 0; i < 16; i++)
{
if (stack_data[i] != (cipher[i] - 20))
{
ok = false;
break;
}
}
verdict = ok ? 1 : -1;
return;
}

case 18:
{
int c = stack_data[--sp];
if (c == 0) ip = uint(arg);
break;
}

default:
verdict = 500;
return;
}

ip+=2;
}
verdict = 501;
}

可以看出是一个栈虚拟机

最上面6行是定义的常量,在main中由:

1
2
3
4
5
v21 = sub_7FF7D3C8EFF0(0x2A0u, (__int64)&unk_7FF7D3D130E0, 0x88EAu);
v3 = sub_7FF7D3C8EFF0(0x80u, (__int64)&unk_7FF7D3D13060, 0x88EAu);
v22 = sub_7FF7D3C8EFF0(0x40u, (__int64)&unk_7FF7D3D13020, 0x88EAu);
v23 = sub_7FF7D3C8EFF0(0x400u, (__int64)&unk_7FF7D3D6F040, 0x88EAu);
v4 = sub_7FF7D3C8EFF0(4u, (__int64)&dword_7FF7D3D13000, 0x88EAu);

这几个定义,即:

1
2
3
4
5
v21 = sub_7FF7D3C8EFF0(0x2A0u, (__int64)&code, 0x88EAu);
v3 = sub_7FF7D3C8EFF0(0x80u, (__int64)&co_consts, 0x88EAu);
v22 = sub_7FF7D3C8EFF0(0x40u, (__int64)&cipher, 0x88EAu);
v23 = sub_7FF7D3C8EFF0(0x400u, (__int64)&stack_data, 0x88EAu);
v4 = sub_7FF7D3C8EFF0(4u, (__int64)&verdict, 0x88EAu);

尝试生成这个栈虚拟机,同时可以发现这个虚拟机是两位两位校验的,所以尝试直接爆破:

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
from prism import *
code = [0x02, 0x00, 0x02, 0x01, 0x02, 0x00, 0x0E, 0x00, 0x02, 0x10, 0x08, 0x00, 0x02, 0x02, 0x02, 0x01, 0x0E, 0x00, 0x02, 0x11, 0x08, 0x00, 0x02, 0x03, 0x02, 0x02, 0x0E, 0x00, 0x02, 0x12, 0x07, 0x00, 0x02, 0x04, 0x02, 0x03, 0x0E, 0x00, 0x02, 0x13, 0x07, 0x00, 0x02, 0x05, 0x02, 0x04, 0x0E, 0x00, 0x02, 0x14, 0x08, 0x00, 0x02, 0x06, 0x02, 0x05, 0x0E, 0x00, 0x02, 0x15, 0x07, 0x00, 0x02, 0x07, 0x02, 0x06, 0x0E, 0x00, 0x02, 0x16, 0x07, 0x00, 0x02, 0x08, 0x02, 0x07, 0x0E, 0x00, 0x02, 0x17, 0x07, 0x00, 0x02, 0x09, 0x02, 0x08, 0x0E, 0x00, 0x02, 0x18, 0x07, 0x00, 0x02, 0x0A, 0x02, 0x09, 0x0E, 0x00, 0x02, 0x19, 0x07, 0x00, 0x02, 0x0B, 0x02, 0x0A, 0x0E, 0x00, 0x02, 0x1A, 0x07, 0x00, 0x02, 0x0C, 0x02, 0x0B, 0x0E, 0x00, 0x02, 0x1B, 0x08, 0x00, 0x02, 0x0D, 0x02, 0x0C, 0x0E, 0x00, 0x02, 0x1C, 0x08, 0x00, 0x02, 0x0E, 0x02, 0x0D, 0x0E, 0x00, 0x02, 0x1D, 0x07, 0x00, 0x02, 0x0F, 0x02, 0x0E, 0x0E, 0x00, 0x02, 0x1E, 0x08, 0x00, 0x10, 0x00, 0x02, 0x10, 0x02, 0x11, 0x0F, 0x00, 0x12, 0x54, 0x02, 0x1F, 0x01, 0x00, 0x03, 0x01]
cipher = [0xF3, 0x82, 0x06, 0x000001FD, 0x00000150, 0x38, 0xB2, 0xDE, 0x0000015A, 0x00000197, 0x9C, 0x000001D7, 0x6E, 0x28, 0x00000146, 0x97]
g_co_const = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB0, 0xC8, 0xFA, 0x86, 0x6E, 0x8F, 0xAF, 0xBF, 0xC9, 0x64, 0xD7, 0xC3, 0xE3, 0xEF, 0x87, 0x00]

class Stack:
def __init__(self):
self.stack = []

def push(self, item):
self.stack.append(item)

def pop(self):
return self.stack.pop()

def peek(self):
return self.stack[-1]

def is_empty(self):
return len(self.stack) == 0

def __getitem__(self, index):
if index >= len(self.stack) or index < 0:
raise IndexError("index out of range")
return self.stack[index]


def main(co_const):
stack = Stack()
ip = 0
verdict = -233
while True:
opcode = code[ip]
args = code[ip + 1]
if opcode == 0x2:
stack.push(co_const[args])
# print(f"push {co_const[args]}")
# if args<16:
# print(f"push co_const[{args}]")
# else:
# print(f"push {co_const[args]}")
elif opcode == 0x7:
b = stack.pop()
a = stack.pop()
stack.push(a + b)
# print(f"pop reg1")
# print(f"pop reg2")
# print(f"add reg1, reg2")
# print(f"push reg1")

elif opcode == 0x8:
a = stack.pop()
b = stack.pop()
stack.push(a - b)
# print(f"pop reg1")
# print(f"pop reg2")
# print(f"sub reg1, reg2")
# print(f"push reg1")
elif opcode == 0xe:
a = stack.pop()
b = stack.pop()
stack.push(a ^ b)
# print(f"pop reg1")
# print(f"pop reg2")
# print(f"xor reg1, reg2")
# print(f"push reg1")
elif opcode == 0xf:
b = stack.pop()
a = stack.pop()
stack.push(a == b)
# print(f"pop reg1")
# print(f"pop reg2")
# print(f"cmp reg1, reg2")
# print(f"push 1 if equal else 0")
elif opcode == 0x10:
# print("final check")
ok = True
for i in range(len(cipher)):
if(stack[i] != cipher[i]-20):
ok = False
return i
verdict = 999 if ok else -1
return verdict
elif opcode == 0x12:
c = stack.pop()
if (c == 0):
ip = args
# print(f"ip = {ip}")
else:
verdict = 500
return verdict
ip += 2
return 501

x = g_co_const[::1]

def search(pos):
if pos == 15:
for a in range(256):
x[pos] = a
res = main(x)
if res == 999:
phex(x[0:16])
return True
for a in range(256):
x[pos] = a
for b in range(256):
x[pos+1] = b
res = main(x)
if res == pos + 1:
if search(pos + 1):
return True
x[pos] = 0
x[pos+1] = 0
return False

if search(0):
print("找到正确的 x:", x)
else:
print("无解或需要调整 main(x) 的返回规范")


"L3HCTF{df9d4ba41258574ccb7155b9d01f5c58}"

image-20250718203632104