Loading... # 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的位即可 ~~~python 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结果 ~~~js 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**: ~~~js 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); ~~~ 不要被代码唬住了,可以观察到 ~~~js r5 = r6.toString; r1 = 10; r10 = r5.bind(r6)(r1); ~~~ 实际等价于下面,即转为10进制 ~~~js 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结果比较 逆向很简单就是一组异或 ~~~python 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**: ~~~python 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解密即可,密钥是多个常量拼接 ~~~python 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 ~~lazy~~ busy 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测试,很难想象这么多人做出来了 * https://m1r1.medium.com/lazy-vm-idekctf-2025-writeup-077a8653a870 赛后复现学习,比赛的时候除了字母i其他都不行,fuzz了很久都没找到其他输入点,但其实也观察到0-8不对劲没有unknown instruction,但当时以为是不可见字符程序没有读取到 首先fuzz输入单字符发现,0-8(字节)、字符i可以输入,其他都不行,要么是被ban的flag字符,要么报错无效指令 接下来分析0-8字节输入做了什么,下面是案例脚本 ~~~python 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读了出来 ~~~python 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吧 > 剩下的两个题我没法搞懂,类型太偏了,以后再说吧 最后修改:2025 年 08 月 05 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