前言
第一次参加Flare-On挑战赛,经过了一周半左右的不懈坚持与坐牢奋斗,最终实现了9道题目的全解,且是国内真正实现全解的选手中名次比较靠前的(第3名),个人还是比较惊讶和非常满意的,毕竟自己一开始也没想到居然能攻克到最后一关并实现全解,但不知不觉地就完成了。题解是边做边写的,可能有些跳跃(毕竟做题的时候就是一个思路或者灵感跳跃到另一个,难免会有回看之后收获新发现的情况),对于特别复杂的题,已经尽可能地整理成按顺序的思路了,还请见谅。
#1 Drill Baby Drill!
只有一个异或,爆破即可
1 | encoded = b"\xd0\xc7\xdf\xdb\xd4\xd0\xd4\xdc\xe3\xdb\xd1\xcd\x9f\xb5\xa7\xa7\xa0\xac\xa3\xb4\x88\xaf\xa6\xaa\xbe\xa8\xe3\xa0\xbe\xff\xb1\xbc\xb9" |
drilling_for_teddies@flare-on.com
#2 project_chimera
先反编译第一层:
1 | import zlib |
得到:
1 | 0 0 RESUME 0 |
base85解码后解压缩得到一段新的字节码,继续反编译:
1 | 0 0 RESUME 0 |
得到密钥b’m\x1b@I\x1dAoe@\x07ZF[BL\rN\n\x0cS’和密文b’r2b-\r\x9e\xf2\x1fp\x185\x82\xcf\xfc\x90\x14\xf1O\xad#]\xf3\xe2\xc0L\xd0\xc1e\x0c\xea\xec\xae\x11b\xa7\x8c\xaa!\xa1\x9d\xc2\x90’,其中密钥有一个xor 42+idx的自解密,解密得到G0ld3n_Tr4nsmut4t10n,再解密即可
Th3_Alch3m1sts_S3cr3t_F0rmul4@flare-on.com
#3 pretty_devilish_file
文件是一个伪造的pdf,其中有一个320字节的buffer似乎是zlib压缩格式,但是被AES加密了,先解密:
1 | qpdf --decrypt --stream-data=uncompress pretty_devilish_file.pdf decrypted.pdf |
然后查看解压出来的内容,发现了一串hex:
1 | ffd8ffe000104a46494600010100000100010000ffdb00430001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101ffc0000b080001002501011100ffc40017000100030000000000000000000000000006040708ffc400241000000209050100000000000000000000000702050608353776b6b7030436747577ffda0008010100003f00c54d3401dcbbfb9c38db8a7dd265a2159e9d945a086407383aabd52e5034c274e57179ef3bcdfca50f0af80aff00e986c64568c7ffd9 |
fromhex后是一个jpg图片,只有一排像素,有明暗相间条纹,读取灰度:
1 | from PIL import Image |
Puzzl1ng-D3vilish-F0rmat@flare-on.com
#4 UnholyDragon
MZ头被改,改回去,程序运行后生成了4个副本并退出,编写脚本比较:
1 | import sys |
发现有4处恰好分别有1、2、3、4个副本不同(虚拟偏移量=真实偏移量+0x400C00):
1 | --- Total 4 offsets have difference --- |
start:
1 | int start() |
是自己实现的启动函数,由于不好定位关键逻辑,所以尝试断点WriteFile函数,发现sub_4A8AC8是实际执行的函数:
1 | void __stdcall SUB_4A8AC8_patchfile(int Hi, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) |
其中第一次断点时lpBuffer的值就是第一个被修改的字节0x94,nNumberOfBytesToWrite_1是1,查询谁调用了这个函数,得到sub_4A8F30,其中有一些关键逻辑:
1 | 416: |
因为本质是基于固定表的单字节的异或加密,所以直接把150改成0即可,运行发现生成了一堆exe,运行其中的编号150得到flag:

dr4g0n_d3n1al_of_s3rv1ce@flare-on.com
#5 ntfsm
程序中存在一个12M的巨型函数sub_14000C0B0,以及一个90781项的跳转表jmptable_140C687B8,在其反编译之前按U取消定义,否则会卡死
注意到函数体中都会jmp到loc_140C685EE,猜测此为控制流分发函数,跳转表内为全部函数的顺序
程序从start开始做了一些简单的初始化后直接进入0x14000C0B0,从这里开始分析,发现只要进行代码操作就会因跳转表无法解析卡死,所以直接清零整个跳转表后再识别
1 | import ida_bytes |
然后将0x14000C0B0的位置开始反编译,直到末尾出现一个jmp rcx(间接跳转):
1 | __int64 __fastcall main_logic(int n2, __int64 a2) |
程序是一个有限状态机,第一次初始化时,将输入存入程序的:input数据流,之后都从这个数据流读取(除非-r清除),查看其隐藏数据流的方式:
1 | $FilePath = ".\ntfsm.exe" |
程序用输入的16字节经过处理后检测返回值是不是16,如果是则解密flag:
1 | __int64 __fastcall Reward_function(__int64 a1) |
定义了一个64字节密文,并用之前正确输入的sha256做密钥进行AES_CBC解密
动态调试发现会jmp rcx,此时进入跳转表第一个函数:
1 | .text:0000000140860241 sub_140860241 proc near |
比较第一个字节是J、U、i中的一个,根据其不同向跳转表指针状态位+2、3、1,否则弹窗,之后返回到控制流分发函数,保存上一个函数中的状态值,并跳转到下一个函数,之后所有的函数都是这个形式,因此编写规则匹配:
1 | import re |
刚好匹配到全部90781个函数,提取其后的比较和增加量:
1 | import re |
用输出的out.txt和jmptable进行dfs:
1 | import sys |
得到唯一解:iqg0nSeCHnOMPm2Q
f1n1t3_st4t3_m4ch1n3s_4r3_fun@flare-on.com
#6 Chain of Demands
发现是pyinstaller打包的elf文件,提取并反编译:
1 | import tkinter as tk |
是一个伪区块链题目,实际上给出了对话日志和RSA公钥:
1 | [ |
LCG使用主机名称生成,但无法猜测主机名称,查看公钥:
1 | -----BEGIN PUBLIC KEY----- |
直接解密后转hex:
1 | 30820121300d06092a864886f70d01010105000382010e003082010902820100 |
得到
1 | n=966937097264573110291784941768218419842912477944108020986104301819288091060794069566383434848927824136504758249488793818136949609024508201274193993592647664605167873625565993538947116786672017490835007254958179800254950175363547964901595712823487867396044588955498965634987478506533221719372965647518750091013794771623552680465087840964283333991984752785689973571490428494964532158115459786807928334870321963119069917206505787030170514779392407953156221948773236670005656855810322260623193397479565769347040107022055166737425082196480805591909580137453890567586730244300524109754079060045173072482324926779581706647 |
n在factordb上已有分解记录:
1 | n = |
直接解密即可:
1 | from Crypto.Util.number import * |
#7 The Boss Needs Help
初始分析
程序主要函数均被加上混淆,分析流量包,发现格式主要有以下几种:
192.168.56.103:第一次是Bearer在twelve.flare-on.com:8000上发送GET请求,之后被一个奇怪的Host theannualtraditionofstaringatdisassemblyforweeks.torealizetheflagwasjustxoredwiththefilenamethewholetime.com:8080劫持,如果有额外消息会被加密后用json格式传递
1 | GET /good HTTP/1.1 |
1 | GET /get HTTP/1.1 |
1 | POST / HTTP/1.1 |
192.168.56.117:接受get和post请求,也按密文发送
1 | HTTP/1.0 200 OK |
流量包中有一个518KB的hex消息,猜测是某种文件,此外劫持的host名称意为“每年的惯例都是往往花了几周逆向结果发现flag只是和文件名xor了一下而已”,可能是某种提示
程序中数据段有SHA-256的常量表和AES的S盒/逆S盒,其中AES的S盒、逆S盒在两个AES-256-CBC加密/解密函数 0x7FF6B5C70D20 0x7FF6B5C70DC0 中使用(基地址0x7FF6B5C20000),且这个函数被两个混淆函数 0x7FF6B5CF3E60 0x7FF6B5D57960 所调用,猜测为关键逻辑
反混淆
方法一:提高反编译阈值,强制反编译
此方法可以反编译main函数,对于所有能够反编译出来的函数,使用arg追踪法:
如果一个函数有参数,那么统计每个参数被xref到的行,从最小的行开始,只统计过程中遇到的有关变量,无关变量不做统计。
注意到地址0x7FF6B608A540的位置的AES逆S盒有一个被混淆的xref:
1 | void __fastcall sub_7FF6B5C78A00(__int64 a1, __int64 a2, unsigned __int64 a3) |
方法二:尝试解混淆
此处可以看出来是一个魔改AES,在S盒替换的步骤有着额外的异或操作,观察汇编发现,似乎只有rax、rcx、rdx大量参与了控制流混淆过程,r8、r9、r10似乎也参与了,nop前后函数体不变,编写脚本nop所有相关汇编指令(不包含call):
1 | import idc |
该脚本只能对一条链(一个case)使用,否则会破坏跳转,分析更大的函数(如main)发现关键逻辑均不会受到混淆部分代码影响,因此可以直接nop整个范围,对main使用该脚本:
1 | import idc |
之后反编译发现main中还有一个try…catch块0x7FF6B60829A4,把这块也处理一下(0x7FF6B60829B1 ~ 0x7FF6B6084EC4,成功解混淆main,且依然可以调试:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
那么,现在可以用相同的方法解混淆其他函数,比如BigFunc1(0x7FF6B5CA1590):
1 | START_EA = [0x7FF6B5CA15BF, 0x7FF6B5CA7974, 0x7FF6B5CAE1BF, 0x7FF6B5CB1603, 0x7FF6B5CB481B, 0x7FF6B5CB8037, 0x7FF6B5CBB8CE, 0x7FF6B5CBEC51, 0x7FF6B5CC219F, 0x7FF6B5CC54BE, 0x7FF6B5CC8F6F, 0x7FF6B5CCC244, 0x7FF6B5CCF593, 0x7FF6B5CD2CAF, 0x7FF6B5CD61A4, 0x7FF6B5CD9537, 0x7FF6B5CDCF8B, 0x7FF6B5CE0443, 0x7FF6B5CE38CD, 0x7FF6B5CE6D25, 0x7FF6B5CE9FDA, 0x7FF6B5CED27B, 0x7FF6B5CF0634] |
反编译出来的BigFunc1并调试:
1 | __int64 __fastcall BigFunc1(__int64 a1) |
到这里已经可以对消息的第一条中出现的Bearer e4b8058f06f7061e8f0f8ed15d23865ba2427b23a695d9b27bc308a26d进行解密了,解密结果为2025082006TheBoss@THUNDERNODE,这个数据不足32字节,暂时不知道有什么用,继续调试:
1 | memset_with_0(v143, 64); |
这部分构造了HTTP请求:
1 | User-Agent |
就是上面的第一次发送的请求,继续分析:
1 | v59 = v58; |
这部分在抓包,没抓到包或者响应不是200就从这里退出了
1 | v68 = v129; |
后面这部分处理了包的内容,提取其中有效的信息:
1 | memset(&buf__11, 0, sizeof(buf__11)); //初始化区域 |
服务器传回的信息中的密文会被切片后填入结构体的偏移量192和224的位置
继续反混淆func2和3(func3还有一个try…catch块0x7FF6B607BA13),由于大函数均采用这种模式,所以可以进一步编写自动化脚本:
1 | import idaapi |
反编译出来的bigfunc2:
1 | __int64 __fastcall BigFunc2(__int64 a1, __int64 a2) |
func2中定义了一个AES_CBC加密,iv为000102030405060708090a0b0c0d0e0f,Key的构造过程则比较复杂:
1 | *(_DWORD *)v34 = 0; |
查看时间格式获取,发现是一个自解密(已去除混淆):
1 | *(_DWORD *)(v2 + 520) = 0x103971E; |
解密之后是%H,也就是小时(依据流量包在这里是06),查看GEN_AES_KEY:
1 | __m128 **__fastcall GEN_AES_KEY(__m128 **key, char *a2, __int64 **a3, _QWORD *p_time_H) |
是把两个block取sha256后相互异或得到密钥,两个block分别来自main中的320字节结构体的偏移量160处的内容和192处的内容+两位小时时间戳(06),经过BigFunc1后,160的位置应该会变成2025082006TheBoss@THUNDERNODE,192处的字节未知
调试
在虚拟机上配置好用户名、机器名、hosts,编写服务器脚本接受程序响应并发送第一个包:
1 | from http.server import HTTPServer, BaseHTTPRequestHandler |
发现BigFunction1可以正常继续执行,192处的截断符是“@”,下断点查看解密的密文,是peanut@theannualtraditionofstaringatdisassemblyforweeks.torealizetheflagwasjustxoredwiththefilenamethewholetime.com:8080,所以192处的实际字符是peanut,动态调试发现在BigFunction2的AES密钥生成处,偏移量160的位置实际上回到了TheBoss@THUNDERNODE,动态调试发现key_1为c30b158c92ffbc95e02b222206cec9676eac6654144e5557a2d97569c11ed8df,key_2为56a49e85c98bd96ce5b6217abc02eb5f3eec3ff4a937e1ccc549d30bcbc3b549,密钥为95af8b095b7465f9059d0358bacc2238504059a0bd79b49b6790a6620add6d96,编写脚本发现可以直接解密,解密整个流量包:
1 | from Crypto.Hash import SHA256 |
解密消息合集:
1 | b'{"ci":"Architecture: x64, Cores: 2","cn":"THUNDERNODE","hi":"TheBoss@THUNDERNODE","mI":"6143 MB","ov":"Windows 6.2 (Build 9200)","un":"TheBoss"}\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10' |
base64解密得到:Yeah, I get it. Some guys, they're happy just to turn the key and drive. But you... you gotta pop the hood. You gotta trace the wires, feel the heat comin' off the block. You're not looking to steal the car... you're just trying to understand the soul of the engine. That's an honest night's work right there.
1 | b'{"msg": "cmd", "d": {"cid": 6, "dt": 20, "np": "TheBoss@THUNDERNODE"}}\n\n\n\n\n\n\n\n\n\n' |
这里改变了block192处的值,变为了TheBoss@THUNDERNODE06,继续解密:
1 | b'{"msg": "cmd", "d": {"cid": 2, "line": "dir /b /s C:\\\\Users\\\\%USERNAME%\\\\Documents\\\\Studio_Masters_Vault\\\\"}}\x03\x03\x03' |
在65号对话导出一个zip,其中有flag.jpg但是有加密
1 | b'{"msg": "cmd", "d": {"cid": 6, "dt": 25, "np": "miami"}}\x08\x08\x08\x08\x08\x08\x08\x08' |
切换密码到miami
1 | b'{"msg": "cmd", "d": {"cid": 2, "line": "dir /b /s C:\\\\Users\\\\%USERNAME%\\\\Documents\\\\Personal_Stuff"}}\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b' |
解密得到:
1 | Email: BornToRun!75 |
密码是TheBigM@n1942!

C4N7_ST4R7_A_FLAR3_WITHOUT_4_$PARK@FLARE-ON.com
#8 FlareAuthenticator
程序的11个主要函数存在大量间接跳转,0x14008F160的位置有一个32768字节的数组,其后不远在0x140097180有一个36字节的数组,0x14009773C有一个1573字节的数组,0x14009C0E0有一个21662大小的间接跳转地址计算表,且导致有大量代码未识别,先编写脚本识别代码:
1 | import ida_bytes |
然后动态调试(需设置环境变量QT_QPA_PLATFORM_PLUGIN_PATH=所在路径),在start下断点,启动instruction tracing,然后运行脚本,trace所有的间接跳转:
1 | import idc |
此脚本记录了程序的整个流程中所有的间接跳转行为:加载GUI界面 → 第一次按下按钮 → 加载输入逻辑 → 按下OK触发检验 → 检验逻辑 → 关闭窗口 → 清零输入框 → 关闭程序 → 程序退出,其中橙色的是程序自己的逻辑,其中黄色的是用户的交互部分,这些交互部分持续时间长,过程中不会发生任何trace行为,所以以时间间隔作为切分标准,在输出中插入用户完成的部分,以此区分每个过程所用到的函数地址段.
分析得到的trace,发现有以下逻辑:

| 事件类型 | 地址范围 | 出现次数 | 对应函数 |
|---|---|---|---|
| 启动程序 | 0x140002D7B ~ 0x14007ED7E | 972 | main → sub_140037160 → sub_140001DE0 → sub_14000D1E0 → main |
| 加载输入 | 0x140013280 ~ 0x1400831D8 | 2 | sub_140012E50 & sub_140081760 |
| 触发校验 | 0x140021593 ~ 0x14002A500 | 16 | sub_1400202B0 |
| 清空输入 | 0x14000D57E ~ 0x14002B29C | 26 | sub_140012E50 |
| 退出程序 | 0x1400730F3 ~ 0x140075C85 | 1 | main |
因此,sub_1400202B0就是最关键的校验函数之一:

发现校验函数是大量线性化的,其中有很大一部分代码被跳过了,可能是未通过某种校验,重复的部分大概有15轮多一些的循环和8轮多一些的循环,接下来先查看到地址发生跳变的位置:
1 | .text:0000000140022113 jmp rax |
这里22113直接跳转到了下一行(22115),从[rbp+2950h+var_21C1]提取了某种校验结果,接下来jnz跳转到22124,否则直接跳到2795C,看起来这就是发生跳变的根本原因:此处的某种校验未通过,接下来动态调试,测试明文1234567890123456789012345,在函数开头断点,搜索内存,找到明文存储的位置:
1 | debug308:000002598BC39660 a12345678901234 db '1234567890123456789012345',0 |
发现同一次启动过程中明文输入是被写入栈上的同一个位置的,在该位置下硬件断点,写入一个字符后发现在0x140015A47的call rax触发了检测:
1 | __int64 __fastcall read_for_input_w(__int64 a1, const void *a2) |
这里就是负责把输入写到栈上的几个函数,同理可以定位到call清空输入函数的位置0x14000FAC1,但是尝试在明文上下断点,发现从按下检验按钮到弹出密码错误的全程根本没有被断到.
来到22113的位置,jnz改jz,发现下面有:
1 | .text:00007FF7E57F22DE movaps xmm0, xmmword ptr [rcx+rdx] |
此处恰好用xmm寄存器加载了之前的36字节数组的前32项,存入了栈上,也打上硬件断点
之后动态调试发现明文的部分被调用,在0x140060BF1(属于sub_14005EF60,这个函数在之前的trace记录中未出现)处的call rax中调用了strlen并求了明文的长度,猜测之前的test比较实际上是反调试,结果运行发现程序居然直接弹出了success,虽然结果是乱码,但这似乎说明检验逻辑还是前面的结果,后面的function应该是32字节密文的解密函数,这说明,实际上输入值的校验逻辑很少(只有从按下按钮到jnz那个位置的一点点),尝试将sub_1400202B0的所有jmp rax全nop掉,发现并不影响后续的解密,因此全部nop后反编译函数,查看前半段:
1 | void __fastcall sub_1400202B0(__int64 a1) |
可以发现其中有一些关键数据(比如7499)和一些比较逻辑:
1 | if ( **v23 != v870 ) |
这里左侧v23是一个定值7499(0x1D4B),右侧是一个随机值(很大,低16位是全零或者1,并且有小概率整个qword都是某固定的14字节乱码的一部分),几乎不可能和左侧相等,而且其生成过程似乎无法在校验函数中定位相关逻辑,校验函数也存在其他方面的难以理解的问题,推断是ida反编译错误,分析汇编(从跳转点开始往上分析):
1 | .text:1400021E09 mov rcx, [rbp+2950h+var_2148] |
发现要让al = 1,则必有[[rbp+2950h+var_3C8]+0x78] == 0x0BC42D5779FEC401
查看这个位置的值,双击两次后将地址加上0x78发现了一个神秘的int64,应该就是要比较的位置,实测改完之后通过了校验(虽然解密失败),说明这里就是校验位,在这里打上硬件断点,动态调试发现每输入1位这里就被改写一次,大概在0x140016AD4附近被修改:
1 | .text:1400015A47 input_pwd: |
发现tag的值是来源于每一位分两次带入函数0x1400081760的返回值相乘之后相加的总和,一共有25位 * 10种 = 250个数据
提取250个乘积数据后,编写z3脚本求解:
1 | from z3 import * |
找到解:4498291314891210521449296
s0m3t1mes_1t_do3s_not_m4ke_any_s3n5e@flare-on.com
#9 10000
#9.1 主程序初始分析
程序中存在1GB的数据,分析主函数:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
程序首先获取目标license.bin,读到34万字节后,每34字节带入检验,前2字节作为dll的索引,后32字节作为输入,过了dll内check函数的校验之后还需要过外层memcmp校验(因为有顺序问题),查看负责加载程序中的10000个校验dll的函数:
1 | _OWORD *__fastcall LOAD_DLL(__int64 idx) |
共计有10000个dll(0000~9999),以某种自定义格式压缩,每次运行时会递归解压缩所有主dll所需的dll并缓存在内存中,之后使用时直接调用,解压完毕后程序会查找主dll内的check函数,找到后用其检验license中的32字节,10000个全部通过且Output等于Target中的10000个dword即用dll的sha256作为AES256密钥解密密文并输出flag
#9.2 提取dll数据并解压
编写脚本提取所有dll:
1 | import pefile |
提取后同构两个函数并解压所有dll,由于原解压函数十分复杂,所以可以直接粘贴ida的反编译代码,发现计算长度的函数不用改就能用,但是解压缩的函数会报错,所以可以采用直接粘贴字节码的方式:
1 | code = bytes.fromhex("") |
主程序:
1 | __attribute__((naked)) long long LZSS_decompress( |
提取后得到10000个dll
#9.3 初步分析dll check函数模式 && 自动化提取
发现其中每个dll均有数百个名为fxxxxxxxxxxxxxx的单个校验函数导出,check函数均遵循格式:
1 | _BOOL8 __fastcall check(unsigned __int8 *a1, __m128 a2, __int64 a3, __int64 a4, __int64 a5) |
通过外部调用的fxxxxxxxx函数名称,发现dll名字顺序与资源顺序相同,编写脚本提取所有dll的导出表:
1 | import os |
再编写能提取dll的函数调用顺序的脚本:
1 | import r2pipe |
#9.4 模拟程序逻辑
然后实现程序的加载dll逻辑,加入更多调试打印,优化dll加载方式,并使校验函数可重复利用:
1 |
|
需要注意到,源程序中LOAD_DLL在尾部的调用v30 = ((__int64 (__fastcall *)(_DWORD *, __int64, _QWORD))v31)(Output, 1, 0);其实调用了DLLEntryPoint,HINSTANCE hinstDLL传入的是Output Buffer的地址,而非正常值,分析DLLmain函数,发现其在初始化时将内部的一个指针指向了Output Buffer,而Output Buffer的修改机制来源于以下函数:
1 | __int64 __fastcall free_obj(__int64 index) |
该函数会释放之前递归加载的dll链,同时如果有dll被释放,那么Output中该dll的索引处的值就会被加上当前所处的外层大循环的轮数,最终累加得到target,先写脚本提取直接调用:
1 | import os |
然后递归生成调用集合:
1 | import sqlite3 |
接下来提取target的值并解矩阵得到主dll的调用顺序:
1 | import sqlite3 |
得到顺序表和二值矩阵,接下来同构加密过程
#9.5 正向同构check运算过程
#9.5.1 前半部分
分析check函数中形如_Z21fxxxxxxxxxxxxxxxxxPh函数的模式,发现一共有3种,分别对应类卷积乘法(const 31字节,间隔是var30和28,不可逆,记为C模式),打乱顺序(32字节,间隔是60和58,可逆,记为B模式)和sbox替换(256字节,可逆,记为A模式),编写脚本提取所有常量:
1 | import os |
同构前面的_Z21fPh函数,需要注意的是,A/B/C模式函数中在开头均有一行*a1 ^= *(_DWORD *)off_268936B6020[id];,说明输入的前四个字节会被异或Output Buffer中对应DLL id索引的dword(需要注意:如果函数来源于别的dll,xor的是别的dll的索引),此处同构可以先按0处理:
1 | import sys |
发现运行结果相同
#9.5.2 后半部分
继续查看check的后半段:
1 | tag1 = 0xC78F79F3C2DE8CA7LL; |
在check的下半段中,一共有16+16个oword作为密钥/密文,3个oword/qword作为tag类的标签,编写脚本提取check中的34个常量(有一个tag初始化为0):
1 | import os |
分析check后半段函数逻辑:
1 | tag1 = 0xDC37C0E304978087uLL; |
此部分中v1实际上是前16个oword的高位,v2是其低位,v2与a1进行循环xor,xor的结果写回v2,即a1中的任何值在xor运算后的结果均不能大于等于tag1的值,这里先patch中间值以通过校验,继续查看,下面是数轮循环:
1 | count = 0u; |
其中所用到的辅助函数是128位的mod运算,简化后是:
1 | from sympy import * |
但实际上这是在求mcnt这个矩阵的行列式并证明其线性无关,因此可以进一步简化:
1 | from sympy import * |
最后面的部分:
1 | v88[0] = 1; |
这部分很简单,注意到这是刚刚的4×4矩阵在tag1整数环上的tag2次幂的快速幂计算,直接同构就行,完整同构:
1 | from sympy import * |
接下来实现check的完整逆向函数:
1 | import os |
解得0000.dll在位置0的明文是27fccd48105201db4cf5fb642b9456be2db4016839d67ee6a1aee9330dfebcef,带入验证发现正确,即逆向成功
#9.6 自动化求解
编写总控脚本,先手动复制文件夹并提取所有常量(同路径会导致线程竞争):
1 | import os |
以及check中的常量:
1 | import os |
然后再调用总求解脚本:
1 | import os |
哈希:600abf28e03c73471a73bb909210dc2d2b4e98c7577d6b71299d2e54d693d14d
Its_l1ke_10000_spooO0o0O0oOo0o0O0O0OoOoOOO00o0o0Ooons@flare-on.com
结语
虽然自己已经经历过不少国际赛小众又独特的题目的洗礼,但Flare-On的不少题目仍让我感觉耳目一新,在传统单调的解密/约束型逆向的基础上融合了不少其他方向的知识或者出题人的小巧思(比如第三题的伪PDF),而不局限于传统的分析逻辑-正向同构-逆向解密的线性形式。大部分题目也都可以或多或少地学到新东西,练习新的思路和解题方式(比如第7题的自动解混淆和第9题的自动化逆向等)。9个题的难度梯度逐级递进,与闯关的比赛形式相得益彰,从前几个题的“分钟级”难度,到第4、5、7、8题的“小时级”难度,再到最后的第9题的“天级”难度,让我这种喜欢赤石的人觉得打起来是一种享受而非坐牢(真的吗?)。今年的难关已经悉数攻克,明年继续来战!