微信号:ikanxue

介绍:致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号.

看雪 2016 CTF 第二题 Solution点评和分析

2016-11-06 18:47 Cherie

出题者:SilentGamb,来自武汉科锐的学员,目前正在3阶段学习,已经学完了汇编程序的编写,C,C++程序的逆向。


看雪版主&评委netwind点评


从作者提供的设计文档可以看出,作者非常认真,并且下了很大功夫。


该题目是一道算法题,作者为了提高难度将算法验证过程放在了LUA脚本里,另外用RC6把所有字符串进行了加密。


要想破解此题,首先得找到算法验证部分,并且得到注册码加密后的密文;然后要对算法有一定了解,能够识别出作者改写过的AES算法,接着就是找到crackme里AES解密的接口进行解密,在AES解密后,再进行一次RC6解密即可还原出注册码,


看得出作者技术扎实,对软件保护和破解有自己独到的认识!


下面选择了一位分析者HHHso的详细分析:


破解历程

1.  第一阶段的攻击确定了字符串解密机制、并初步猜测内嵌了脚本模块,并幸运低由解密机制关联出加密机制(为后续由加密逆推使用解密奠定了基调,解密注册码所需)
2.  第二阶段确认了Lua修改脚本模块并很幸运逆推到开发者修改参照的Lua版本(这为后续准确推断函数名称及准确反编译出脚本奠定了基础),这个过程整了一个通宵,11月5日早上七点多才睡下,可惜八点多就醒了。之后参加了十一点同学的婚礼,回来已经顶不住昏睡过去了,晚上吃了点夜宵继续。
3.  这要重审第一阶段的注册过程确定注册机制,并在算法还原和直接零碎调用之间做破解抉择。得到了注册码。

(1.1)
一开始从错误提示信息着手,想法肯定是寻找成功提示的信息点在哪里。
由于是中文,考虑到IDA string的缺陷,所以打算手工找找中文存储形式。
.text:0040121A push    eax
.text:0040121B push   [esp+4+hWnd] ; hWnd
.text:0040121F call    ds:SetWindow

(1.2)
溯源SetWindow调用栈时发现文本设置用的是一套通行机制,其中一环(命名)函数 Hi_SetTips_sub_4011C0 提供字符串和时间
而其前名是对中文字符串的内容的解密过程,解密函数为 Hi_maybe_dectrypstr_sub_41B422,追溯了所有引用点。
并通过在运行的OD的直接修改EIP的方式对所有引用点关联的字符内容进行一次统一解密(原因是解密代码可以重用,且解密部分代码片段耦合度高,堆栈相对稳定)
以下是直接修改OD EIP 执行片段解密出的大部分中文信息

(1.3) 
在解密函数Hi_maybe_dectrypstr_sub_41B422 中
基本过程是:
解密信息体初始化 Hi_decObjInit,
解密过程 Hi_decObjDecrypt,
解密信息移动 Hi_decObjCopyOut,
解密信息体析构 Hi_decObjDtor
而在 Hi_decObjInit 函数交叉引用分析中,确定了加密函数  0041B3B0 sub_41B3B0,其对 输入的注册码进行加密

(1.4) 
从(1.3)确认的 加密函数 sub_41B3B0 溯源 到 Hi_encrypt_sub_41B494,其上下文是获取输入的注册码,长度必须为 0x10的倍数,
且由函数 Hi_CheckAZaz09_sub_4018FB 检测保证必须是字母和数字 

(1.5)
紧接(1.4)继续溯源,基本验证过程就在 消息响应函数 sub_401482 中,
其先通过 
00401498 call    Hi_getEncSerail_sub_4017EA 获取加密后的序列号
提示正在注册 
004014EC call    Hi_SetTips_sub_4011C0 
”正在注册, 请稍候..."
然后进入主要注册检验环节


(2.1)
在(1.5)进入主要注册校验环节 sub_41AEF1 后,解密出改版的Lua脚本并执行,
一开始并为甄别确认 041AF84 call    luaL_loadbuffer 函数,蛮力分析后发现其自成一套读取使用解密出的字节内容的系统(后来确认为Lua的ZIO)


(2.2)
Lua的确认过程
这个过程开了挂,毕竟第一阶段的猜测只猜到了开始,对部分行数都使用了Hi_script_run_xxx的命名,也留意到了以下字符信息,
一开始百度了部分关键字符没有得到很直观的开发代码关联(一开始也没上谷歌,这个还是靠谱些,以前找开源代码特征每每都是它帮上大忙)
不小心瞄了一眼群上有些聪明的大侠纷纷给出了Lua的特征;恰好电脑上有自己之前搞分析
的Lua源码,也就上vs2010和UltraEdit遍历搜索了一下,很不幸命中了。

