GFSJ1200-【BugCat】
脱壳 花指令 异常处理 爆破
脱壳
拿到题目查壳发现是一个upx 看十六进制发现变成CAT了 修改回去发现还是脱壳不了 然后尝试手脱壳
由于是UPX使用ESP定律来手脱壳 在pushad之后下一个硬件断点
然后理想状态是运行到了popad之后 然后找大跳点 但是这次没有到理想状态
发现触发了异常 但是我们又不知道哪里触发了异常 我们要去找 一个地方 一个最常见的地方 TLS 回调 也就是作者最容易放暗桩的位置 TLS Callback 是 Windows 加载器在程序正式入口点之前自动执行的函数 也就是我们打开程序的流程是这样的
加载 EXE
↓
执行 TLS Callback
↓
执行程序入口点 EntryPoint
↓
进入 main / WinMain
所以我们要在TLS那里下一个断点 也就是设置中的
下了之后断点断在了TLS开始的地方我们要往后运行查看有无什么操作
运行到这里发现进行了一个条件断点检测 因为下一个条件断点 就是把目标地址原来的第 1 个字节临时改成0xCC 所以我们挺多方法进行绕过的
我这里选择直接nop
然后就运行到了popad后面 发现下面就是一个相对于其他函数的大跳位置 也就是 402FC7 这个位置
我们进去然后dump出来就好了
dump出来有挺多的花指令
花指令
去除花指令的脚本 让AI写的 有点多 懒得写。。。。
import os
import json
import time
import ida_bytes
import ida_segment
import ida_kernwin
import ida_auto
import ida_idaapi
import ida_loader
import ida_nalt
import ida_name
try:
import ida_diskio
except Exception:
ida_diskio = None
BADADDR = ida_idaapi.BADADDR
# For this sample, keep the import-fixing .SCY segment untouched.
ALLOW_SEG_NAMES = {"CAT0", "CAT1"}
MAX_PASSES = 8
def b(ea):
v = ida_bytes.get_wide_byte(ea)
if v is None or v < 0:
return None
return v & 0xFF
def db(ea, n):
out = []
for i in range(n):
x = b(ea + i)
if x is None:
return None
out.append(x)
return bytes(out)
def u32(ea):
bs = db(ea, 4)
if bs is None:
return None
return int.from_bytes(bs, "little", signed=False)
def s32(ea):
bs = db(ea, 4)
if bs is None:
return None
return int.from_bytes(bs, "little", signed=True)
def s8(x):
return x - 0x100 if x >= 0x80 else x
def seg_ok(seg):
if seg is None:
return False
name = ida_segment.get_segm_name(seg) or ""
if name not in ALLOW_SEG_NAMES:
return False
if not (seg.perm & ida_segment.SEGPERM_EXEC):
return False
return True
def same_seg(a, c):
sa = ida_segment.getseg(a)
sc = ida_segment.getseg(c)
return sa is not None and sc is not None and sa.start_ea == sc.start_ea
def junk_score(bs):
if not bs:
return 0
bad_seq = [
b"\x0f\x05", # syscall in x86 user-mode junk
b"\x9a", # far call junk
b"\x64\xe3", # fs:jcxz split junk
b"\x80\x83\x54", # fake ModRM block seen in this sample
b"\x09\xa5\xd9", # fake fnstenv-style junk
b"\xac\x6b",
b"\xc0\xf9\x72",
b"\x54\x09",
]
sc = 0
for q in bad_seq:
if q in bs:
sc += 3
for x in bs:
if x in (0x0F, 0x05, 0x9A, 0xF4, 0xD9, 0xAC, 0xC0, 0x64, 0xE3):
sc += 1
return sc
patches = []
def is_all_nop(ea, n):
bs = db(ea, n)
return bs is not None and all(x == 0x90 for x in bs)
def patch_nops(ea, n, typ, reason):
if n <= 0:
return False
if is_all_nop(ea, n):
return False
old = db(ea, n)
if old is None:
return False
for i in range(n):
ida_bytes.patch_byte(ea + i, 0x90)
patches.append({
"type": typ,
"ea": "0x%X" % ea,
"size": n,
"old": old.hex(" "),
"new": " ".join(["90"] * n),
"reason": reason,
})
return True
def try_call_addret(ea, end_ea):
if b(ea) != 0xE8 or ea + 10 > end_ea:
return False
rel = s32(ea + 1)
if rel is None:
return False
target = ea + 5 + rel
if not (ea + 5 <= target <= min(ea + 0x40, end_ea - 5)):
return False
if not same_seg(ea, target):
return False
if db(target, 3) != b"\x83\x04\x24" or b(target + 4) != 0xC3:
return False
imm = b(target + 3)
if imm is None:
return False
resume = ea + 5 + imm
if not (target + 5 <= resume <= min(ea + 0x60, end_ea)):
return False
if imm > 0x40:
return False
return patch_nops(ea, resume - ea, "call_addesp_ret_skip", "call/add-[esp]/ret trampoline; resumes at 0x%X" % resume)
def try_short_jmp(ea, end_ea):
if b(ea) != 0xEB or ea + 2 > end_ea:
return False
rel = s8(b(ea + 1))
if rel <= 0:
return False
target = ea + 2 + rel
if target > end_ea:
return False
skipped = db(ea + 2, rel)
if skipped is None:
return False
if rel <= 8:
return patch_nops(ea, 2 + rel, "short_jmp_over_junk", "EB +%d skips %s" % (rel, skipped.hex(" ")))
if rel <= 0x20 and junk_score(skipped) >= 2:
return patch_nops(ea, 2 + rel, "short_jmp_over_junk_scored", "EB +%d junk_score=%d" % (rel, junk_score(skipped)))
return False
def try_cf_guard(ea, end_ea):
x0, x1 = b(ea), b(ea + 1)
if (x0, x1) not in ((0xF9, 0x72), (0xF8, 0x73)): # stc;jb or clc;jae
return False
rel = b(ea + 2)
if rel is None or rel <= 0 or rel > 0x20:
return False
target = ea + 3 + rel
if target > end_ea:
return False
skipped = db(ea + 3, rel)
if skipped is None:
return False
if rel <= 5 or junk_score(skipped) >= 2:
typ = "stc_jb_always" if x0 == 0xF9 else "clc_jae_always"
return patch_nops(ea, 3 + rel, typ, "constant carry branch skips %s" % skipped.hex(" "))
return False
def clean():
for pass_no in range(MAX_PASSES):
before = len(patches)
seg = ida_segment.get_first_seg()
while seg:
if seg_ok(seg):
ea = seg.start_ea
while ea < seg.end_ea:
if try_call_addret(ea, seg.end_ea):
ea += patches[-1]["size"]
continue
if try_short_jmp(ea, seg.end_ea):
ea += patches[-1]["size"]
continue
if try_cf_guard(ea, seg.end_ea):
ea += patches[-1]["size"]
continue
ea += 1
seg = ida_segment.get_next_seg(seg.start_ea)
if len(patches) == before:
break
def annotate_known_routes():
names = {
0x401180: "hash_rolling_probe_4000",
0x401210: "seh_filter_av_or_singlestep",
0x4012D0: "rtl_continue_handler_patch_context",
0x401300: "seh_filter_state_machine",
0x4013F0: "maybe_install_rtl_continue_handler",
0x401540: "anti_debug_hide_thread_wow64_probe",
0x401650: "worker_check_thread",
0x401790: "veh_exception_dispatcher",
0x401BB0: "anti_debug_peb_beingdebugged_toggle",
0x401C70: "validate_input_core",
0x401F50: "main_prompt_and_check",
}
for ea, nm in names.items():
try:
ida_name.set_name(ea, nm, ida_name.SN_CHECK | ida_name.SN_FORCE)
except Exception:
pass
comments = {
0x4013F0: "Dynamically resolves ntdll!RtlAddVectoredContinueHandler and installs 0x4012D0 when command line condition passes.",
0x401540: "Anti-debug helper: OpenThread/GetCurrentThreadId, IsWow64Process, NtSetInformationThread(ThreadHideFromDebugger=0x11).",
0x401650: "Worker check route: IsDebuggerPresent index + int 0x2D + rolling hash blocks over global string at 0x40601C.",
0x401790: "Unhandled/VEH exception dispatcher: handles breakpoint/single-step paths and edits CONTEXT to redirect control flow.",
0x401C70: "Core validator: length 0x1E, prefix/suffix gates, printable range, then starts worker thread at 0x401650.",
0x401F50: "Main route: print prompt, read std::cin line, call validate_input_core, print CORRECT/WRONG, system('pause').",
}
for ea, c in comments.items():
try:
ida_bytes.set_cmt(ea, c, 0)
except Exception:
pass
def write_log():
inp = ida_nalt.get_input_file_path()
base = os.path.splitext(inp)[0]
out_json = base + ".ida_flower_patchlog.json"
data = {
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
"input": inp,
"patched_total": len(patches),
"patched_bytes_total": sum(p["size"] for p in patches),
"patches": patches,
}
with open(out_json, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print("[+] patchlog:", out_json)
return out_json
def export_exe():
inp = ida_nalt.get_input_file_path()
base = os.path.splitext(inp)[0]
out_exe = base + ".ida_flower_cleaned.exe"
try:
if ida_diskio is not None:
qf = ida_diskio.qfopen(out_exe, "wb")
ok = ida_loader.gen_file(ida_loader.OFILE_EXE, qf, 0, BADADDR, 0)
ida_diskio.qfclose(qf)
else:
ok = ida_loader.gen_file(ida_loader.OFILE_EXE, out_exe, 0, BADADDR, 0)
print("[+] exported:", out_exe, "ok=", ok)
except Exception as e:
print("[!] export failed, use File -> Produce file -> Create EXE file:", e)
out_exe = None
return out_exe
clean()
annotate_known_routes()
ida_auto.plan_and_wait(ida_idaapi.cvar.inf.min_ea, ida_idaapi.cvar.inf.max_ea)
log_path = write_log()
export_exe()
ida_kernwin.msg("[BugCat flower cleaner] patched %d sites, %d bytes. Log: %s\n" % (len(patches), sum(p["size"] for p in patches), log_path))
我简单看了看 一共三种 花指令
call $+...
add dword ptr [esp], imm
ret
; 垃圾字节
; 真实代码
stc
jb short xxx ; 永真跳转
clc
jae short xxx ; 永真跳转
jmp short +1/+2/+3 ; 跳过垃圾字节
主程序 进行了一个反调试检测 一个VEH异常处理
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned __int8 v3; // al
int v5; // [esp+20h] [ebp-28h]
int v6; // [esp+24h] [ebp-24h]
int v7; // [esp+28h] [ebp-20h]
int v8; // [esp+2Ch] [ebp-1Ch]
int v9; // [esp+30h] [ebp-18h]
int v10; // [esp+34h] [ebp-14h]
int v11; // [esp+44h] [ebp-4h]
NtSetInformationThread((HANDLE)0xFFFFFFFE, ThreadHideFromDebugger, nullptr, 0);
if ( !NtSetInformationThread((HANDLE)0xFFFFFFFE, ThreadHideFromDebugger, (PVOID)4, 4u) )
exit(1);
AddVectoredExceptionHandler(0, Handler);
v5 = 0;
v9 = 0;
v10 = 15;
v11 = 0;
sub_402360(std::cout, aCapooWantsASpe);
v3 = std::ios::widen(std::cin + *(_DWORD *)(std::cin + 4), 10);
sub_402890(v3);
if ( (unsigned __int8)sub_401C70(nullptr, v6, v7, v8, 0, 15) )
sub_402360(std::cout, dword_404268);
else
sub_402360(std::cout, dword_40427C);
return system(Command);
}
sub_401C70 最上面就是一个长度判断 和一个 flag包裹判断
char __cdecl sub_401C70(_DWORD *Block, int a2, int a3, int a4, unsigned int a5, unsigned int a6)
{
size_t v6; // ecx
_DWORD **p_Block; // eax
void *v8; // eax
void **v9; // ecx
void **v10; // esi
char result; // al
char *v12; // ecx
__int128 v13; // [esp+10h] [ebp-3Ch] BYREF
__int64 v14; // [esp+20h] [ebp-2Ch]
void **v15; // [esp+28h] [ebp-24h]
void **v16; // [esp+2Ch] [ebp-20h]
char v17; // [esp+33h] [ebp-19h]
HANDLE ThreadHandle; // [esp+34h] [ebp-18h] BYREF
DWORD ExitCode[5]; // [esp+38h] [ebp-14h] BYREF
ExitCode[4] = 0;
if ( a5 == 30 && (unsigned __int8)sub_4027A0(v16) && (unsigned __int8)sub_402800(v16) )
{
LODWORD(v13) = 0;
v14 = 0xF00000000LL;
if ( a5 < 5 )
sub_402350();
v6 = 25;
if ( a5 - 5 < 0x19 )
v6 = a5 - 5;
p_Block = &Block;
if ( a6 >= 0x10 )
p_Block = (_DWORD **)Block;
sub_402210(&v13, (char *)p_Block + 5, v6);
if ( HIDWORD(qword_40602C) >= 0x10 )
{
v8 = ::Block;
if ( (unsigned int)(HIDWORD(qword_40602C) + 1) >= 0x1000 )
{
if ( (unsigned int)::Block - *((_DWORD *)::Block - 1) - 4 > 0x1F )
goto LABEL_27;
v8 = *((void **)::Block - 1);
}
sub_402D4F(v8);
}
*(__int128 *)&::Block = v13;
qword_40602C = v14;
v9 = &::Block;
v10 = &::Block;
if ( HIDWORD(v14) >= 0x10 )
{
v10 = (void **)::Block;
v9 = (void **)::Block;
}
v15 = v10;
v16 = (void **)((char *)v9 + v14);
if ( v10 == (void **)((char *)v9 + v14) )
{
LABEL_19:
ThreadHandle = nullptr;
ExitCode[0] = dword_406018;
NtCreateThreadEx(&ThreadHandle, 0x1FFFFF, 0, -1, sub_401650, 6, 6, 0, 0, 0, 0);
NtSetInformationThread(ThreadHandle, ThreadHideFromDebugger, nullptr, 0);
if ( !WaitForSingleObject(ThreadHandle, 0xFFFFFFFF) )
{
if ( GetExitCodeThread(ThreadHandle, ExitCode) )
{
ExitCode[0] = LOBYTE(ExitCode[0]);
if ( LOBYTE(ExitCode[0]) == 1 )
{
result = 1;
goto LABEL_24;
}
}
}
}
else
{
while ( 1 )
{
v17 = *(_BYTE *)v10;
if ( (unsigned __int8)(v17 - 33) > 0x5Du )
break;
v10 = (void **)((char *)v15 + 1);
v15 = v10;
if ( v10 == v16 )
goto LABEL_19;
}
}
}
result = 0;
LABEL_24:
v17 = result;
if ( a6 >= 0x10 )
{
v12 = (char *)Block;
if ( a6 + 1 >= 0x1000 )
{
v12 = (char *)*(Block - 1);
if ( (unsigned int)((char *)Block - v12 - 4) > 0x1F )
LABEL_27:
invalid_parameter_noinfo_noreturn();
}
sub_402D4F(v12);
return v17;
}
return result;
}
也就是flag长这样 同时flag也要在33-126这个范围内
flag{xxxxxxxxxxxxxxxxxxxxxxxx}
然后我们从下往上看 能看到有一个 创造隐藏线程的函数
NtCreateThreadEx(&ThreadHandle, 0x1FFFFF, 0, -1, sub_401650, 6, 6, 0, 0, 0, 0);
NtSetInformationThread(ThreadHandle, ThreadHideFromDebugger, nullptr, 0);
线程函数是 sub_401650
char __cdecl sub_401650(int a1)
{
int v1; // esi
int v2; // ebx
void **v3; // ecx
void **v4; // ecx
void **v5; // edi
void **i; // eax
char v7; // cl
_OWORD v9[4]; // [esp+10h] [ebp-64h]
int v10; // [esp+50h] [ebp-24h]
int v11; // [esp+54h] [ebp-20h]
void **v12; // [esp+58h] [ebp-1Ch]
CPPEH_RECORD ms_exc; // [esp+5Ch] [ebp-18h]
ms_exc.registration.TryLevel = 0;
v9[0] = xmmword_4042B0;
v9[1] = xmmword_4042E0;
v9[2] = xmmword_4042D0;
v9[3] = xmmword_4042C0;
v1 = 0;
v2 = 0;
v10 = 0;
v11 = IsDebuggerPresent();
__asm { int 2Dh; Windows NT - debugging services: eax = type }
v3 = &Block;
if ( HIDWORD(qword_40602C) >= 0x10 )
v3 = (void **)Block;
v12 = v3;
v4 = &Block;
if ( HIDWORD(qword_40602C) >= 0x10 )
v4 = (void **)Block;
v5 = (void **)((char *)v4 + qword_40602C);
for ( i = v12; ; v12 = i )
{
while ( 1 )
{
if ( i == v5 )
{
ms_exc.registration.TryLevel = -2;
return 1;
}
v7 = *(_BYTE *)i;
v10 = ++v2;
if ( v2 == a1 )
break;
v1 = v7 + 31 * v1;
i = (void **)((char *)v12 + 1);
v12 = (void **)((char *)v12 + 1);
}
if ( (dword_406018 ^ v1) != *((_DWORD *)v9 + v11) )
break;
++v11;
v2 = 0;
v10 = 0;
v1 = 0;
i = (void **)((char *)i + 1);
}
return 0;
}
发现有一个会触发异常 也就是说会触发异常码0x80000003的函数 正正好好我们下面就是一个判断
__asm { int 2Dh; Windows NT - debugging services: eax = type }
这里不能直接相信 sub_401650 的伪代码 因为 IsDebuggerPresent 后面有一个 int 2D 这个指令会主动触发异常 而程序前面已经注册了 VEH 异常处理函数 所以这里不是程序崩溃 而是故意进入异常处理逻辑
LONG __stdcall Handler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
if ( ExceptionInfo->ExceptionRecord->ExceptionCode == 0x80000003 )
{
if ( byte_406420 )
JUMPOUT(0);
*(_DWORD *)(ExceptionInfo->ContextRecord->Ebp - 32) *= 2;
ExceptionInfo->ContextRecord->Dr0 = (DWORD)&loc_4016FC;
ExceptionInfo->ContextRecord->Dr1 = (DWORD)&loc_401706;
ExceptionInfo->ContextRecord->Dr2 = (DWORD)&loc_401711;
ExceptionInfo->ContextRecord->Dr3 = (DWORD)&loc_401726;
ExceptionInfo->ContextRecord->Dr7 = 85;
++ExceptionInfo->ContextRecord->Eip;
byte_406420 = 1;
sub_401540();
return -1;
}
else
{
if ( ExceptionInfo->ExceptionRecord->ExceptionCode == -2147483644 )
{
switch ( ExceptionInfo->ContextRecord->Dr6 & 0xF )
{
case 1u:
ExceptionInfo->ContextRecord->Ecx += 33;
ExceptionInfo->ContextRecord->Eip += 4;
break;
case 2u:
++ExceptionInfo->ContextRecord->Ebx;
++ExceptionInfo->ContextRecord->Eip;
break;
case 4u:
ExceptionInfo->ContextRecord->Esi = 53 * ExceptionInfo->ContextRecord->Eax;
ExceptionInfo->ContextRecord->Eip += 5;
break;
case 8u:
ExceptionInfo->ContextRecord->Ecx = *(_DWORD *)(ExceptionInfo->ContextRecord->Ebp - 32);
ExceptionInfo->ContextRecord->Esi ^= dword_406018;
ExceptionInfo->ContextRecord->Eip += 3;
break;
default:
return 0;
}
}
return 0;
}
}
第一次触发的是 0x80000003 异常 进入 Handler 后 程序设置了四个硬件断点
ExceptionInfo->ContextRecord->Dr0 = (DWORD)&loc_4016FC;
ExceptionInfo->ContextRecord->Dr1 = (DWORD)&loc_401706;
ExceptionInfo->ContextRecord->Dr2 = (DWORD)&loc_401711;
ExceptionInfo->ContextRecord->Dr3 = (DWORD)&loc_401726;
ExceptionInfo->ContextRecord->Dr7 = 85;
Dr0 到 Dr3 是硬件断点地址寄存器 Dr7 = 85 用来启用这些硬件断点
然后 Handler 又修改了 EIP
++ExceptionInfo->ContextRecord->Eip;
这样程序就会跳过 int 2D 继续执行后面的校验逻辑
所以真实执行路线是
进入 sub_401650
↓
执行 IsDebuggerPresent
↓
执行 int 2D
↓
触发 0x80000003 异常
↓
进入 Handler
↓
设置 Dr0 Dr1 Dr2 Dr3
↓
设置 Dr7 启用硬件断点
↓
修改 EIP 跳过 int 2D
↓
返回 sub_401650 继续执行
后面执行到 loc_4016FC loc_401706 loc_401711 loc_401726 这些地址时 会再次触发硬件断点异常
第二次触发的是 0x80000004 单步异常 Handler 通过 Dr6 & 0xF 判断当前命中了哪个硬件断点
switch ( ExceptionInfo->ContextRecord->Dr6 & 0xF )
{
case 1:
ExceptionInfo->ContextRecord->Ecx += 33;
ExceptionInfo->ContextRecord->Eip += 4;
break;
case 2:
++ExceptionInfo->ContextRecord->Ebx;
++ExceptionInfo->ContextRecord->Eip;
break;
case 4:
ExceptionInfo->ContextRecord->Esi = 53 * ExceptionInfo->ContextRecord->Eax;
ExceptionInfo->ContextRecord->Eip += 5;
break;
case 8:
ExceptionInfo->ContextRecord->Ecx = *(_DWORD *)(ExceptionInfo->ContextRecord->Ebp - 32);
ExceptionInfo->ContextRecord->Esi ^= dword_406018;
ExceptionInfo->ContextRecord->Eip += 3;
break;
}
这里四个 case 分别对应四个硬件断点
case 1 对应 Dr0 作用是把 ECX 加上 33 也就是加上 0x21
case 2 对应 Dr1 作用是把 EBX 加 1 这里的 EBX 是循环计数器
case 4 对应 Dr2 作用是把 ESI 改成 53 * EAX 这里的 ESI 是 hash 累加变量
case 8 对应 Dr3 作用是把 ESI 和 dword_406018 异或 这是每组 hash 结束后的异或操作
所以静态伪代码里看到的
v1 = v7 + 31 * v1;
并不是真实完整逻辑
因为真实执行过程中 VEH 会通过硬件断点不断修改 ECX EBX ESI EIP
也就是说 这里的真实逻辑不是单纯由普通汇编决定的 而是由普通指令加异常处理器一起决定的
结合这些寄存器变化 可以把每组 hash 逻辑还原成
h = 0;
h = h * 257 + ch0;
h = h * 257 + ch1;
h = h * 257 + ch2;
h = h * 257 + ch3;
h ^= dword_406018;
其中 dword_406018 的值是
0x052251FF
也就是每 4 个字符为一组 先按照 257 做滚动 hash 最后再异或 seed
前面的 sub_401C70 已经判断了输入长度 包裹格式 以及字符范围
if ( a5 == 30 && sub_4027A0(v16) && sub_402800(v16) )
后面又把输入从偏移 5 的位置开始复制出来
sub_402210(&v13, (char *)p_Block + 5, v6);
所以真正参与校验的是去掉前面包裹之后的主体内容
再看字符范围判断
if ( (unsigned __int8)(v17 - 33) > 0x5Du )
break;
这里限制字符范围是 0x21 到 0x7E
主体内容一共 24 字节 目标 hash 一共有 6 个 所以刚好可以分成 6 组 每组 4 字节
24 / 6 = 4
目标 hash 表来自 sub_401650 开头加载的几个 xmmword
v9[0] = xmmword_4042B0;
v9[1] = xmmword_4042E0;
v9[2] = xmmword_4042D0;
v9[3] = xmmword_4042C0;
这里不能直接按照地址从小到大取 要按照程序实际加载顺序取
最后得到 6 个目标 hash
hs = [
0x66BF1BAC,
0x473AC6FC,
0x4433C0D0,
0x289B6CEF,
0x71A8B6EC,
0x53775C73,
]
一开始可以用四层循环枚举每组 4 个字符 但是这里其实可以直接反推
因为每组 hash 的公式是
h = (((a * 257 + b) * 257 + c) * 257 + d) ^ 0x052251FF
先把异或还原掉
x = h ^ 0x052251FF
那么就有
x = ((a * 257 + b) * 257 + c) * 257 + d
这个形式可以看成 257 进制展开
因为每个字符都小于 257 所以可以直接用取模和整除把 4 个字符反推出来
反推顺序是从最后一个字符开始
d = x % 257
x //= 257
c = x % 257
x //= 257
b = x % 257
x //= 257
a = x % 257
最后得到的顺序是 a b c d 我看他们的wp都是写了一个爆破的exp 我在想这样就不需要爆破 94^4 了 每个 hash 都可以直接反推出对应的 4 个字符 最后把 6 组结果按顺序拼接 再按照题目要求补上外层包裹即可 同时程序里还有一个 SEH 异常处理函数 sub_401300 它同样处理 0x80000004 单步异常 会根据 Dr6 判断命中的硬件断点并修改 EAX EBX 等寄存器 其中 sub_401180 会计算程序前 0x4000 字节的滚动 hash 更像是代码完整性校验和反调试干扰 不是最终 flag hash 表的来源 所以解题时只需要知道它们会影响异常路线 不需要把它当成主加密逻辑
exp
hs = [0x66BF1BAC, 0x473AC6FC, 0x4433C0D0, 0x289B6CEF, 0x71A8B6EC, 0x53775C73]
seed = 0x052251FF
out = []
for h in hs:
x = h ^ seed
d = x % 257
x //= 257
c = x % 257
x //= 257
b = x % 257
x //= 257
a = x % 257
s = bytes([a, b, c, d])
out.append(s)
print(b"".join(out))
flag
CatCTF{bu9CAT+C@P0o-1S_s0-CUTe~}
评论