-
-
[原创] 第三题 寻踪觅源 WP
-
2020-4-18 09:03 3325
-
注意:本篇 wp 不涉及对题目校验算法的完整分析,请关注我正在抱着的大腿以获得最佳题解。
题目用 quickjs 实现,本人由于过菜,并不会逆其虚拟机字节码。好在作者并没有丧心病狂到将符号去除,通过把在 main 函数内被注册了的 js 函数中看着嫌疑比较大的都稍微下断点调一调跑一跑跟一跟,我们可以发现校验算法与 quickjs 的大整数运算相关。这样一来我将关注重点放在了 BigInt(Float) 的相关函数上,例如 bf_mul
/ bf_cmp
等等,关注程序在哪些地方、以何种顺序调用了这些函数。
有了源码之后我便失去了在 IDA 中打标记的动力,因为可以直接在 quickjs 库中找到 bf_mul
/ bf_cmp
的声明:
int bf_cmpu(const bf_t *a, const bf_t *b){...} int bf_mul(bf_t *r, const bf_t *a, const bf_t *b, limb_t prec, bf_flags_t flags){...}
可以发现这些函数操作对象是 bf_t
数据类型,它的结构如下:
typedef struct { struct bf_context_t *ctx; int sign; slimb_t expn; limb_t len; limb_t *tab; } bf_t;
它是以 hexadecimal floating number(这瞎编的名字是什么?)的形式存储的,limb_t 在我的机器上其实就是一个 DWORD,存的是十六进制的小数有效值部分,len 是 tab 数组的长度。既然打算去跟踪这些函数调用,自然会想到在每一次调用的时候输出调用的参数(比方说打印出 mul 、cmp 的两个操作数),因为已经清楚 bf_t 的结构,我们已经可以直接打印出这个 bf_t。
因为懒惰,写代码之前我在源码中报着期望的目光一瞧瞧二看看,果然在 libbf.c
里我发现了:
/* for debugging */ void bf_print_str(const char *str, const bf_t *a) { ...
于是乎写了个调试器,hook 了 bf_mul
/ bf_cmp
,顺便把 quickjs 的源码拉进来用,在被 hook 函数 hit 的时候输出相应的调用参数:
#include <Windows.h> #include <iostream> #include "libbf.h" void Bp(LPVOID BpAddr); void bf_print_str_(const bf_t *a); bf_t *get_bf_t_from_remote_memory(PVOID addr); void destroy_bf_t(bf_t *n); #define ProcessNameToDebug L"F:\\OrzRiatreWhileEatingPizza\\lelfei_fix.exe" STARTUPINFO si = {0}; PROCESS_INFORMATION pi = {0}; BYTE orgs[100] = {0}; void ProcessDebugEvent(DEBUG_EVENT *dbg_event, DWORD *dbg_status) { switch (dbg_event->dwDebugEventCode) { case EXIT_THREAD_DEBUG_EVENT: { printf("The thread %d exited with code: %d\n", dbg_event->dwThreadId, dbg_event->u.ExitThread.dwExitCode); *dbg_status = DBG_CONTINUE; break; } case CREATE_THREAD_DEBUG_EVENT: { printf("Thread 0x%x (Id: %d) created at: 0x%x\n", dbg_event->u.CreateThread.hThread, dbg_event->dwThreadId, dbg_event->u.CreateThread.lpStartAddress); *dbg_status = DBG_CONTINUE; break; } case EXCEPTION_DEBUG_EVENT: { EXCEPTION_DEBUG_INFO &exception = dbg_event->u.Exception; PVOID &addr = exception.ExceptionRecord.ExceptionAddress; switch (exception.ExceptionRecord.ExceptionCode) { case STATUS_BREAKPOINT: // Same value as EXCEPTION_BREAKPOINT CONTEXT lcContext; lcContext.ContextFlags = CONTEXT_ALL; GetThreadContext(pi.hThread, &lcContext); if (addr == (PVOID)(0x0042A3D0 - 5)) { lcContext.Eip += 6; SetThreadContext(pi.hThread, &lcContext); PVOID a = (PVOID)(lcContext.Ecx); PVOID b = (PVOID)(lcContext.Edx); bf_t *n1 = get_bf_t_from_remote_memory(a); bf_t *n2 = get_bf_t_from_remote_memory(b); bf_print_str_(n1); printf(" * "); bf_print_str_(n2); printf(";\n"); destroy_bf_t(n1); destroy_bf_t(n2); } if (addr == (PVOID)(0x00434210 - 5)) { lcContext.Eip += 6; SetThreadContext(pi.hThread, &lcContext); PVOID a = (PVOID)(lcContext.Eax); PVOID b = (PVOID)(lcContext.Edx); bf_t *n1 = get_bf_t_from_remote_memory(a); bf_t *n2 = get_bf_t_from_remote_memory(b); printf("cmp "); bf_print_str_(n1); printf(" == "); bf_print_str_(n2); printf("\n"); destroy_bf_t(n1); destroy_bf_t(n2); } *dbg_status = DBG_CONTINUE; break; default: printf("Unhandled exception at %p.\n", addr); *dbg_status = DBG_EXCEPTION_NOT_HANDLED; } } default: *dbg_status = DBG_CONTINUE; } return; } bf_t *get_bf_t_from_remote_memory(PVOID addr) { bf_t *tmp = new bf_t; ReadProcessMemory(pi.hProcess, addr, tmp, sizeof(bf_t), NULL); limb_t *tmp_limb_t = new limb_t(tmp->len); ReadProcessMemory(pi.hProcess, tmp->tab, tmp_limb_t, tmp->len * sizeof(limb_t), NULL); tmp->tab = tmp_limb_t; return tmp; } void destroy_bf_t(bf_t *n) { // 这里写的有 bug(程序会崩溃),但既然这只是个 dirty script, // 并且我也懒得找是哪儿写崩了,那么我们就直接砍掉这个函数吧 return; delete n->tab; n->tab = nullptr; delete n; n = nullptr; } void bf_print_str_(const bf_t *a) { slimb_t i; if (a->expn == BF_EXP_NAN) { printf("NaN"); } else { if (a->sign) putchar('-'); if (a->expn == BF_EXP_ZERO) { putchar('0'); } else if (a->expn == BF_EXP_INF) { printf("Inf"); } else { printf("0x0."); for (i = a->len - 1; i >= 0; i--) printf("%08x", a->tab[i]); printf("p%d", a->expn); } } } void Bp(LPVOID BpAddr) { BYTE INT3 = 0xcc; BYTE trampo[7] = {0, 0, 0xcc, 0x90, 0x90, 0x90, 0x90}; BYTE jmp[2] = {0xeb, 0xf7}; ReadProcessMemory(pi.hProcess, BpAddr, trampo, 2, NULL); // move WriteProcessMemory(pi.hProcess, BpAddr, jmp, sizeof(jmp), NULL); WriteProcessMemory(pi.hProcess, (LPVOID)((int)BpAddr - 7), trampo, sizeof(trampo), NULL); FlushInstructionCache(pi.hProcess, (LPVOID)((int)BpAddr - 7), 9); } void SetBreakPoints() { Bp((LPVOID)0x00434210); Bp((LPVOID)0x0042A3D0); } void CreateDebuggee() { ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); CreateProcess(ProcessNameToDebug, NULL, NULL, NULL, FALSE, DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi); } int main() { CreateDebuggee(); SetBreakPoints(); DEBUG_EVENT debug_event = {0}; for (;;) { if (!WaitForDebugEvent(&debug_event, INFINITE)) return 0; DWORD dbg_status; ProcessDebugEvent(&debug_event, &dbg_status); ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, dbg_status); } }
值得夸奖的是 binary 里面这些函数的头部都留有类似 lea edi, [edi+0]
这样又长又没用的废话指令方便我们进行 hook。
把给的 UserName / SN 拿来跑跑:
0x0.ac000000p6 * 0; 0x0.ac000000p6 * 0x0.d4000000p6; 0x0.ac000000p6 * 0x0.92800000p12; 0x0.ac000000p6 * 0x0.c4fe0000p17; 0x0.ac000000p6 * 0x0.845b2a00p23; 0x0.ac000000p6 * 0x0.b1da84a0p28; 0x0.ac000000p6 * 0x0.eefda25780000000p33; 0x0.ac000000p6 * 0x0.a092691354000000p39; 0x0.ac000000p6 * 0x0.d7c4bd31fd000000p44; 0x0.ac000000p6 * 0x0.90f82f1d960d8000p50; 0x0.ac000000p6 * 0x0.c2cd7f4fc1a28c00p55; 0x0.ac000000p6 * 0x0.82e2118996193820p61; 0x0.ac000000p6 * 0x0.afdfc790e1b1e378p66; 0x0.ac000000p6 * 0x0.ec54b42aaf4709a9c2000000p71; 0x0.ac000000p6 * 0x0.9ec8e90cadc3ba7e10180000p77; 0x0.ac000000p6 * 0x0.d55df929097f029965b08000p82; 0x0.fe000000p7 * 0x0.907c1b9ed001586ff0330000p81; 0x0.c8000000p7 * 0; 0x0.c8000000p7 * 0x0.80000000p1; 0x0.c8000000p7 * 0x0.ad000000p8; 0x0.c8000000p7 * 0x0.87640000p15; 0x0.c8000000p7 * 0x0.d38e5800p21; 0x0.c8000000p7 * 0x0.a54739c0p28; 0x0.c8000000p7 * 0x0.811fa52620000000p35; 0x0.c8000000p7 * 0x0.c9c1720ba6000000p41; 0x0.c8000000p7 * 0x0.9d9f211919ba0000p48; 0x0.c8000000p7 * 0x0.f648a3b738330000p54; 0x0.c8000000p7 * 0x0.c068bfe723e7d8e8p61; 0x0.c8000000p7 * 0x0.9651d5ec940d2177f0000000p68; 0x0.c8000000p7 * 0x0.eadfde41a754844b78800000p74; 0x0.c8000000p7 * 0x0.b77ee5a34aba075af62c8000p81; cmp 0x0.8f5b2367926155bf1052ca00p88 == 0x0.8f5b2367926155bf1052ca00p88
经过几轮观察,猜测 UserName 的作用除了计算出一个 BigInt1 之外还对某种形式的表进行了搅乱,这个表用来将 SN 的每 2 个 bytes 置换为一个 100 内的十进制数,然后这些十进制数会被不断拼接成一个 BigInt2,进而与 BigInt1 进行比较。于是我在 UserName 固定时将验证算法简单地抽象成了 assert(f(UserName)==g(SN)),其中 f、g 互不相干,固定 UserName 为 KCTF
后可以得到 BigInt1 为:
0x0.c95140ea9f2e45d83f013800p88
现在需要将这个数转换为十进制,Python 直接处理这种数据会丢失精度,因才疏学浅不了解其它更好的方法,我手糊了一个脚本用来生成算式:
def gen_exp(h): # h: 0x0.xxxxxxxxxpxxx h = h[4:] significand, exp = h.split('p') exp = int(exp) s_sum = '(' for i in range(len(significand)): s_sum += str(int(significand[i], 16)) + f'/({16**(i+1)})+' s_sum = s_sum[:-1] + f')*({2**exp})' print(s_sum) gen_exp('0x0.c95140ea9f2e45d83f013800p88')
然后把输出丢给科学家们用的 Mathematica 求解:
In[0] = (12/(16) + 9/(256) + 5/(4096) + 1/(65536) + 4/(1048576) + 0/(16777216) + 14/(268435456) + 10/(4294967296) + 9/(68719476736) + 15/(1099511627776) + 2/(17592186044416) + 14/(281474976710656) + 4/(4503599627370496) + 5/(72057594037927936) + 13/(1152921504606846976) + 8/(18446744073709551616) + 3/(295147905179352825856) + 15/(4722366482869645213696) + 0/(75557863725914323419136) + 1/(1208925819614629174706176) + 3/(19342813113834066795298816) + 8/(309485009821345068724781056) + 0/(4951760157141521099596496896) + 0/(79228162514264337593543950336))*(309485009821345068724781056) Out[0] = 243377798925556026477314360
简单地划分一下这个结果得到:02 43 37 77 98 92 55 56 02 64 77 31 43 60
。
接下来便是跑表的过程了,同样因为没有什么好办法,我开始了手动跑表,跑了一会儿发现在每个段(用 ---
隔开的部分)内 in-out 的映射关系是有规律的:
in out ------ d0 92 d1 93 d2 90 d3 91 d4 96 d5 97 d6 94 d7 95 d8 error d9 error da 98 db 99 --- 70 32 71 33 72 30 73 31 74 36 75 37 76 34 77 35 78 error 79 error 7a 38 7b 39
于是后续阶段里每个段只需要跑一个结果即可推出剩余的部分。
得到表之后,把 02 43 37 77 98 92 55 56 02 64 77 31 43 60
映射回来,即为本题的 flag: 40017535dad01714402635730122
。
PS. 我在手动跑表的过程中得知附件被换掉了,内心顿时受到 1w+ 的暴击伤害并直接回到了泉水(指床)开始了为期一晚的挂机疗养,后经美味 pizza 与强壮大腿的滋润才得以康复。
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界