(2.3)
对着手头上的Lua源码进行函数的甄别,从lua.c的 main 到 pmain ,关键的 Lua_status 需要初始化,
在第一阶段猜定为自成一套的buf读写机制时,和其他的笼统分析中,都注意到了[xxx_reg + 10] 的输入参数,xxx_reg是一个固定信息体或对象的thisPtr指针;
这个信息体的最初由前面跟踪对注册码输入的合法性检验函数 004018FB Hi_CheckAZaz09_sub_4018FB  中引出 ,在这个函数中
的调用对二进制转成十六进制字符串信息,上面有调试检测,不过后续怎么影响,直接在OD加载后跳转到 IsDebuggerPresent
修改为(原谅,一般都只用原版OD,一般只带od script插件,不会自动帮我过滤反调试)
7527CA70 > 64:A1 30000000   MOV EAX,DWORD PTR FS:[30]
7527CA76   33C0             XOR EAX,EAX
7527CA78   90               NOP
7527CA79   90               NOP
7527CA7A   C3               RETN

(2.4)
跑题了!无论从程序入口start顺推还是从消息处理逆推,在 00401000 _WinMain@16 中的 00401013 call    sub_41AD8F 有对 该信息体进行初始化。
当然 [xxx_reg + 10] 也初步裁定为 lua_State
所以在 sub_41AD8F 中,猜定了 luaL_newstate 和 luaL_openlibs,并由 luaL_openlibs 的甄别得到了猜定的相互印证(作者修改lua所参考的版本的确定由luaL_openlibs关联是信息确定)
在0041ADD5 call    luaL_openlibs的入口引用了 0042E5A0 loadedlibs 信息表,
而我电脑的现有版本是Lua 5.1, 没有 "_G",也没有"utf8"
所以直接百度(还是谷歌?)忘了,关键字是 "lua  utf8",utf8为最新lua版本所支持,所以下在了Lua 5.33, 比对 Lua 5.33的 loadedlibs,果然匹配上了。
于是后期全部基于Lua 5.33做函数甄别。
关键的关联信息表

(2.5)
Lua 5.33 函数的甄别进行了很长时间,还是很有趣的。

在 sub_41AD8F 中,除了 

"0041ADC3 call    luaL_newstate","0041ADD5 call    luaL_openlibs",

最关键是对注册关联了两个Lua的C函数,实际破解工作基于
fnGetRegSnToVerify = 4019A2 Hi_fnGetRegSnToVerify
fnCalcUserInputRegSnAfterEnc = 4019C7 Hi_fnCalcUserInputRegSnAfterEnc
两个函数即可,不过还是完成的反编译了作者加密的"ls"版本的Lua脚本。

.text:0041AE16 push    offset Hi_fnGetRegSnToVerify
.text:0041AE1B push    dword ptr [esi+10h]
.text:0041AE1E call    lua_pushcclosure
.text:0041AE23 push    [ebp+lpMem]
.text:0041AE26 push    dword ptr [esi+10h]
.text:0041AE29 call    lua_setglobal

.text:0041AE76 push    ebx
.text:0041AE77 push    offset Hi_fnCalcUserInputRegSnAfterEnc
.text:0041AE7C push    dword ptr [esi+10h]
.text:0041AE7F call    lua_pushcclosure
.text:0041AE84 push    [ebp+lpMem]
.text:0041AE87 push    dword ptr [esi+10h]
.text:0041AE8A call    lua_setglo

(2.6)
Lua 脚本反编译
上述代码片段解密出 Lua脚本并加载,作者将 "Lua 5.33" 修改成了 "ls 1.1"
一开始找不到合适的Lua反编译工具,考虑到编译的0x400代码不多,最初做了
手工反编译(手工初步反编译的信息参考后续),看到手工的结果还是放弃了治疗。
短时间用python也写不出完备的反编译工具。幸运的是休息调整过后,找到了
https://github.com/viruscamp/luadec
最合适的是它支持5.33,下载它和着Lua 5.33的源码进行编译。得到了反编译工具。
当然,最合适的方式是也学作者把"Lua 5.33"改为 "ls 1.1"定制反编译工具。
不过基于Lua的机制分支,对比了dump出来d "ls 1.1"编译脚本ls_11_script.out,和Lua 5.33的 hello.out脚本,
选择了直接对 ls_11_script.out 文件头部修正为 "Lua 5.3",并进行反编译,结果还差强人意。反编译的Lua脚本和手工反编译的信息如下。
(还是工具好呀!)

(2.6.1)
工具反编译结果,应该联合前面的
fnGetRegSnToVerify = 4019A2 Hi_fnGetRegSnToVerify
fnCalcUserInputRegSnAfterEnc = 4019C7 Hi_fnCalcUserInputRegSnAfterEnc
信息形成完整lua脚本



手工反编译结果,code指令的操作码可以通过下属python脚本提取,后面尝试提取A,B,C参数时涉及到的一大堆的属性,

考虑到时间问题,放弃了治疗(也大致猜到了代码的逻辑)

