脱壳

UPX壳
打开010可以发现是比较明显的UPX魔改壳,改成UPX后脱壳就可以脱壳成功
去混淆

进去之后发现是一个间接跳转的花指令,反编译卡住的点是这里的跳转地址的加法常量是取的data取的常数进行的加法,这里也就两种跳法,一个是上图的加法,一个下减法,写一个idapython脚本匹配字节码,然后去patch跳过的字节码和jmp rax就行
import ida_segment import ida_bytes import ida_kernwin import ida_auto
PATTERNS = [ b"\x48\x01\xC8\xFF\xE0", b"\x48\x29\xC8\xFF\xE0", ]
PATCH_OFFSET = 3 PATCH_LEN = 6 PATCH_BYTES = b"\x90" * PATCH_LEN
patched = []
def patch_match(ea): patch_ea = ea + PATCH_OFFSET
old_bytes = ida_bytes.get_bytes(patch_ea, PATCH_LEN) if old_bytes is None or len(old_bytes) != PATCH_LEN: print(f"[skip] 0x{ea:X}: cannot read patch range") return
ida_bytes.patch_bytes(patch_ea, PATCH_BYTES)
patched.append((ea, patch_ea, old_bytes)) print( f"[patched] match @ 0x{ea:X}, " f"patch @ 0x{patch_ea:X}, " f"old = {old_bytes.hex(' ').upper()}, " f"new = {PATCH_BYTES.hex(' ').upper()}" )
for i in range(ida_segment.get_segm_qty()): seg = ida_segment.getnseg(i) if not seg: continue
if not (seg.perm & ida_segment.SEGPERM_EXEC): continue
start = seg.start_ea end = seg.end_ea size = end - start
data = ida_bytes.get_bytes(start, size) if not data: continue
seg_name = ida_segment.get_segm_name(seg) print(f"[scan] {seg_name} 0x{start:X}-0x{end:X}")
for pat in PATTERNS: pos = 0 while True: idx = data.find(pat, pos) if idx == -1: break
ea = start + idx
if ea + PATCH_OFFSET + PATCH_LEN <= end: patch_match(ea) else: print(f"[skip] 0x{ea:X}: patch range crosses segment end")
pos = idx + 1
ida_auto.auto_wait() ida_kernwin.refresh_idaview_anyway()
print(f"\nDone. Total patched: {len(patched)}")
|


注意到字符串或者说程序里面的打印,也给大家提示了quick js的题目
像这种偏框架的题目,大家可以选择网上找一份quick js的程序自己编译或者说自己写一个编译然后bindiff可以比对恢复符号,这里调试讲解

这里动调或者静态分析可以看出是bytes转的hex,我们可以输入1111进行判断

动调看输入的变化,这里注意参数input_1

发现这里就是调用点
之后往上找参数,发现js绑定的代码在下图所示位置,并且绑定了c层的一个函数encrypt,这个函数就是一个正常的SM4算法,密钥也可以在里面找到是CTF2026_Wow_Key!

需要注意的是,JS bytecode 不能直接静态提取,因为程序在 init_array 中对 bytecode 做了解密处理。这里有两种做法:
- 静态分析
init_array 中的解密逻辑,提取后自行解密;(chacha20)
- 程序运行起来后,在内存中 dump 解密后的 QuickJS bytecode。
这里选择第二种方式。

import ida_bytes
addr = 0x555555675280 length = 2048
data = ida_bytes.get_bytes(addr, length) print(data.hex())
|
https://bytecodestudio.com/quickjs-decompiler
可以选择反编译器,推荐用这个网站直接梭

(function() { function encry(key, data) { var S = []; for (var i = 0; i < 256; i++) { S[i] = i; } var j = 0; for (i = 0; i < 256; i++) { j = j + S[i] + key[i % key.length] & 255; t = S[i]; i[__unknown_to_propkey2__] = S[j]; S[j] = t; } var out = []; var a = 0; var b = 0; for (var n = 0; n < data.length; n++) { a = a + 1 & 255; b = b + S[a] & 255; t = S[a]; a[__unknown_to_propkey2__] = S[b]; S[b] = t; out.push(data[n] ^ S[S[a] + S[b] & 255]); } return out; } function hexToBytes(h) { var r = []; for (var i = 0; i < h.length; i += 2) { r.push(parseInt(h.substr(i, 2), 16)); } return r; } function bytesToHex(a) { return (a.map(function(b) { return ("0" + (b & 255).toString(16)).slice(-2); })).join(""); } function check_flag(input_hex) { var hex = encrypt(input_hex); var b = hexToBytes(hex); var a4_b = encry(Key, b); return bytesToHex(a4_b) === TARGET_HEX; } var Key = [81, 117, 105, 99, 107, 74, 83, 95, 74, 97, 121, 95, 50, 48, 50, 54]; var TARGET_HEX = "6be6b4afed03d442e06300518997a115cb5f974566d40ca9aaca74972bb30b10c59a122a3558aa3b50df2dca033cd009"; check_flag = check_flag; hexToBytes = hexToBytes; bytesToHex = bytesToHex; })();
|
整体逻辑比较简单,就是进来之后调用了C层的SM4算法,然后调用js的rc4加密,都是标准的算法,解密如下

flag{3f2504e0-4f89-11d3-9a0c-0305e82c3301}
整体题目并不难,核心算法均为标准算法,真正的重点在于混淆和QuickJS bytecode 的处理。