脱壳

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

# 确保 patch 范围不越过当前段
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 做了解密处理。这里有两种做法:

  1. 静态分析 init_array 中的解密逻辑,提取后自行解密;(chacha20)
  2. 程序运行起来后,在内存中 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 的处理。