2024 KCTF 大赛于8月15日正式开赛!比赛设置了多维度的评分体系,包括难度值、火力值和精致度积分,旨在引导竞赛的难度和趣味度,使其更具挑战性和吸引力。同时,也为参赛选手提供了更加公平、有趣的竞赛平台。
今天中午12点,第三题《绝境逢生》已截止答题,【Nepnep】战队用时19小时45分24秒抢先攻破此题,第二名来自【雨落星沉】战队、第三名来自【tacesrever】战队。
*注意:签到题《逐光启航》持续开放,整个比赛期间均可提交答案获得积分
本题共持续了4天,且仅有3支战队成功提交flag,想必确实有一定的难度。接下来一起看下该题的设计思路及解析吧。
出题战队:水泊梁山
战队成员ID:zZhouQing、寞叶_、0xchang
设计思路
设计了一个保护框架,程序的算法较为简单。
在之前,我拿到了一份 vmp38 样本,感觉指令变形的样子挺有特色,所以照猫画虎抄了过来。
指令变形主要是基于栈的混淆,学习的样本是 Tencent、滴水壳。效果如上图所示。
将 jcc 指令抹去,通过 hash 等操作直接跳转到 branch/target。不过在本题当中,我并未开启 ShadowJcc 选项。
将与代码分支相关的指令做变形。可以看到 IDA 在默认状态下,已经无法正确分析控制流。
可以看到有一些 ObfXX 字样的代码文件,通过宏的方式讲常量隐藏起来。不过我的编译器设置直接优化掉了,在本题中并未体现其特征。
我将程序的一些关键变量统一地交给 User 管理,防止被暴力搜索引用发现 Flag。
NtSetInformationThread/NtQueryInformationThread
RDTSC/timeGetTime
IsDebuggerPresent
值得注意的是,反调试结果与 Flags 的计算相结合。
在 ReadMe.md 的目录下,存在着 AntiDbgSigCollect.py 文件,这是我一开始准备用来辅助特征检测的脚本,不过后来并未打算添加。因为我一开始是写来针对 CpuDbg 师傅的。
用到如下内容:
查表移位
RC6
DES
CRC32
MD5
正常登录时如图所示:
被检测到反调试时如图所示:
一个基本块被引用多少次,就会相应的构建多少个基本块。(或者说是基本块的复制)
// 这串代码中,会构建 4 次基本块。
jmp_1(LABEL_1);
jmp_2(LABEL_1);
jmp_3(LABEL_1);
jmp_4(LABEL_1);
LABEL_1:
nop();
nop();
nop();
nop();
印象里花指令的模板源是抄的 OllyDbg 的插件 DeJunk.dll。(似乎这个插件又集成了其他插件的模板,很强大)
;Originary Write by ljtt
S = 9C6A??730BEB02????E806000000????73F7????83C404EB02????FF0C247101??79E07A01??83C4049DEB01??
S = 9C720AEB01??E805000000????72F4??83C4049DEB01??
S = 72037301??
不过遗憾的是,花指令功能似乎与其他功能发生了冲突,我并没有开启。
变形的基础是栈,我們知道,vmp 38 引入了内存的概念,在内存上做文章。
出現形式如下:
push [addr]
push [addr]
mnemonic r,rm
mnemonic r,imm
pop [addr+rd_offset]
pop [addr+rd_offset]
我感觉其挺有特色,抄了过来。(同时也借鉴了 x_tvm.exe)
一般地情況下,基本塊若要實現跳轉,需要經過如下過程。
cmp reg,const_v\cmp [mem],const_v\cmp [rm],const_v
jcc branch_target
基本思路便是混淆 const_v,混淆 branch_target,各位請看實現過程。
以 Cmp_rm32_imm8 類型指令爲例,將 Cmp_rm32_imm8 與 Je_rel32_32 倆條指令替換成如下形式:
// 注:
// [mem] 存儲 const_v
// this_ins_next_2_ip je 指令對應 IP 的下倆條指令地址
xor dword [mem],reg
cmp dword [mem],hash_branch_target
je this_ins_next_2_ip
jmp [mem]
此時,KCTFEr 若要逆向循環次數,變需要對 const_v 進行逆向,或進行統計記錄分析,有著不錯的反調試效果。
不过遗憾的是,ShadowJcc 功能似乎与其他功能发生了冲突,我并没有开启。
实际上小菜还逆向了 Safengine24 ,给它抄了一下,不过没有合并到 2024年KCTF水泊梁山 的代码里。
当然,我也得注意加强自身的代码和安全能力,这次的题目花指令和ShadowJcc没有增加上去,很是遗憾。
指令变形的模板是我随便想的,没有依据 eflags 进行设计,程序执行流程可能出错。这可能从理论上说明了,在一定程度上的验证算法层面可能有唯一解,但添加了指令变形这一影响因素,可能存在多解。
要解决这一问题,我想我需要做一张 eflags 的真值表,基于 eflags 真值表来编写基于栈的混淆。这一基础性的工作完成了,我想也就可以肆无忌惮地套用具有一定周期性的理论或是布尔代数来混淆。
如果完成了这一基础性工作,我想也就可以用来证明程序验证算法是否有可能出现因eflags导致的多解了。
在题目当中,我只测试了 KCTF 和 sha256(file,16) 是能正常工作的,但测试并不严谨。倘若坛友感兴趣,欢迎讨论交流。
这次的题目,经过混淆后的执行代码就有至少 1690257 字节未利用。这样的效果实在是令人难以接受。
在发现混淆后的效果令人难以接受后,发现将程序进行压缩能有不错的效果。利用 upx 加壳,印象里是将 10mb 压缩到了 1mb,这可能说明了我的混淆仅仅只有 1mb 的量,或者说我的混淆 熵值 太低,安全性是不够的。
这说明了混淆至少需要改进俩个地方:
变形后的熵值(需要多样化的变形)
增大内存空间的利用率
这也就需要我设计一个计算一定范围的代码块的熵值算法。我想,可以从 Nisy 前辈的 Baymax toOls 入手。
混淆至少需要修复一个地方:变形代码影响的 eflag 与原指令保持一致。
当然,我会保留原有的混淆形式,通过宏的方式进行管理,如果你认为当前的函数对执行流程不用过多在意,那就用吧!
赛题解析
本赛题解析由看雪论坛大牛【上学困难户】提供,来自Nepnep战队。
一、初步分析
拿到题目,我们稍微调试分析可以发现:
代码混淆的比较严重
call xxxx均被隐藏
程序带有反调试
这些大幅增加了静态分析的难度。程序还实现了多种反调试技术,如检测调试器等,让动态调试也有些许麻烦。
1.1分析过程:早期分析流程
这里基本上直接人工整理代码,我们可以知道大概算法,随后得到SN即可,下面放出加密脚本和大致算法流程,如果有误请在评论区提出。
SN分5小节,每小节计算大致如下:
Name与8位的特定数组字节加,之后循环与i,table1,table2按字节xor,最后将其看作两个dword量,进行xor 0x506。然后进行base编码,再对编码结果进行加密(process_data),得到的就是第1小节SN。第3小节类似该流程
第2小节为Name进行CRC后,xor 0xffffffff,之后减去第1小节SN的开头4字节,然后加0x1000,最后xor 第1小节SN开头的4字节。第4小节类似,不过这里要对第2小节CRC,然后not即可(最终结果用十六进制的文本值,如21ECFFFF)。
第5小节为DES部分,要求解密结果下列计算的值,既等于Name进行base编码后进行CRC,然后对其结果xor 0xffffffff后的文本值。
如下是第一小节和第三小节会用到的算法脚本。其中input_values输入BASE编码的值即可,输出既为SN。
def inv_process_data(const_list, input_data):
temp = [0] * 6
temp[3], temp[2], temp[1], temp[0] = input_data
pos = 0
temp[2] = (temp[2] + const_list[pos]) & 0xFFFFFFFF
pos += 1
temp[0] = (temp[0] + const_list[pos]) & 0xFFFFFFFF
pos += 1
for _ in range(5):
temp[4], temp[3], temp[2], temp[1], temp[0] = temp[3], temp[2], temp[1], temp[0], temp[3]
calculation = ((32 * temp[3]) ^ 3) * temp[3] & 0xFFFFFFFF
shift_val = ((calculation << 4) | (calculation >> 28)) & 0xFFFFFFFF
temp[4] = shift_val
calculation = ((32 * temp[1]) ^ 3) * temp[1] & 0xFFFFFFFF
shift_val = ((calculation << 4) | (calculation >> 28)) & 0xFFFFFFFF
temp[5] = shift_val
temp[0] ^= temp[4]
calculation = temp[0] & 0xFFFFFFFF
temp_val = (0x20 - temp[5]) & 0x1F
temp[0] = ((calculation >> temp_val) | (calculation << (temp[5] & 0x1F))) & 0xFFFFFFFF
temp[0] = (temp[0] + const_list[pos]) & 0xFFFFFFFF
pos += 1
temp[2] ^= temp[5]
calculation = temp[2] & 0xFFFFFFFF
temp_val = (0x20 - temp[4]) & 0x1F
temp[2] = ((calculation >> temp_val) | (calculation << (temp[4] & 0x1F))) & 0xFFFFFFFF
temp[2] = (temp[2] + const_list[pos]) & 0xFFFFFFFF
pos += 1
temp[3] = (temp[3] + const_list[pos]) & 0xFFFFFFFF
pos += 1
temp[1] = (temp[1] + const_list[pos]) & 0xFFFFFFFF
return list(reversed(temp[:4]))
a_constant_list = [
0x7796B480, 0x77A446B0, 0x77972020, 0x779769B0, 0x77A44730, 0x77A447F0,
0x77975790, 0x77A44870, 0x779725B0, 0x779769B0, 0x77A44780, 0x77A447F0,
0x779AD4F0, 0x77A44870
]
input_values = [0x6952754A, 0x30695130, 0x00000000, 0x00000000]
result = inv_process_data(a_constant_list, input_values)
for number in result:
hex_str = format(number, '08x')
reversed_str = ' '.join([hex_str[i:i+2] for i in range(0, len(hex_str), 2)][::-1])
print(reversed_str.upper(), end=' ')
对于DES部分,不再过多阐述,这里可以直接去修改算法逻辑和数据来实现加密和解密。如果要逆向该部分算法的话,要注意置换那边是3bytes的左移和右移。
1.2分析过程:后期补充分析
早期选择用x64dbg+脚本来处理混淆,效果一般。
后期采用IDAPython配合各类第三方库,效果还好,但是脚本调试到最后有各种bug,这里就先给出处理了部分混淆后的IDA截图。
整体算法结构和反调试相关部分勉强能看,如果想要更细致的反混淆,那就需要花费漫长的时间了。
二、总结
带着混淆直接分析起来会比较麻烦,还是得想办法处理掉混淆后看起来会好很多,不然分析算法会非常头疼。
主要难点还是前面的加密算法和后面被修改过的DES的分析,以及如何处理反调试等。
对于反调试,本题实现了包括IsDebuggerPresent、NtSetInformationThread、ZwQueryInformationThread等API检测,以及时间检测等技术。可以通过修改调试器行为、Hook相关API等方式绕过。感兴趣的可以自行搜索,我这边测试用插件可以绕过。
今日中午12点,第四题 神秘信号
正式开赛
球分享
球点赞
球在看
点击阅读原文查看更多