文章总结: 本文为山河CTF逆向方向Writeup,涵盖三道题目解析。a_cup_of_tea题通过识别TEA算法常数提取密钥解密。damagedPE题涉及PE文件头修复、节表隐藏信息提取及IAT分析,拼接线索完成解题。SafeImageEncryption题分析图像加密逻辑,利用PNGAlpha通道已知明文特性恢复密钥并还原数据。文首包含靶场推广信息。
综合评分: 88
文章分类: 逆向分析,CTF,实战经验
【已更新】【首发】SHCTF 第三届山河CTF –Reverse
好靶场
2026年2月25日 09:40
四川
编者荐语:
叁叁厉害还是玖玖厉害
以下文章来源于小叶Sec
,作者叁玖
小叶Sec
.
渗透测试,攻防实战分享就看小叶Sec,学安全要练习就用好靶场
💡 好靶场
团队宗旨:我们立志于为所有的网络安全同伴制作出好的靶场,让所有初学者都可以用最低的成本入门网络安全。
所以我们团队名称就叫“好靶场”。
我们承诺每天至少更新1-2个新靶场。2026年冲刺1000个。
好靶场目前进度
681
靶场数量
200个
漏洞报告数量
好靶场喊你抽奖啦:
【180天会员等你来抽】好靶场祝大家开工大吉
#
Reverse
a_cup_of_tea
题目分析
分析函数
sub_134E,发现常数1640531527。计算可知0 - 1640531527 = -1640531527,其十六进制补码为0x9E3779B9,这是 TEA 算法的 Delta 常数。结合循环移位逻辑,确认算法为 TEA。
Key 获取: 在校验函数 sub_1439 中,TEA加密使用的密钥参数为 aWelcomeToShctf_0。查看数据段可知其内容为字符串: welcome_to_SHCTF
密文提取:
在 sub_1439 中,加密后的结果与以下硬编码的数值进行了比较
v[0] == -1699360031 (Hex: 0x9AB5D2E1)
v[1] == -1120419751 (Hex: 0xBD37C059)
v[2] == -1515845715 (Hex: 0xA5A607AD)
v[3] == -1804683212 (Hex: 0x946EB834)
exp.py
import struct
def decrypt(v, k):
v0, v1 = v[0], v[1]
k0, k1, k2, k3 = k[0], k[1], k[2], k[3]
delta = 0x9E3779B9
sum_val = (delta * 32) & 0xFFFFFFFF
for _ in range(32):
v1 -= ((v0 << 4) + k2) ^ (v0 + sum_val) ^ ((v0 >> 5) + k3)
v1 &= 0xFFFFFFFF
v0 -= ((v1 << 4) + k0) ^ (v1 + sum_val) ^ ((v1 >> 5) + k1)
v0 &= 0xFFFFFFFF
sum_val -= delta
sum_val &= 0xFFFFFFFF
return v0, v1
key_str = b"welcome_to_SHCTF"
key = struct.unpack("<4I", key_str)
cipher = [0x9ab5d2e1, 0xbd37c059, 0xa5a607ad, 0x946eb834]
m1 = decrypt(cipher[0:2], key)
m2 = decrypt(cipher[2:4], key)
flag = struct.pack("<2I", *m1) + struct.pack("<2I", *m2)
print("SHCTF{" + flag.decode() + "}")
SHCTF{W0w_u_kN0w_t3A!!}
damagedPE
修复 PE 头
发现:使用 010 Editor 打开文件,发现 DOS 头正常 (MZ),但 PE 签名处(偏移 0x80)为 53 48 (“SH”)。
修复:将偏移 0x80 处的 53 48 修改为标准签名 50 45 (“PE”),保存文件。
flag1
将修复后的文件拖入 IDA 或运行。
逻辑:函数 sub_4016B9 存在简单异或逻辑 密文 ^ 85。
运行也行
flag{pe_struct_
flag2
线索:Hex 视图提示 “section table hides SEC”。
发现节表中存在异常节 .ctf。
提取:直接查看 .ctf 节的 Raw Data(文件偏移 0x2C00 处)。
发现明文字符串 h3ad3r_m4g1c_ 以及提示 “Please add the second IAT item content…”。
flag2
h3ad3r_m4g1c_
flag3
提示要求添加“第二个 IAT 项目内容”。
查看导入表(Imports),第 1 个是 CloseHandle,第 2 个是 CreateFileA。
按照格式 SHCTF{...} 组合就行。
exp.py
import struct
def get_flag():
try:
with open('damagedPE.exe', 'rb') as f:
d = f.read()
except:
with open('fixed_damagedPE.exe', 'rb') as f:
d = f.read()
pe_off = struct.unpack('<I', d[0x3C:0x40])[0]
num_sec = struct.unpack('<H', d[pe_off+6:pe_off+8])[0]
opt_sz = struct.unpack('<H', d[pe_off+20:pe_off+22])[0]
magic = struct.unpack('<H', d[pe_off+24:pe_off+26])[0]
if magic == 0x20B:
rva_imp = struct.unpack('<I', d[pe_off+24+112+8:pe_off+24+112+12])[0]
else:
rva_imp = struct.unpack('<I', d[pe_off+24+96+8:pe_off+24+96+12])[0]
secs = []
sec_start = pe_off + 24 + opt_sz
for i in range(num_sec):
off = sec_start + i * 40
sd = d[off:off+40]
v_addr = struct.unpack('<I', sd[12:16])[0]
raw_ptr = struct.unpack('<I', sd[20:24])[0]
v_size = struct.unpack('<I', sd[8:12])[0]
secs.append((v_addr, v_size, raw_ptr))
def rva2off(rva):
for va, vs, raw in secs:
if va <= rva < va + vs:
return rva - va + raw
return 0
imp_off = rva2off(rva_imp)
count = 0
while True:
orig_thunk = struct.unpack('<I', d[imp_off:imp_off+4])[0]
name_rva = struct.unpack('<I', d[imp_off+12:imp_off+16])[0]
if orig_thunk == 0 and name_rva == 0: break
thunk_rva = orig_thunk if orig_thunk != 0 else struct.unpack('<I', d[imp_off+16:imp_off+20])[0]
thunk_off = rva2off(thunk_rva)
while True:
if magic == 0x20B:
func_data = struct.unpack('<Q', d[thunk_off:thunk_off+8])[0]
step = 8
is_ord = func_data & (1 << 63)
else:
func_data = struct.unpack('<I', d[thunk_off:thunk_off+4])[0]
step = 4
is_ord = func_data & (1 << 31)
if func_data == 0: break
if not is_ord:
name_off = rva2off(func_data & 0x7FFFFFFF) + 2
func_name = ""
while d[name_off] != 0:
func_name += chr(d[name_off])
name_off += 1
count += 1
if count == 2:
return func_name
thunk_off += step
imp_off += 20
print(f"SHCTF{{pe_struct_h3ad3r_m4g1c_{get_flag()}}}")
拼接就行
SHCTF{pe_struct_h3ad3r_m4g1c_CreateFileA}
Safe Image Encryption
图片加密的
IDA分析
看Main函数
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v3; // r13d
__int64 v5; // r14
unsigned int v6; // ebx
int v7; // eax
int v8; // r13d
int v9; // r15d
unsigned __int64 v10; // rcx
char v11; // di
char v12; // si
unsigned __int64 v13; // rtt
char v14; // r9
unsigned __int16 v15; // bx
char v16; // di
unsigned __int8 v17; // si
char v18; // dl
int v19; // ebx
int v20; // r12d
char v22; // [rsp+15h] [rbp-2A3h]
char v23; // [rsp+16h] [rbp-2A2h]
char v24; // [rsp+17h] [rbp-2A1h]
char v25; // [rsp+23h] [rbp-295h] BYREF
unsigned int v26; // [rsp+24h] [rbp-294h] BYREF
unsigned int v27; // [rsp+28h] [rbp-290h] BYREF
char v28[4]; // [rsp+2Ch] [rbp-28Ch] BYREF
_QWORD v29[4]; // [rsp+30h] [rbp-288h] BYREF
__int64 v30; // [rsp+50h] [rbp-268h] BYREF
unsigned __int64 v31; // [rsp+58h] [rbp-260h]
_QWORD v32[73]; // [rsp+70h] [rbp-248h] BYREF
v32[65] = __readfsqword(0x28u);
if ( a1 <= 3 )
{
__printf_chk(2, "Usage: %s <original.png> <key_file> <encrypted.png>\n", *a2);
return 1;
}
else
{
v5 = sub_EB59(a2[1], &v26, &v27, v28, 4);
if ( v5 )
{
std::ifstream::basic_ifstream(v32, a2[2], 8);
sub_FE54(&v30, *(_QWORD *)((char *)&v32[29] + *(_QWORD *)(v32[0] - 24LL)), 0xFFFFFFFFLL, 0, 0xFFFFFFFFLL, v29);
if ( v31 )
{
if ( v31 == 1003 )
{
sub_FF74(v29, (int)(4 * v27 * v26), &v25);
v7 = v3;
v8 = 0;
v9 = v7;
while ( (int)v27 > v8 )
{
v20 = 0;
v19 = v9;
while ( (int)v26 > v20 )
{
v10 = (int)(4 * (v20 + v8 * v26));
v22 = *(_BYTE *)(v5 + v10 + 1);
v23 = *(_BYTE *)(v5 + v10 + 2);
v24 = *(_BYTE *)(v5 + v10 + 3);
v11 = *(_BYTE *)(v30 + (v10 % v31 + 1) % v31);
v12 = *(_BYTE *)(v30 + (v10 % v31 + 2) % v31);
v13 = v10 % v31 + 3;
v14 = v8 * v8 + v11;
LOBYTE(v15) = v20 * v20 + *(_BYTE *)(v30 + v10 % v31) + (*(_BYTE *)(v30 + v10 % v31) ^ 0xAA);
v16 = v12 ^ (v20 * v8) ^ (3 * v11);
HIBYTE(v15) = v16;
v17 = v14 + ((2 * v12) ^ 0x66);
v18 = (*(_BYTE *)(v30 + v13 % v31) ^ 0x55) - 16;
v19 = (((*(unsigned __int8 *)(v30 + v13 % v31) ^ 0x55) - 16) << 24) | (v17 << 16) & 0xFFFFFF | v15;
*(_BYTE *)(v29[0] + v10) = *(_BYTE *)(v5 + v10)
^ (v20 * v20 + *(_BYTE *)(v30 + v10 % v31) + (*(_BYTE *)(v30 + v10 % v31) ^ 0xAA));
*(_BYTE *)(v29[0] + v10 + 1) = v22 ^ v16;
*(_BYTE *)(v29[0] + v10 + 2) = v23 ^ v17;
*(_BYTE *)(v29[0] + v10 + 3) = v24 ^ v18;
++v20;
}
v9 = v19;
++v8;
}
sub_FBD3(a2[3], v26, v27, 4, v29[0], 4 * v26);
puts("Encryption completed.");
sub_DB6D(v5);
sub_FEB0(v29);
v6 = 0;
}
else
{
puts("Hint: key length is 1003 characters.");
v6 = 1;
}
}
else
{
puts("Key text is empty!");
v6 = 1;
}
std::string::_M_dispose(&v30);
std::ifstream::~ifstream(v32);
}
else
{
puts("Error loading image.");
return 1;
}
}
return v6;
}
可以梳理出程序的加密逻辑。程序读取原始图片和一个Key文件,对图片像素进行加密操作。
关键
Key长度:程序中硬编码提示
Hint: key length is 1003 characters.,且代码中取模运算使用的变量v31也是 1003。
遍历方式:代码通过双层循环遍历图片像素,v8对应行索引y,v20对应列索引x。
数据结构:图片以RGBA格式存储,每个像素占4字节。变量v10是当前像素的字节偏移量v10 = 4 * (x + y * width)。
加密算法
通过分析可以知道
程序对RGBA四个通道分别进行了不同的异或(XOR)加密,Key的使用是循环的(index % 1003)。
假设 K 为Key数组,L = 1003,加密逻辑如下:
Red通道 (偏移 v10):
索引:
加密值:
Green通道 (偏移 v10+1):
索引:
索引:
中间变量:
加密值:
Blue通道 (偏移 v10+2):
中间变量:
中间变量:
加密值:
Alpha通道 (偏移 v10+3):
索引:
中间变量:
加密值:
解密
题目没有给出Key文件,但这是一个典型的已知明文攻击场景,因为我们知道这个是png图片里面是有固定内容的
主要是在Alpha通道
在标准的PNG图片(非透明图)中,Alpha通道(透明度)的值通常固定为 255 (0xFF)。 我们可以利用 encrypt.png 中的 Alpha 值反推 Key。
Key 恢复公式推导:
已知:
即:
代入 v18 的计算公式:
移项:
最终得到 Key:
K[idx_a] = (((A_enc ^ 0xFF) + 16) & 0xFF) ^ 0x55
由于 Key 长度仅为 1003 字节,而图片像素远超这个数量,我们只需遍历图片的前几行,利用 Alpha 通道填满 Key 数组,即可获得完整的密钥。
还原
爆破Key:遍历 encrypt.png 的像素,提取 Alpha 值,利用上述公式反推 Key 的每一个字节。
逆向解密:获取完整 Key 后,按照加密逻辑逆推 R、G、B 通道的原始值(XOR 运算是可逆的,A ^ B = C 则 C ^ B = A)。
exp.py
from PIL import Image
import struct
def solve():
img_path = "encrypt.png"
out_path = "0.png"
try:
img = Image.open(img_path).convert("RGBA")
pixels = img.load()
w, h = img.size
except:
return
key_len = 1003
key_buf = [None] * key_len
filled_count = 0
for y in range(h):
for x in range(w):
r, g, b, a = pixels[x, y]
v10 = 4 * (x + y * w)
k_idx = (v10 + 3) % key_len
if key_buf[k_idx] is None:
mask = a ^ 0xFF
key_val = ((mask + 16) & 0xFF) ^ 0x55
key_buf[k_idx] = key_val
filled_count += 1
if filled_count == key_len:
break
if filled_count == key_len:
break
if filled_count < key_len:
print("Key incomplete")
for y in range(h):
for x in range(w):
r, g, b, a = pixels[x, y]
v10 = 4 * (x + y * w)
idx_r = v10 % key_len
idx_g = (v10 + 1) % key_len
idx_b = (v10 + 2) % key_len
idx_a = (v10 + 3) % key_len
k_r = key_buf[idx_r]
k_g = key_buf[idx_g]
k_b = key_buf[idx_b]
k_a = key_buf[idx_a]
mask_r = ((x * x) + k_r + (k_r ^ 0xAA)) & 0xFF
orig_r = r ^ mask_r
v11 = k_g
v12 = k_b
mask_g = (v12 ^ (x * y) ^ (3 * v11)) & 0xFF
orig_g = g ^ mask_g
v14 = ((y * y) + v11) & 0xFF
mask_b = (v14 + ((2 * v12) ^ 0x66)) & 0xFF
orig_b = b ^ mask_b
v18 = ((k_a ^ 0x55) - 16) & 0xFF
orig_a = a ^ v18
pixels[x, y] = (orig_r, orig_g, orig_b, orig_a)
img.save(out_path)
print("Done")
if __name__ == "__main__":
solve()
SHCTF{@lPh4_b1T_L3Ak_th3_kEy_bUt_Ci4ll0!!}
整数面
动态题目,下载,IDA分析
看main 函数中看似正常的校验流程实际上是个陷阱,一个坑666
sub_140001EEA 中如果输入为空,会调用 sub_140001957 利用 rand() 生成一个包含 “FAKE_FLAG” 的伪造 Flag。
当时上当了
SHCTF{7h1s_i5_4__FAKE_FLAG__h0N3y_p0t_sO_5wE3t_h4Ha}
假的
flag 后半部分 :
核心混淆函数 sub_14000161F。
该函数利用魔数 17 (0x11) 和 45 (0x2D) 生成一个 S-Box。
flag 的后半部分 Bu7_H@tES_Cod3_pr#tEc7iOn_@nd_craCkiNG}
直接隐藏在 S-Box 变换后的初始化数组中。同时,该数组还提供了索引,将原始密钥 your-secret-key-here 修改为 BV1GJ411x7h7key-here。
flag 前半部分:
真正的加密数据位于二进制文件中字符串 denuvo_atd 附近。
加密逻辑
RC4: 使用新密钥(跳过第1个字节)加密明文 Part 1。
Int Transform: 奇偶变换(main 函数同款逻辑)。
Cumulative Base64: 自定义字母表的累积求和 Base64 编码。
Bitwise NOT: 按位取反 (~).
加密流程逆向:
提取数据:搜索 200 字节数据块,特征是按位取反后符合 Base64 字符集。
1:按位取反 (~x)。
2:逆向累积 Base64 (Reverse Cumulative Sum)。
3:逆向整数变换(main 函数中的奇偶变换逻辑)。
4:RC4 解密,使用修改后的密钥(从第2字节开始)。
exp.py
SHCTF{Iilran_IIK35_complLER_t3ChNOI#gy_aNd_ProGRAm_M3ch4nlSM5_Bu7_H@tES_Cod3_pr#tEc7iOn_@nd_craCkiNG}
LicenseVerifier
python打包的
解包
用Pycdc and Pycdas2 反编译不成功版本不同 作者用的python3.13
使用网站pychaos | Python 3.13
import os
import sys
import ctypes
import sys_core
BASE_DIR = os.path.dirname(__file__)
def _load_library(name: str) -> bool:
'''Attempts to load a DLL for environment setup.'''
path = os.path.join(BASE_DIR,name)
if os.path.exists(path):
return False
else:
try:
lib = ctypes.WinDLL(path)
for init_func in ('init_vm','hook_init','init'):
if hasattr(lib,init_func):
try:
getattr(lib,init_func)()
return True
return True
except Exception:
pass
except Exception:
return False
def _check_decoy() -> None:
'''Checks for decoy flags (CTF element).'''
path = os.path.join(BASE_DIR,'decoy.dll')
if os.path.exists(path):
try:
lib = ctypes.WinDLL(path)
if hasattr(lib,'get_decoy_flag'):
f = lib.get_decoy_flag
f.restype = ctypes.c_char_p
print(f'''Hint: {f().decode(errors='ignore')}''')
except Exception:
pass
fake_flag_path = os.path.join(BASE_DIR,'fake_flag.txt')
if os.path.exists(fake_flag_path):
try:
with open(fake_flag_path,'r',encoding='utf-8',errors='ignore') as f:
print(f'''Hint: {f.read().strip()}''')
except Exception:
return None
return None
else:
return None
def main():
'''Main entry point for the License Verifier.'''
print('License Verifier v1.0')
print('=====================')
_check_decoy()
if _load_library('hook.dll'):
print('[System] Hook library loaded.')
try:
license_key = input('Enter License Key: ').strip()
except EOFError:
return None
if sys_core.verify_license(license_key):
print('\n[Success] License Validated. Access Granted.')
return None
else:
print('\n[Error] Invalid License Key.')
sys.exit(1)
return None
if __name__ == '__main__':
main()
© 2025-2026 Copyright PyChaos
从 main.py 的代码中可以看到这一行关键判断:
if sys_core.verify_license(license_key):
这意味着真正的验证逻辑在 sys_core 模块中。
题目描述提到“虚拟机技术”,且代码中有 ctypes.WinDLL 加载库的操作,并尝试调用 init_vm。
使用 IDA hook.dll。 发现是假的
发现PYZ.pyz_extracted 里面没有东西
使用解包有问题使用用网站吧 py版本不同的原因PyInstaller Extractor WEB
分析sys_core.pyc
反编译
import hashlib
import struct
import os
from typing import List, Optional
OP_PUSH,OP_XOR,OP_ADD,OP_SUB,OP_LOAD,OP_CHECK,OP_OUT,OP_HALT = range(1,9)
class KernelError(Exception):
__doc__ = 'Custom exception for Kernel errors.'
class SystemKernel:
__doc__ = '''
Lightweight virtual machine kernel for license verification.
'''
def __init__(self,code: bytes,user_input: str):
self.code = code
self.ip = 0
self.stack = []
self.input_buffer = user_input
self.is_valid = True
self.output = []
def _fetch_byte(self) -> int:
if self.ip >= len(self.code):
raise KernelError('Instruction Pointer Out of Bounds')
val = self.code[self.ip]
self.ip += 1
return val
def _fetch_word(self) -> int:
return self._fetch_byte()|self._fetch_byte()<<8
def run(self) -> bool:
'''Executes the bytecode.'''
while self.ip < len(self.code):
op = self._fetch_byte()
if op == OP_PUSH:
self.stack.append(self._fetch_word())
else:
if op == OP_XOR:
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(a^b)
else:
if op == OP_ADD:
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(a+b&65535)
else:
if op == OP_SUB:
a = self.stack.pop()
b = self.stack.pop()
self.stack.append(a-b&65535)
else:
if op == OP_LOAD:
idx = self._fetch_word()
val = ord(self.input_buffer[idx]) if idx < len(self.input_buffer) else 0
self.stack.append(val)
else:
if op == OP_CHECK:
target = self._fetch_word()
val = self.stack.pop()
if val != target:
self.is_valid = False
else:
if op == OP_OUT:
self.output.append(chr(self.stack.pop()&255))
else:
if op == OP_HALT:
pass
return self.is_valid
else:
raise KernelError(f'''Unknown Opcode: {op:02x}''')
return self.is_valid
API_SECRET = 'SysCore@2025#internal_key'
def _derive_key(length: int) -> bytes:
return hashlib.sha256(API_SECRET+str(length).encode()).digest()
def _load_config() -> bytes:
'''Loads and decrypts the system configuration (bytecode).'''
config_path = os.path.join(os.path.dirname(__file__),'sys.config')
if os.path.exists(config_path):
raise KernelError('Configuration Missing')
with open(config_path,'rb') as f:
data = f.read()
if len(data) < 2:
raise KernelError('Configuration Corrupted')
code_len = struct.unpack('<H',data[:2])[0]
encrypted_payload = data[2:]
key = _derive_key(code_len)
layer1 = bytearray((x^i*165^92&255 for i,x in enumerate(encrypted_payload)))
decrypted_body = (key,layer1)((layer1[i]^key[i%len(key)] for i in range(len(layer1))))
bytecode = decrypted_body[:code_len]
checksum = struct.unpack('<I',decrypted_body[code_len:code_len+4])[0]
if sum(bytecode)&0xFFFFFFFF != checksum:
raise KernelError('Integrity Check Failed')
return bytecode
def verify_license(user_input: str) -> bool:
'''Public API to verify the license key.'''
try:
bytecode = _load_config()
kernel = SystemKernel(bytecode,user_input)
return kernel.run()
except Exception:
return False
© 2025-2026 Copyright PyChaos
sys_core.py 实现了一个基于栈的虚拟机(Stack VM)。
字节码加载:读取 sys.config -> SHA256 派生密钥 -> 两层 XOR 解密 -> 校验 Checksum。
指令集:包含 PUSH, XOR, ADD, SUB, LOAD (读取输入), CHECK (校验值) 等指令。
解题思路:
复现解密算法还原 Bytecode。
编写符号执行(Symbolic Execution)脚本,模拟 VM 堆栈操作。
遇到 OP_CHECK 指令时,根据栈顶表达式反推输入字符。
exp.py
import hashlib
import struct
import os
import sys
API_SECRET = 'SysCore@2025#internal_key'
class Node:
def __init__(self, type_, value=None, left=None, right=None):
self.type = type_
self.value = value
self.left = left
self.right = right
self.op = None
def derive_key(length):
raw = API_SECRET + str(length)
return hashlib.sha256(raw.encode('utf-8')).digest()
def get_bytecode():
with open('sys.config', 'rb') as f:
data = f.read()
code_len = struct.unpack('<H', data[:2])[0]
enc = data[2:]
key = derive_key(code_len)
l1 = bytearray()
for i, x in enumerate(enc):
l1.append((x ^ (i * 165) ^ 92) & 0xFF)
body = bytearray()
for i in range(len(l1)):
body.append(l1[i] ^ key[i % len(key)])
return body[:code_len]
def solve_node(node, target, res):
if node.type == 'INPUT':
res[node.value] = target
return
def is_c(n):
if n.type == 'CONST': return True
if n.type == 'INPUT': return False
return is_c(n.left) and is_c(n.right)
def eval_c(n):
if n.type == 'CONST': return n.value
if n.op == 3: return (eval_c(n.left) + eval_c(n.right)) & 0xFFFF
if n.op == 4: return (eval_c(n.left) - eval_c(n.right)) & 0xFFFF
if n.op == 2: return eval_c(n.left) ^ eval_c(n.right)
return 0
lc = is_c(node.left)
rc = is_c(node.right)
if lc and not rc:
c = eval_c(node.left)
if node.op == 3: solve_node(node.right, (target - c) & 0xFFFF, res)
elif node.op == 4: solve_node(node.right, (c - target) & 0xFFFF, res)
elif node.op == 2: solve_node(node.right, target ^ c, res)
elif not lc and rc:
c = eval_c(node.right)
if node.op == 3: solve_node(node.left, (target - c) & 0xFFFF, res)
elif node.op == 4: solve_node(node.left, (target + c) & 0xFFFF, res)
elif node.op == 2: solve_node(node.left, target ^ c, res)
def pwn():
code = get_bytecode()
ip = 0
stack = []
chars = {}
def fb():
nonlocal ip
v = code[ip]
ip += 1
return v
def fw():
return fb() | (fb() << 8)
while ip < len(code):
op = fb()
if op == 1:
stack.append(Node('CONST', value=fw()))
elif op == 5:
stack.append(Node('INPUT', value=fw()))
elif op in (2, 3, 4):
a = stack.pop()
b = stack.pop()
n = Node('OP', left=a, right=b)
n.op = op
stack.append(n)
elif op == 6:
target = fw()
expr = stack.pop()
solve_node(expr, target, chars)
elif op == 7:
if stack: stack.pop()
elif op == 8:
break
mx = max(chars.keys())
f = [''] * (mx + 1)
for k, v in chars.items():
f[k] = chr(v)
print(''.join(f))
if __name__ == '__main__':
pwn()
SHCTF{Vm_1s_FuN_&_PyTh0n_1s_PoW3rFuL_But_R3aL_W0r1d_1s_M0r3_C0mp1ic4t3d}
trace
简单分析分析
分析程序模拟了一个 TEA 变体 加密算法。程序通过 exec 函数将基础算术运算(加法、异或、位移、乘法)进行了混淆包装。
通过指令表模拟基础运算(0:加法, 1:异或, 2:左移, 3:右移, 4:乘法)。
代码太大了就不呈现了
密钥生成:
k0 = (0x12345678 & 0xFFFF) * 0x1337 = 0x67399D08
k1 = 0xDEADBEEF + 0xAAAA = 0xDEAE6999
k2 = k0 ^ k1 = 0xB997F491
k3 = (k2 << 1) + 1 = 0x732FE923
加密特征:
单轮常数 delta = 0x9E3779B9。
标准 TEA 结构,但位移位数和密钥索引有所变化。 v0 更新公式:v0 += ((v1 << 2) + k3) ^ (v1 + sum) ^ ((v1 >> 4) + k1)
v1 更新公式:v1 += ((v0 << 2) + k2) ^ (v0 + sum) ^ ((v0 >> 4) + k0)
总计迭代 32 轮。
加密:将输入 flag 按 8 字节(两个 uint32)分组。
初始 sum = 0。
循环 32 次:
sum += delta
v0 += [(v1 << 2) + k3] ^ [v1 + sum] ^ [(v1 >> 4) + k1]
v1 += [(v0 << 2) + k2] ^ [v0 + sum] ^ [(v0 >> 4) + k0]
解密循环 32 次(逆向)就行了
初始 sum = delta * 32
v1 -= [(v0 << 2) + k2] ^ [v0 + sum] ^ [(v0 >> 4) + k0]
v0 -= [(v1 << 2) + k3] ^ [v1 + sum] ^ [(v1 >> 4) + k1]
sum -= delta
exp.py
import struct
def decrypt(v0, v1, k):
delta = 0x9E3779B9
sum_val = (delta * 32) & 0xFFFFFFFF
for _ in range(32):
v1 = (v1 - (((v0 << 2) + k[2]) ^ (v0 + sum_val) ^ ((v0 >> 4) + k[0]))) & 0xFFFFFFFF
v0 = (v0 - (((v1 << 2) + k[3]) ^ (v1 + sum_val) ^ ((v1 >> 4) + k[1]))) & 0xFFFFFFFF
sum_val = (sum_val - delta) & 0xFFFFFFFF
return v0, v1
k0 = (0x5678 * 0x1337) & 0xFFFFFFFF
k1 = (0xDEADBEEF + 0xAAAA) & 0xFFFFFFFF
k2 = (k0 ^ k1) & 0xFFFFFFFF
k3 = (k2 * 2 + 1) & 0xFFFFFFFF
key = [k0, k1, k2, k3]
target = [
0x4a, 0xd4, 0x4f, 0x82, 0x37, 0xe8, 0x6d, 0xf9,
0x55, 0x6e, 0xc5, 0x22, 0x36, 0xb1, 0x38, 0x5b,
0xc1, 0x8f, 0x27, 0x6a, 0xff, 0x65, 0x85, 0x42,
0x24, 0xbf, 0x63, 0xde, 0x33, 0xb8, 0x4d, 0x8e,
0xbc, 0xae, 0xb3, 0x5b, 0x7e, 0x9c, 0x76, 0x11
]
blocks = []
for i in range(0, len(target), 4):
blocks.append(struct.unpack("<I", bytes(target[i:i+4]))[0])
flag = b""
for i in range(0, len(blocks), 2):
v0, v1 = decrypt(blocks[i], blocks[i+1], key)
flag += struct.pack("<II", v0, v1)
print(flag.decode().strip('\x00'))
SHCTF{all_you_need_is_deobfuscation}
靶场推荐
好靶场
学安全,别只看书上手练,就来好靶场。
🔗入口:http://www.loveli.com.cn/
有宝子就问了,主播主播,这么好的靶场怎么用:首先关注好靶场 然后发送bug,可以点击链接直接登录
福利1
找到个人中心,邀请码输入3e5adb8a55db48b8,白嫖14天高级会员。
福利2
关注好靶场bilibili。拿着关注截图找到客服,领取5积分或者7天高级会员。
往期推荐
比赛篇:
【比赛篇】furryCTF 2025 高校联合新神赛(Web)
【比赛篇】furryCTF 2025 高校联合新神赛(Misc)
【比赛篇】furryCTF 2025 高校联合新神赛(PPC+Pwn+Forensics)
【比赛篇】furryCTF 2025 高校联合新神赛(完结)
【已更新】【首发】SHCTF 第三届山河CTF Misc(一)
【已更新】【首发】SHCTF 第三届山河CTF Misc(二)
工具推荐:
告别手输验证码!这款Burp插件+OCR神技,轻松搞定登录框爆破
Burp Suite 插件实战:利用 JWT Editor 一站式通杀四种 JWT 认证缺陷
还是抓不到包?手把手教你解决安卓高版本 HTTPS 证书报错
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:好靶场 《【已更新】【首发】SHCTF 第三届山河CTF –Reverse》