(3.1)
注册码破解
核心注册码计算函数为 041B22D call    sub_402B8C,
核心算法步骤是,对加密后的输入注册码进行两次异或xor操作,根据可逆性,直接提取异或矩阵就完事了。

fnGetRegSnToVerify = 4019A2 Hi_fnGetRegSnToVerify
fnCalcUserInputRegSnAfterEnc = 4019C7 Hi_fnCalcUserInputRegSnAfterEnc

(3.1.1)
测试输入 
1234567890ABCDEF1234567890ABCDEF

(3.1.2)
由前面解密字符串函数对应的解密函数(参考前面分析)加密得到
testEncryptedSerial_in
00CA84B8  0B 97 00 33 BE 94 1B 5B 16 2B 16 7F DD B8 14 76  
00CA84C8  0B 97 00 33 BE 94 1B 5B 16 2B 16 7F DD B8 14 76  
(3.1.3)异或矩阵1
xor0 = [0xF5, 0x91, 0x23, 0x5E, 0x8D, 0xB0, 0x87, 0xE2, 0xAE, 0xEE, 0xDE, 0x93, 0x88, 0xF2, 0xAC, 0xA3,
  0x4F, 0x9F, 0xB7, 0x61, 0x10, 0x23, 0xFB, 0x30, 0x19, 0x69, 0xB8, 0xAD, 0xCE, 0x52, 0x00, 0x6C]
(3.1.4)异或矩阵2
xor1 = [0x1A, 0xAB, 0xD4, 0x70, 0xAE, 0x1A, 0x31, 0xD7, 0x4E, 0x7F, 0x02, 0x27, 0xDA, 0x3A, 0xD3, 0xC0,
  0xC7, 0xBF, 0xA0, 0xE2, 0xD7, 0x92, 0xF0, 0xE5, 0xF8, 0x64, 0xD3, 0x04, 0x96, 0xAD, 0x17, 0x41]

(3.1.5)
计算的注册码结果
testEncryptedSerial_out = [0xE4, 0xAD, 0xF7, 0x1D, 0x9D, 0x3E, 0xAD, 0x6E, 0xF6, 0xBA, 0xCA, 0xCB, 0x8F, 0x70, 0x6B, 0x15,
  0x83, 0xB7, 0x17, 0xB0, 0x79, 0x25, 0x10, 0x8E, 0xF7, 0x26, 0x7D, 0xD6, 0x85, 0x47, 0x03, 0x5B]
  
逆算注册码过程,4019A2 Hi_fnGetRegSnToVerify 函数得到校验的注册码
g_strRegSnToVerify=[0xA4, 0x47, 0x98, 0x0C, 0x9E, 0x40, 0xD7, 0xF6, 0xEB, 0x76, 0x6E, 0x6D, 0x7E, 0xA3, 0x3E, 0xEB,
  0xD5, 0x51, 0x30, 0x06, 0x7D, 0xC0, 0xFB, 0x6C, 0xC2, 0x7A, 0x43, 0xC5, 0xA4, 0xC9, 0xB1, 0xFD]
通过二次异或得到加密的注册码,简单换算pyth脚本如下
最后解密得到(还记得前面解密中文字符串的过程么,对了,也是直接修改OD的EIP,填充参数的输入内容,之后揭秘得到要输入的注册码)
022C8848  73 74 4B 35 43 4B 70 42 73 77 37 54 50 46 34 35  stK5CKpBsw7TPF45

一下是两个异或算子提取位置,和解密使用的代码片段(修改最少,解密都关联了一个一字节的因子,对注册码的是0x18,对其他中文字符串是其他)

(3.1.a)
异或矩阵1 Hi_xor1_unk_42D224 ,静态,直接提取

(3.1.b)
异或矩阵1  edi 指示的内存区

(3.1.c)
解密代码调用片段, dword_42D204 为输入的算法因子,两处引用,除了这里,就是前面的"0041B494 Hi_encrypt_sub_41B494"函数里
这也是Lua脚本的解密过程。直接修改OD EIP = 0041AF37,运行过程中跳过 memecpy关联的指令,
在调用进入0041AF74之前,间参数1和2分别修改为要解密的内容和大小,直接F8之后得到解密内容

附件为 Lua 5.33 及 反编译工具,和 和解密出来的修改前后的 脚本反编译
luadec.exe  ls_11_script.out
得到前面的lua脚本反编译结果

LuaDec533with_ls_11_script.7z. 





看雪安全 · 看雪众测
持续关注安全16年,专业为您服务!

快,关注这个公众号,一起涨姿势~
 
看雪学院 更多文章 浅谈基本块理论在程序自动跟踪中的应用 安卓源码+内核修改编译(修改内核调试标志绕过反调试) 这个年头,当个总统不容易啊...... 你来或不来,我就在这里 安卓Stagefright高危漏洞分析学习总结
猜您喜欢 尝试"分答" 敏捷破冰之旅——路虽遥,行则至 我是如何向别人提问题的? 网页如何生成二维码? Python之MVC