-
-
[原创]看雪 2016 CTF 第二题 Solution
-
发表于: 2016-11-6 10:16 5087
-
破解历程:
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的方式对所有引用点关联的字符内容进行一次统一解密(原因是解密代码可以重用,且解密部分代码片段耦合度高,堆栈相对稳定)
.text:004014B0 push offset dword_42D864 ;
.text:004014B0 ; 正在注册, 请稍候...
.text:004014B5 push eax ; void *
.text:004014B6 mov [ebp+lpString], eax
.text:004014B9 call _memcpy
.text:004014BE movzx eax, ds:byte_42D21C
.text:004014C5 add esp, 10h
.text:004014C8 mov ecx, Hi_dword_439850_withDebug ;
.text:004014C8 ; its.0Chww would be set 0 if isdebugging
.text:004014CE mov [ebp+var_10], esi
.text:004014D1 push eax
.text:004014D2 lea eax, [ebp+var_10]
.text:004014D5 push eax
.text:004014D6 lea eax, [ebp+lpString]
.text:004014D9 push eax
.text:004014DA call Hi_maybe_dectrypstr_sub_41B422
.text:004014DF test eax, eax
.text:004014E1 pop esi
.text:004014E2 jz short loc_4014F3
.text:004014E4 push 320h ; dwMilliseconds
.text:004014E9 push [ebp+lpString] ; lpString
.text:004014EC call Hi_SetTips_sub_4011C0
以下是直接修改OD EIP 执行片段解密出的大部分中文信息
0042D364 "Pediy CTF 2016 Crackme by SilentGamb"
0042D464 看雪CrackMe攻防大赛2016 **********************************两处引用
看雪CrackMe攻防大赛2016
输入注册码完成后, 按回车键进行注册
正在注册, 请稍候...
重试次数太多了,请重新运行程序
注册失败..., 请重新输入
0042D564 Serial
0042D964 输入注册码完成后, 按回车键进行注册
0042D864 正在注册, 请稍候... **********************************
0042D764 "注册失败..., 请重新输入"
0042D664 重试次数太多了,请重新运行程序
0042DE64 "fnGetRegSnToVerify"
0042DF64 "fnCalcUserInputRegSnAfterEnc"
0042D204 ************************* cbSize:0x400
42E064 userRegister
42E164 getRegSnAfterCal
(1.3)在解密函数 Hi_maybe_dectrypstr_sub_41B422 中
基本过程是:
解密信息体初始化 Hi_decObjInit,
解密过程 Hi_decObjDecrypt,
解密信息移动 Hi_decObjCopyOut,
解密信息体析构 Hi_decObjDtor
而在 Hi_decObjInit 函数交叉引用分析中,确定了加密函数 0041B3B0 sub_41B3B0,其对 输入的注册码进行加密
.text:0041B422 Hi_maybe_dectrypstr_sub_41B422 proc near
.text:0041B422
.text:0041B422 var_AC= byte ptr -0ACh
.text:0041B422 var_C= dword ptr -0Ch
.text:0041B422 var_4= dword ptr -4
.text:0041B422 P1_rStr= dword ptr 8
.text:0041B422 P2_rSize= dword ptr 0Ch
.text:0041B422 P3_rTor= dword ptr 10h
.text:0041B422
.text:0041B422 mov eax, offset loc_42C727
.text:0041B427 call __EH_prolog
.text:0041B42C sub esp, 0A0h
.text:0041B432 push esi
.text:0041B433 push edi
.text:0041B434 lea ecx, [ebp+var_AC]
.text:0041B43A call Hi_decObjInit
.text:0041B43F mov edi, [ebp+P1_rStr]
.text:0041B442 mov esi, [ebp+P2_rSize]
.text:0041B445 and [ebp+var_4], 0
.text:0041B449 lea ecx, [ebp+var_AC]
.text:0041B44F push dword ptr [edi] ; P3_str
.text:0041B451 push dword ptr [esi] ; P2_size
.text:0041B453 push [ebp+P3_rTor] ; P1_tor
.text:0041B456 call Hi_decObjDecrypt
.text:0041B45B push dword ptr [edi] ; void *
.text:0041B45D lea ecx, [ebp+var_AC]
.text:0041B463 push dword ptr [esi] ; int
.text:0041B465 call Hi_decObjCopyOut
.text:0041B46A xor ecx, ecx
.text:0041B46C cmp eax, [esi]
.text:0041B46E setz cl
.text:0041B471 or [ebp+var_4], 0FFFFFFFFh
.text:0041B475 mov esi, ecx
.text:0041B477 lea ecx, [ebp+var_AC]
.text:0041B47D call Hi_decObjDtor
.text:0041B482 mov ecx, [ebp+var_C]
.text:0041B485 mov eax, esi
.text:0041B487 pop edi
.text:0041B488 pop esi
.text:0041B489 mov large fs:0, ecx
.text:0041B490 leave
.text:0041B491 retn 0Ch
.text:0041B491 Hi_maybe_dectrypstr_sub_41B42
(1.4) 从(1.3)确认的 加密函数 sub_41B3B0 溯源 到 Hi_encrypt_sub_41B494,其上下文是获取输入的注册码,长度必须为 0x10的倍数,
且由函数 Hi_CheckAZaz09_sub_4018FB 检测保证必须是字母和数字
.text:0040184B call ds:GetWindowTextA
.text:00401851 push dword ptr [esi] ; char *
.text:00401853 call _strlen
.text:00401858 test eax, eax
.text:0040185A pop ecx
.text:0040185B jz short loc_401884
.text:0040185D mov ecx, eax
.text:0040185F and ecx, 0Fh
.text:00401862 test cl, cl
.text:00401864 jnz short loc_401884
.text:00401866 push eax
.text:00401867 push dword ptr [esi]
.text:00401869 call Hi_CheckAZaz09_sub_4018FB
.text:0040186E pop ecx
.text:0040186F test eax, eax
.text:00401871 pop ecx
.text:00401872 jz short loc_401884
.text:00401874 mov ecx, Hi_dword_439850_withDebug ;
.text:00401874 ; its.0Chww would be set 0 if isdebugging
.text:0040187A push edi
.text:0040187B push esi
.text:0040187C call Hi_encrypt_sub_41B494
(1.5)紧接(1.4)继续溯源,基本验证过程就在 消息响应函数 sub_401482 中,
其先通过 00401498 call Hi_getEncSerail_sub_4017EA 获取加密后的序列号
提示正在注册 004014EC call Hi_SetTips_sub_4011C0 ”正在注册, 请稍候..."
然后进入主要注册检验环节
.text:00401508 lea eax, [ebp+P2_rEncSerialSize]
.text:0040150B push eax ; P2_rEncSerialSize
.text:0040150C lea eax, [ebp+P1_rEncSerail]
.text:0040150F push eax ; P1_rEncSerial
.text:00401510 call sub_41AEF1
(2.1)
在(1.5)进入主要注册校验环节 sub_41AEF1 后,解密出改版的Lua脚本并执行,
一开始并为甄别确认 041AF84 call luaL_loadbuffer 函数,蛮力分析后发现其自成一套读取使用解密出的字节内容的系统(后来确认为Lua的ZIO)
.text:0041AF5F movzx eax, byte ptr ds:dword_42D204
.text:0041AF66 add esp, 0Ch
.text:0041AF69 mov ecx, esi
.text:0041AF6B push eax
.text:0041AF6C lea eax, [ebp+var_8]
.text:0041AF6F push eax
.text:0041AF70 lea eax, [ebp+lpMem]
.text:0041AF73 push eax
.text:0041AF74 call Hi_maybe_dectrypstr_sub_41B422
.text:0041AF79 push ebx
.text:0041AF7A push ebx
.text:0041AF7B push [ebp+var_8]
.text:0041AF7E push [ebp+lpMem]
.text:0041AF81 push dword ptr [esi+10h]
.text:0041AF84 call luaL_loadbuffer
(2.2)Lua的确认过程
这个过程开了挂,毕竟第一阶段的猜测只猜到了开始,对部分行数都使用了Hi_script_run_xxx的命名,也留意到了以下字符信息,
一开始百度了部分关键字符没有得到很直观的开发代码关联(一开始也没上谷歌,这个还是靠谱些,以前找开源代码特征每每都是它帮上大忙)
不小心瞄了一眼群上有些聪明的大侠纷纷给出了Lua的特征;恰好电脑上有自己之前搞分析
的Lua源码,也就上vs2010和UltraEdit遍历搜索了一下,很不幸命中了。
.data:00435204 aTable db 'table',0 ; DATA XREF: .rdata:0042E5B8
.data:00435204 ; .rdata:0042ED04
.data:0043520A align 4
.data:0043520C aCoroutine db 'coroutine',0 ; DATA XREF: .rdata:0042E5B0
.data:00435216 align 4
.data:00435218 aPackage db 'package',0 ; DATA XREF: .rdata:0042E5A8
.data:00435220 aSetvbuf db 'setvbuf',0 ; DATA XREF: .rdata:0042E6B8
.data:00435228 aSeek db 'seek',0
(2.3)对着手头上的Lua源码进行函数的甄别,从lua.c的 main 到 pmain ,关键的 Lua_status 需要初始化,
在第一阶段猜定为自成一套的buf读写机制时,和其他的笼统分析中,都注意到了[xxx_reg + 10] 的输入参数,xxx_reg是一个固定信息体或对象的thisPtr指针;
这个信息体的最初由前面跟踪对注册码输入的合法性检验函数 004018FB Hi_CheckAZaz09_sub_4018FB 中引出 ,在这个函数中
00401934 call Hi_BinToHexChars{
...
00401985 call Hi_BinToHexChar{
...
.text:004018D6 call ds:IsDebuggerPresent
.text:004018DC test eax, eax
.text:004018DE jz short loc_4018F3
.text:004018E0 mov eax, Hi_dword_439850_withDebug ;
.text:004018E0 ; its.0Chww would be set 0 if isdebugging
.text:004018E5 mov Hi_set1_by_IsDebuggerPresent, 1
.text:004018EF and dword ptr [eax+0Ch], 0
...
}
...
}
的调用对二进制转成十六进制字符串信息,上面有调试检测,不过后续怎么影响,直接在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关联是信息确定)
.text:0041ADC3 call luaL_newstate ;
.text:0041ADC3 ; #define lua_open() luaL_newstate()
.text:0041ADC8 cmp eax, ebx
.text:0041ADCA mov [esi+10h], eax
.text:0041ADCD jz loc_41AEA0
.text:0041ADD3 push edi
.text:0041ADD4 push eax
.text:0041ADD5 call 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做函数甄别。
.text:0040C06F luaL_openlibs proc near ; CODE XREF: sub_41AD8F+46
.text:0040C06F
.text:0040C06F P1_lua_State = dword ptr 4
.text:0040C06F
.text:0040C06F mov eax, ds:off_42E5A4
.text:0040C074 push esi
.text:0040C075 mov esi, offset loadedlibs
关键的关联信息表
.rdata:0042E5A0 loadedlibs dd offset a_g ; DATA XREF: luaL_openlibs+6
.rdata:0042E5A0 ; "_G"
.rdata:0042E5A4 off_42E5A4 dd offset luaopen_base ; DATA XREF: luaL_openlibs
.rdata:0042E5A8 dd offset aPackage ; "package"
.rdata:0042E5AC dd offset luaopen_package
.rdata:0042E5B0 dd offset aCoroutine ; "coroutine"
.rdata:0042E5B4 dd offset sub_408209
.rdata:0042E5B8 dd offset aTable ; "table"
.rdata:0042E5BC dd offset luaopen_table
.rdata:0042E5C0 dd offset aIo ; "io"
.rdata:0042E5C4 dd offset luaopen_io
.rdata:0042E5C8 dd offset aOs ; "os"
.rdata:0042E5CC dd offset luaopen_os
.rdata:0042E5D0 dd offset aString ; "string"
.rdata:0042E5D4 dd offset luaopen_string
.rdata:0042E5D8 dd offset aMath ; "math"
.rdata:0042E5DC dd offset luaopen_math
.rdata:0042E5E0 dd offset aUtf8 ; "utf8"
.rdata:0042E5E4 dd offset luaopen_utf8
.rdata:0042E5E8 dd offset aDebug ; "debug"
.rdata:0042E5EC dd offset luaopen_debug
.rdata:0042E5F0 dd 0
.rdata:0042E5F4 dd 0
(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 脚本反编译
.text:0041AF51 push [ebp+var_8] ; size_t
.text:0041AF54 push offset dword_42DA64
.text:0041AF59 push eax ; void *
.text:0041AF5A call _memcpy
.text:0041AF5F movzx eax, byte ptr ds:dword_42D204
.text:0041AF66 add esp, 0Ch
.text:0041AF69 mov ecx, esi
.text:0041AF6B push eax
.text:0041AF6C lea eax, [ebp+var_8]
.text:0041AF6F push eax
.text:0041AF70 lea eax, [ebp+lpMem]
.text:0041AF73 push eax
.text:0041AF74 call Hi_maybe_dectrypstr_sub_41B422
.text:0041AF79 push ebx
.text:0041AF7A push ebx
.text:0041AF7B push [ebp+var_8]
.text:0041AF7E push [ebp+lpMem]
.text:0041AF81 push dword ptr [esi+10h]
.text:0041AF84 call luaL_loadbuff
上述代码片段解密出 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脚本
------- ------- ------- ------- ------- ------- -------
-- Decompiled using luadec 2.2 rev: for Lua 5.3 from https://github.com/viruscamp/luadec
-- Command line: enc.out
-- params : ...
-- function num : 0 , upvalues : _ENV
g_strRegSn = " "
g_strRegSnToVerify = ""
userRegister = function(strRegSnIn)
-- function num : 0_0 , upvalues : _ENV
local iRc = -1
g_strRegSn = strRegSnIn
g_strRegSnToVerify = fnGetRegSnToVerify()
g_strRegSn = fnCalcUserInputRegSnAfterEnc(g_strRegSn)
if g_strRegSn == g_strRegSnToVerify then
iRc = 1024
end
g_strRegSnToVerify = ""
return iRc
end
getRegSnAfterCalc = function(strRegSnIn)
-- function num : 0_1 , upvalues : _ENV
return g_strRegSn
end
------- ------- ------- ------- ------- ------- -------
(2.6.2)手工反编译结果,code指令的操作码可以通过下属python脚本提取,后面尝试提取A,B,C参数时涉及到的一大堆的属性,
考虑到时间问题,放弃了治疗(也大致猜到了代码的逻辑)
#define SIZE_C 9
#define SIZE_B 9
#define SIZE_Bx 18 (SIZE_C + SIZE_B)
#define SIZE_A 8
#define SIZE_Ax 26 (SIZE_C + SIZE_B + SIZE_A)
#define SIZE_OP 6
#define POS_OP 0
#define POS_A 6 (POS_OP + SIZE_OP)
#define POS_C 14 (POS_A + SIZE_A)
#define POS_B 23 (POS_C + SIZE_C)
#define POS_Bx 14 POS_C
#define POS_Ax 6 POS_A
ii = 0x80404008
def g(p,s,ebx = ii):
#p=6;s=8;
if s == 8:
return c_byte((ebx>>p)&(pow(2,s+1)-1) & 0xFF).value
else:
return (ebx>>p)&(pow(2,s+1)-1)
指令操作码 = g(0,6,指令码)
------- ------- ------- ------- ------- ------- -------
@TaskBegin.ls(P1,P2,...){
v1,v2,v3,v4,v5,v6,v7;
code(7):
08 40 40 80 /* A B C UpValue[A][RK(B)] := RK(C) */
08 C0 40 81 /* A B C UpValue[A][RK(B)] := RK(C) */
2C 00 00 00
08 00 00 82
2C 40 00 00
08 00 80 82
26 00 80 00
08,40,40,80
const(6):
L1 = "g_strRegSn"
L2 = " "
L3 = "g_strRegSnToVerify"
L4 = ""
L5 = "userRegister"
L6 = "getRegSnAfterCalc"
Upvalues(1):
(1,0)
Protos(2):
""[ln.5,ln.10](P1){
V1~V4
code(0x11):
41 00 00 00
08 00 80 80
86 C0 40 00
A4 80 80 00
08 80 00 81
86 00 41 00
C6 40 40 00
A4 80 00 01
08 80 80 80
86 40 40 00
C6 80 40 00
1F C0 00 01
1E 00 00 80
41 40 01 00
08 80 41 81
66 00 00 01
26 00 80 00
const(7):
L1 = Integer(-1)
L2 = "g_strRegSn"
L3 = "g_strRegSnToVerify"
L4 = "fnGetRegSnToVerify"
L5 = "fnCalcUserInputRegSnAfterEnc"
L6 = Integer(0x400)
L7 = ""
Upvalues(1):
(0,0)
Proto(0):
debug:
lineinfo.size = 0x11 * u4LineNo
n = 0x02 *:
{varname."strRegSnIn",sPC.0,ePC.0x11}
{varname."iRC",sPC.0x01,ePC.0x11}
upvalues.names cnt = 0x01:
{"_ENV"}
}
""[ln.0x13,ln.0x15](P1){
V1,V2;
code(3):
46 00 40 00
66 00 00 01
26 00 80 00
const(1):
L1 = "g_strRegSn"
Upvalues(1):
(0,0)
Proto(0):
debug:
lineinfo.size = 0x03 * u4LineNo>> 0x14,0x14,0x15
n = 0x01 *:
{varname."strRegSnIn",sPC.0,ePC.0x3}
upvalues.names cnt = 0x01:
{"_ENV"}
}
debug:
line = 0x07 * >> 01 02 10 05 15 13 15
n = 0: nothing
name cnt = 10
{_ENV}
}
------- ------- ------- ------- ------- ------- -------
(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脚本如下
def dexor(inm):
for i in xrange(0,0x10):
#print "0x{:02X}, ".format((inm[i] ^ xor0[i] ^ xor1[i])&0xFF),
print "{:02X} ".format((inm[i] ^ xor0[i] ^ xor1[i])&0xFF),
print
for i in xrange(0x10,0x20):
#print "0x{:02X}, ".format((inm[i] ^ xor0[i] ^ xor1[i])&0xFF),
print "{:02X} ".format((inm[i] ^ xor0[i] ^ xor1[i])&0xFF),
print
4B 7D 6F 22 BD EA 61 C3 0B E7 B2 D9 2C 6B 41 88
5D 71 27 85 BA 71 F0 B9 23 77 28 6C FC 36 A6 D0
tgtEndSerail=[0x4B, 0x7D, 0x6F, 0x22, 0xBD, 0xEA, 0x61, 0xC3, 0x0B, 0xE7, 0xB2, 0xD9, 0x2C, 0x6B, 0x41, 0x88,
0x5D, 0x71, 0x27, 0x85, 0xBA, 0x71, 0xF0, 0xB9, 0x23, 0x77, 0x28, 0x6C, 0xFC, 0x36, 0xA6, 0xD0,]
最后解密得到(还记得前面解密中文字符串的过程么,对了,也是直接修改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 ,静态,直接提取
.text:00402BF7 xor edx, edx
.text:00402BF9 cmp [esi+474h], edx
.text:00402BFF jnz short loc_402C1D
.text:00402C01 cmp ecx, edx
.text:00402C03 jle short loc_402C1D
.text:00402C05 mov edi, offset Hi_xor1_unk_42D224
.text:00402C0A mov eax, ebx
.text:00402C0C sub edi, ebx
.text:00402C0E
.text:00402C0E loc_402C0E: ; CODE XREF: sub_402B8C+8F
.text:00402C0E mov cl, [edi+eax]
.text:00402C11 xor [eax], cl
.text:00402C13 inc edx
.text:00402C14 inc eax
.text:00402C15 cmp edx, [esi+3CCh]
.text:00402C1B jl short loc_402C0E
(3.1.b)异或矩阵1 edi 指示的内存区
.text:00402C1D lea edi, [esi+3F4h]
.text:00402C23 push ebx
.text:00402C24 push edi
.text:00402C25 mov ecx, esi
.text:00402C27 call sub_402D7C
.text:00402C2C push [ebp+P1_inBuf] ; P2_outbuf
.text:00402C2F mov ecx, esi
.text:00402C31 push edi ; P1_3f4hbuf
.text:00402C32 call sub_40264E
(3.1.c)解密代码调用片段, dword_42D204 为输入的算法因子,两处引用,除了这里,就是前面的"0041B494 Hi_encrypt_sub_41B494"函数里
这也是Lua脚本的解密过程。直接修改OD EIP = 0041AF37,运行过程中跳过 memecpy关联的指令,
在调用进入0041AF74之前,间参数1和2分别修改为要解密的内容和大小,直接F8之后得到解密内容
text:0041AF37 mov eax, 400h
.text:0041AF3C push eax ; unsigned int
.text:0041AF3D mov [ebp+var_8], eax
.text:0041AF40 call ??2@YAPAXI@Z ; operator new(uint)
.text:0041AF45 cmp eax, ebx
.text:0041AF47 pop ecx
.text:0041AF48 mov [ebp+lpMem], eax
.text:0041AF4B jz loc_41B12E
//.text:0041AF51 push [ebp+var_8] ; size_t
//.text:0041AF54 push offset dword_42DA64
//.text:0041AF59 push eax ; void *
//.text:0041AF5A call _memcpy
.text:0041AF5F movzx eax, byte ptr ds:dword_42D204
//.text:0041AF66 add esp, 0Ch
.text:0041AF69 mov ecx, esi
.text:0041AF6B push eax
.text:0041AF6C lea eax, [ebp+var_8]
.text:0041AF6F push eax
.text:0041AF70 lea eax, [ebp+lpMem]
.text:0041AF73 push eax
.text:0041AF74 call Hi_maybe_dectrypstr_sub_41B422
附件为 Lua 5.33 及 反编译工具,和 和解密出来的修改前后的 脚本反编译
luadec.exe ls_11_script.out
得到前面的lua脚本反编译结果
LuaDec533with_ls_11_script.7z
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的方式对所有引用点关联的字符内容进行一次统一解密(原因是解密代码可以重用,且解密部分代码片段耦合度高,堆栈相对稳定)
.text:004014B0 push offset dword_42D864 ;
.text:004014B0 ; 正在注册, 请稍候...
.text:004014B5 push eax ; void *
.text:004014B6 mov [ebp+lpString], eax
.text:004014B9 call _memcpy
.text:004014BE movzx eax, ds:byte_42D21C
.text:004014C5 add esp, 10h
.text:004014C8 mov ecx, Hi_dword_439850_withDebug ;
.text:004014C8 ; its.0Chww would be set 0 if isdebugging
.text:004014CE mov [ebp+var_10], esi
.text:004014D1 push eax
.text:004014D2 lea eax, [ebp+var_10]
.text:004014D5 push eax
.text:004014D6 lea eax, [ebp+lpString]
.text:004014D9 push eax
.text:004014DA call Hi_maybe_dectrypstr_sub_41B422
.text:004014DF test eax, eax
.text:004014E1 pop esi
.text:004014E2 jz short loc_4014F3
.text:004014E4 push 320h ; dwMilliseconds
.text:004014E9 push [ebp+lpString] ; lpString
.text:004014EC call Hi_SetTips_sub_4011C0
以下是直接修改OD EIP 执行片段解密出的大部分中文信息
0042D364 "Pediy CTF 2016 Crackme by SilentGamb"
0042D464 看雪CrackMe攻防大赛2016 **********************************两处引用
看雪CrackMe攻防大赛2016
输入注册码完成后, 按回车键进行注册
正在注册, 请稍候...
重试次数太多了,请重新运行程序
注册失败..., 请重新输入
0042D564 Serial
0042D964 输入注册码完成后, 按回车键进行注册
0042D864 正在注册, 请稍候... **********************************
0042D764 "注册失败..., 请重新输入"
0042D664 重试次数太多了,请重新运行程序
0042DE64 "fnGetRegSnToVerify"
0042DF64 "fnCalcUserInputRegSnAfterEnc"
0042D204 ************************* cbSize:0x400
42E064 userRegister
42E164 getRegSnAfterCal
(1.3)在解密函数 Hi_maybe_dectrypstr_sub_41B422 中
基本过程是:
解密信息体初始化 Hi_decObjInit,
解密过程 Hi_decObjDecrypt,
解密信息移动 Hi_decObjCopyOut,
解密信息体析构 Hi_decObjDtor
而在 Hi_decObjInit 函数交叉引用分析中,确定了加密函数 0041B3B0 sub_41B3B0,其对 输入的注册码进行加密
.text:0041B422 Hi_maybe_dectrypstr_sub_41B422 proc near
.text:0041B422
.text:0041B422 var_AC= byte ptr -0ACh
.text:0041B422 var_C= dword ptr -0Ch
.text:0041B422 var_4= dword ptr -4
.text:0041B422 P1_rStr= dword ptr 8
.text:0041B422 P2_rSize= dword ptr 0Ch
.text:0041B422 P3_rTor= dword ptr 10h
.text:0041B422
.text:0041B422 mov eax, offset loc_42C727
.text:0041B427 call __EH_prolog
.text:0041B42C sub esp, 0A0h
.text:0041B432 push esi
.text:0041B433 push edi
.text:0041B434 lea ecx, [ebp+var_AC]
.text:0041B43A call Hi_decObjInit
.text:0041B43F mov edi, [ebp+P1_rStr]
.text:0041B442 mov esi, [ebp+P2_rSize]
.text:0041B445 and [ebp+var_4], 0
.text:0041B449 lea ecx, [ebp+var_AC]
.text:0041B44F push dword ptr [edi] ; P3_str
.text:0041B451 push dword ptr [esi] ; P2_size
.text:0041B453 push [ebp+P3_rTor] ; P1_tor
.text:0041B456 call Hi_decObjDecrypt
.text:0041B45B push dword ptr [edi] ; void *
.text:0041B45D lea ecx, [ebp+var_AC]
.text:0041B463 push dword ptr [esi] ; int
.text:0041B465 call Hi_decObjCopyOut
.text:0041B46A xor ecx, ecx
.text:0041B46C cmp eax, [esi]
.text:0041B46E setz cl
.text:0041B471 or [ebp+var_4], 0FFFFFFFFh
.text:0041B475 mov esi, ecx
.text:0041B477 lea ecx, [ebp+var_AC]
.text:0041B47D call Hi_decObjDtor
.text:0041B482 mov ecx, [ebp+var_C]
.text:0041B485 mov eax, esi
.text:0041B487 pop edi
.text:0041B488 pop esi
.text:0041B489 mov large fs:0, ecx
.text:0041B490 leave
.text:0041B491 retn 0Ch
.text:0041B491 Hi_maybe_dectrypstr_sub_41B42
(1.4) 从(1.3)确认的 加密函数 sub_41B3B0 溯源 到 Hi_encrypt_sub_41B494,其上下文是获取输入的注册码,长度必须为 0x10的倍数,
且由函数 Hi_CheckAZaz09_sub_4018FB 检测保证必须是字母和数字
.text:0040184B call ds:GetWindowTextA
.text:00401851 push dword ptr [esi] ; char *
.text:00401853 call _strlen
.text:00401858 test eax, eax
.text:0040185A pop ecx
.text:0040185B jz short loc_401884
.text:0040185D mov ecx, eax
.text:0040185F and ecx, 0Fh
.text:00401862 test cl, cl
.text:00401864 jnz short loc_401884
.text:00401866 push eax
.text:00401867 push dword ptr [esi]
.text:00401869 call Hi_CheckAZaz09_sub_4018FB
.text:0040186E pop ecx
.text:0040186F test eax, eax
.text:00401871 pop ecx
.text:00401872 jz short loc_401884
.text:00401874 mov ecx, Hi_dword_439850_withDebug ;
.text:00401874 ; its.0Chww would be set 0 if isdebugging
.text:0040187A push edi
.text:0040187B push esi
.text:0040187C call Hi_encrypt_sub_41B494
(1.5)紧接(1.4)继续溯源,基本验证过程就在 消息响应函数 sub_401482 中,
其先通过 00401498 call Hi_getEncSerail_sub_4017EA 获取加密后的序列号
提示正在注册 004014EC call Hi_SetTips_sub_4011C0 ”正在注册, 请稍候..."
然后进入主要注册检验环节
.text:00401508 lea eax, [ebp+P2_rEncSerialSize]
.text:0040150B push eax ; P2_rEncSerialSize
.text:0040150C lea eax, [ebp+P1_rEncSerail]
.text:0040150F push eax ; P1_rEncSerial
.text:00401510 call sub_41AEF1
(2.1)
在(1.5)进入主要注册校验环节 sub_41AEF1 后,解密出改版的Lua脚本并执行,
一开始并为甄别确认 041AF84 call luaL_loadbuffer 函数,蛮力分析后发现其自成一套读取使用解密出的字节内容的系统(后来确认为Lua的ZIO)
.text:0041AF5F movzx eax, byte ptr ds:dword_42D204
.text:0041AF66 add esp, 0Ch
.text:0041AF69 mov ecx, esi
.text:0041AF6B push eax
.text:0041AF6C lea eax, [ebp+var_8]
.text:0041AF6F push eax
.text:0041AF70 lea eax, [ebp+lpMem]
.text:0041AF73 push eax
.text:0041AF74 call Hi_maybe_dectrypstr_sub_41B422
.text:0041AF79 push ebx
.text:0041AF7A push ebx
.text:0041AF7B push [ebp+var_8]
.text:0041AF7E push [ebp+lpMem]
.text:0041AF81 push dword ptr [esi+10h]
.text:0041AF84 call luaL_loadbuffer
(2.2)Lua的确认过程
这个过程开了挂,毕竟第一阶段的猜测只猜到了开始,对部分行数都使用了Hi_script_run_xxx的命名,也留意到了以下字符信息,
一开始百度了部分关键字符没有得到很直观的开发代码关联(一开始也没上谷歌,这个还是靠谱些,以前找开源代码特征每每都是它帮上大忙)
不小心瞄了一眼群上有些聪明的大侠纷纷给出了Lua的特征;恰好电脑上有自己之前搞分析
的Lua源码,也就上vs2010和UltraEdit遍历搜索了一下,很不幸命中了。
.data:00435204 aTable db 'table',0 ; DATA XREF: .rdata:0042E5B8
.data:00435204 ; .rdata:0042ED04
.data:0043520A align 4
.data:0043520C aCoroutine db 'coroutine',0 ; DATA XREF: .rdata:0042E5B0
.data:00435216 align 4
.data:00435218 aPackage db 'package',0 ; DATA XREF: .rdata:0042E5A8
.data:00435220 aSetvbuf db 'setvbuf',0 ; DATA XREF: .rdata:0042E6B8
.data:00435228 aSeek db 'seek',0
(2.3)对着手头上的Lua源码进行函数的甄别,从lua.c的 main 到 pmain ,关键的 Lua_status 需要初始化,
在第一阶段猜定为自成一套的buf读写机制时,和其他的笼统分析中,都注意到了[xxx_reg + 10] 的输入参数,xxx_reg是一个固定信息体或对象的thisPtr指针;
这个信息体的最初由前面跟踪对注册码输入的合法性检验函数 004018FB Hi_CheckAZaz09_sub_4018FB 中引出 ,在这个函数中
00401934 call Hi_BinToHexChars{
...
00401985 call Hi_BinToHexChar{
...
.text:004018D6 call ds:IsDebuggerPresent
.text:004018DC test eax, eax
.text:004018DE jz short loc_4018F3
.text:004018E0 mov eax, Hi_dword_439850_withDebug ;
.text:004018E0 ; its.0Chww would be set 0 if isdebugging
.text:004018E5 mov Hi_set1_by_IsDebuggerPresent, 1
.text:004018EF and dword ptr [eax+0Ch], 0
...
}
...
}
的调用对二进制转成十六进制字符串信息,上面有调试检测,不过后续怎么影响,直接在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关联是信息确定)
.text:0041ADC3 call luaL_newstate ;
.text:0041ADC3 ; #define lua_open() luaL_newstate()
.text:0041ADC8 cmp eax, ebx
.text:0041ADCA mov [esi+10h], eax
.text:0041ADCD jz loc_41AEA0
.text:0041ADD3 push edi
.text:0041ADD4 push eax
.text:0041ADD5 call 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做函数甄别。
.text:0040C06F luaL_openlibs proc near ; CODE XREF: sub_41AD8F+46
.text:0040C06F
.text:0040C06F P1_lua_State = dword ptr 4
.text:0040C06F
.text:0040C06F mov eax, ds:off_42E5A4
.text:0040C074 push esi
.text:0040C075 mov esi, offset loadedlibs
关键的关联信息表
.rdata:0042E5A0 loadedlibs dd offset a_g ; DATA XREF: luaL_openlibs+6
.rdata:0042E5A0 ; "_G"
.rdata:0042E5A4 off_42E5A4 dd offset luaopen_base ; DATA XREF: luaL_openlibs
.rdata:0042E5A8 dd offset aPackage ; "package"
.rdata:0042E5AC dd offset luaopen_package
.rdata:0042E5B0 dd offset aCoroutine ; "coroutine"
.rdata:0042E5B4 dd offset sub_408209
.rdata:0042E5B8 dd offset aTable ; "table"
.rdata:0042E5BC dd offset luaopen_table
.rdata:0042E5C0 dd offset aIo ; "io"
.rdata:0042E5C4 dd offset luaopen_io
.rdata:0042E5C8 dd offset aOs ; "os"
.rdata:0042E5CC dd offset luaopen_os
.rdata:0042E5D0 dd offset aString ; "string"
.rdata:0042E5D4 dd offset luaopen_string
.rdata:0042E5D8 dd offset aMath ; "math"
.rdata:0042E5DC dd offset luaopen_math
.rdata:0042E5E0 dd offset aUtf8 ; "utf8"
.rdata:0042E5E4 dd offset luaopen_utf8
.rdata:0042E5E8 dd offset aDebug ; "debug"
.rdata:0042E5EC dd offset luaopen_debug
.rdata:0042E5F0 dd 0
.rdata:0042E5F4 dd 0
(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 脚本反编译
.text:0041AF51 push [ebp+var_8] ; size_t
.text:0041AF54 push offset dword_42DA64
.text:0041AF59 push eax ; void *
.text:0041AF5A call _memcpy
.text:0041AF5F movzx eax, byte ptr ds:dword_42D204
.text:0041AF66 add esp, 0Ch
.text:0041AF69 mov ecx, esi
.text:0041AF6B push eax
.text:0041AF6C lea eax, [ebp+var_8]
.text:0041AF6F push eax
.text:0041AF70 lea eax, [ebp+lpMem]
.text:0041AF73 push eax
.text:0041AF74 call Hi_maybe_dectrypstr_sub_41B422
.text:0041AF79 push ebx
.text:0041AF7A push ebx
.text:0041AF7B push [ebp+var_8]
.text:0041AF7E push [ebp+lpMem]
.text:0041AF81 push dword ptr [esi+10h]
.text:0041AF84 call luaL_loadbuff
上述代码片段解密出 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脚本
------- ------- ------- ------- ------- ------- -------
-- Decompiled using luadec 2.2 rev: for Lua 5.3 from https://github.com/viruscamp/luadec
-- Command line: enc.out
-- params : ...
-- function num : 0 , upvalues : _ENV
g_strRegSn = " "
g_strRegSnToVerify = ""
userRegister = function(strRegSnIn)
-- function num : 0_0 , upvalues : _ENV
local iRc = -1
g_strRegSn = strRegSnIn
g_strRegSnToVerify = fnGetRegSnToVerify()
g_strRegSn = fnCalcUserInputRegSnAfterEnc(g_strRegSn)
if g_strRegSn == g_strRegSnToVerify then
iRc = 1024
end
g_strRegSnToVerify = ""
return iRc
end
getRegSnAfterCalc = function(strRegSnIn)
-- function num : 0_1 , upvalues : _ENV
return g_strRegSn
end
------- ------- ------- ------- ------- ------- -------
(2.6.2)手工反编译结果,code指令的操作码可以通过下属python脚本提取,后面尝试提取A,B,C参数时涉及到的一大堆的属性,
考虑到时间问题,放弃了治疗(也大致猜到了代码的逻辑)
#define SIZE_C 9
#define SIZE_B 9
#define SIZE_Bx 18 (SIZE_C + SIZE_B)
#define SIZE_A 8
#define SIZE_Ax 26 (SIZE_C + SIZE_B + SIZE_A)
#define SIZE_OP 6
#define POS_OP 0
#define POS_A 6 (POS_OP + SIZE_OP)
#define POS_C 14 (POS_A + SIZE_A)
#define POS_B 23 (POS_C + SIZE_C)
#define POS_Bx 14 POS_C
#define POS_Ax 6 POS_A
ii = 0x80404008
def g(p,s,ebx = ii):
#p=6;s=8;
if s == 8:
return c_byte((ebx>>p)&(pow(2,s+1)-1) & 0xFF).value
else:
return (ebx>>p)&(pow(2,s+1)-1)
指令操作码 = g(0,6,指令码)
------- ------- ------- ------- ------- ------- -------
@TaskBegin.ls(P1,P2,...){
v1,v2,v3,v4,v5,v6,v7;
code(7):
08 40 40 80 /* A B C UpValue[A][RK(B)] := RK(C) */
08 C0 40 81 /* A B C UpValue[A][RK(B)] := RK(C) */
2C 00 00 00
08 00 00 82
2C 40 00 00
08 00 80 82
26 00 80 00
08,40,40,80
const(6):
L1 = "g_strRegSn"
L2 = " "
L3 = "g_strRegSnToVerify"
L4 = ""
L5 = "userRegister"
L6 = "getRegSnAfterCalc"
Upvalues(1):
(1,0)
Protos(2):
""[ln.5,ln.10](P1){
V1~V4
code(0x11):
41 00 00 00
08 00 80 80
86 C0 40 00
A4 80 80 00
08 80 00 81
86 00 41 00
C6 40 40 00
A4 80 00 01
08 80 80 80
86 40 40 00
C6 80 40 00
1F C0 00 01
1E 00 00 80
41 40 01 00
08 80 41 81
66 00 00 01
26 00 80 00
const(7):
L1 = Integer(-1)
L2 = "g_strRegSn"
L3 = "g_strRegSnToVerify"
L4 = "fnGetRegSnToVerify"
L5 = "fnCalcUserInputRegSnAfterEnc"
L6 = Integer(0x400)
L7 = ""
Upvalues(1):
(0,0)
Proto(0):
debug:
lineinfo.size = 0x11 * u4LineNo
n = 0x02 *:
{varname."strRegSnIn",sPC.0,ePC.0x11}
{varname."iRC",sPC.0x01,ePC.0x11}
upvalues.names cnt = 0x01:
{"_ENV"}
}
""[ln.0x13,ln.0x15](P1){
V1,V2;
code(3):
46 00 40 00
66 00 00 01
26 00 80 00
const(1):
L1 = "g_strRegSn"
Upvalues(1):
(0,0)
Proto(0):
debug:
lineinfo.size = 0x03 * u4LineNo>> 0x14,0x14,0x15
n = 0x01 *:
{varname."strRegSnIn",sPC.0,ePC.0x3}
upvalues.names cnt = 0x01:
{"_ENV"}
}
debug:
line = 0x07 * >> 01 02 10 05 15 13 15
n = 0: nothing
name cnt = 10
{_ENV}
}
------- ------- ------- ------- ------- ------- -------
(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脚本如下
def dexor(inm):
for i in xrange(0,0x10):
#print "0x{:02X}, ".format((inm[i] ^ xor0[i] ^ xor1[i])&0xFF),
print "{:02X} ".format((inm[i] ^ xor0[i] ^ xor1[i])&0xFF),
for i in xrange(0x10,0x20):
#print "0x{:02X}, ".format((inm[i] ^ xor0[i] ^ xor1[i])&0xFF),
print "{:02X} ".format((inm[i] ^ xor0[i] ^ xor1[i])&0xFF),
4B 7D 6F 22 BD EA 61 C3 0B E7 B2 D9 2C 6B 41 88
5D 71 27 85 BA 71 F0 B9 23 77 28 6C FC 36 A6 D0
tgtEndSerail=[0x4B, 0x7D, 0x6F, 0x22, 0xBD, 0xEA, 0x61, 0xC3, 0x0B, 0xE7, 0xB2, 0xD9, 0x2C, 0x6B, 0x41, 0x88,
0x5D, 0x71, 0x27, 0x85, 0xBA, 0x71, 0xF0, 0xB9, 0x23, 0x77, 0x28, 0x6C, 0xFC, 0x36, 0xA6, 0xD0,]
最后解密得到(还记得前面解密中文字符串的过程么,对了,也是直接修改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 ,静态,直接提取
.text:00402BF7 xor edx, edx
.text:00402BF9 cmp [esi+474h], edx
.text:00402BFF jnz short loc_402C1D
.text:00402C01 cmp ecx, edx
.text:00402C03 jle short loc_402C1D
.text:00402C05 mov edi, offset Hi_xor1_unk_42D224
.text:00402C0A mov eax, ebx
.text:00402C0C sub edi, ebx
.text:00402C0E
.text:00402C0E loc_402C0E: ; CODE XREF: sub_402B8C+8F
.text:00402C0E mov cl, [edi+eax]
.text:00402C11 xor [eax], cl
.text:00402C13 inc edx
.text:00402C14 inc eax
.text:00402C15 cmp edx, [esi+3CCh]
.text:00402C1B jl short loc_402C0E
(3.1.b)异或矩阵1 edi 指示的内存区
.text:00402C1D lea edi, [esi+3F4h]
.text:00402C23 push ebx
.text:00402C24 push edi
.text:00402C25 mov ecx, esi
.text:00402C27 call sub_402D7C
.text:00402C2C push [ebp+P1_inBuf] ; P2_outbuf
.text:00402C2F mov ecx, esi
.text:00402C31 push edi ; P1_3f4hbuf
.text:00402C32 call sub_40264E
(3.1.c)解密代码调用片段, dword_42D204 为输入的算法因子,两处引用,除了这里,就是前面的"0041B494 Hi_encrypt_sub_41B494"函数里
这也是Lua脚本的解密过程。直接修改OD EIP = 0041AF37,运行过程中跳过 memecpy关联的指令,
在调用进入0041AF74之前,间参数1和2分别修改为要解密的内容和大小,直接F8之后得到解密内容
text:0041AF37 mov eax, 400h
.text:0041AF3C push eax ; unsigned int
.text:0041AF3D mov [ebp+var_8], eax
.text:0041AF40 call ??2@YAPAXI@Z ; operator new(uint)
.text:0041AF45 cmp eax, ebx
.text:0041AF47 pop ecx
.text:0041AF48 mov [ebp+lpMem], eax
.text:0041AF4B jz loc_41B12E
//.text:0041AF51 push [ebp+var_8] ; size_t
//.text:0041AF54 push offset dword_42DA64
//.text:0041AF59 push eax ; void *
//.text:0041AF5A call _memcpy
.text:0041AF5F movzx eax, byte ptr ds:dword_42D204
//.text:0041AF66 add esp, 0Ch
.text:0041AF69 mov ecx, esi
.text:0041AF6B push eax
.text:0041AF6C lea eax, [ebp+var_8]
.text:0041AF6F push eax
.text:0041AF70 lea eax, [ebp+lpMem]
.text:0041AF73 push eax
.text:0041AF74 call Hi_maybe_dectrypstr_sub_41B422
附件为 Lua 5.33 及 反编译工具,和 和解密出来的修改前后的 脚本反编译
luadec.exe ls_11_script.out
得到前面的lua脚本反编译结果
LuaDec533with_ls_11_script.7z
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
他的文章
- [原创] KCTF 2022 Win. 第六题 约束与伪随机 6745
- [原创] KCTF 2021 Win. 第二题 排排坐 21175
- [原创] KCTF 2021 Win. 第一题 算力与攻击模式 4118
- 鸿蒙通识 26032
- [原创] KCTF 2021 Spr. 第二题 未选择的路 9250
看原图
赞赏
雪币:
留言: