这道题已经完全脱壳了,只剩下算法的逆运算了。因为第二天还要搬砖,就没有熬夜搞了 T_T
脱壳
壳分析
拿到题目后发现在不调试的情况下让它自己正常运算都要好几秒,再一看代码基本就能想到是解密几行汇编再执行,于是找到解密代码的规律:
这里接收并保存完输入的用户名和密码后便开始解密代码
解密完后,通过call eax
的方式跳转到解密后的代码
用x32dbg
调试看看解密后的代码,03754814
处的指令很像函数头,准备再看看下一次解密的代码
此时在后面的代码里没有发现call eax
,而是一堆有问题的指令
经过调试后发现call eax
的那部分代码也是动态解密出来的(由于截图分了几次调试截,地址不一样)
在调试时还发现,在解密新代码的同时会把上一步解密出的所有代码,包括解密代码在内,全部抹掉(机智)
再观察第二次解密出的代码,更加明显了,popfd pushfd
和其他寄存器出栈压栈的操作就是在恢复和保存环境(后面解密出的代码也是这样的操作,就不截图了),另外还有个发现,执行真正的逻辑代码是在解密出call eax
那段代码后
总结壳规律
- 通过
call eax
的方式到下一次解密操作
- 每次解密
call eax
那段代码都是执行完jne xxx
的那段代码
- 解密完
call eax
那段代码后,跳到恢复寄存器环境执行真正的逻辑代码那里,而这段逻辑代码在每次进入call eax
就可以看到,而且除了第一次都很有规律,伴随着popfd
开始pushfd
结束
- 通过写
x32dbg
脚本跑了一遍,发现这样的解密操作一共执行了2880
次(可怕),而2880
次之后还有验证的逻辑代码要执行,验证逻辑代码执行完后就是判断序列号运算是否成功、输出结果的操作了(没有加密完所有的验证逻辑代码,应该是因为再多加密几次,运算时间就太长了,会违规)
脱壳脚本
用的是x32dbg
需要注意的地方:
- 程序有反调试,会导致运算结果不正确,在脚本第一次暂停时去
x32dbg
菜单调试->高级->隐藏调试器
- 这个脚本只是获取到了前
2880次
解密的指令,因为后面的指令不需要解密,直接抠出来就完了
- 脚本把指令的16进制机器码和汇编打印在了日志窗口,复制出来,通过notepad++的替换功能用正则匹配将非
#
或>
开头的行删掉,操作一下就分别得到汇编代码和16进制机器码数据了
- 他每次解密后的代码格式不固定,有时候有1个
jne
有时候有多个,所以有时候需要多次寻找才能找到
- 反汇编的方式查找指令速度超级超级慢,所以用查找内存特征的方式定位指令,所以定位到之后要判断是否定位正确
- 只能下硬件断点,因为解密代码依赖于前面的代码数据,如果有软件断点,内存中读到的数据就是
CC
,解密后的数据就不对了
- 脚本要跑半个多小时,没错,2880次呢!
(附件x32dbg.txt
)
// 加载完程序后再加载脚本
// 按空格键开始
$num = 0x0
// MAX: 0xb40
$stop_num = 0xb40
bph 004014DF
// 这里暂停后,去设置隐藏调试器
// F9运行起来,输入用户名和序列号
// 输入完了,按空格开始获取指令
// 获取的信息在日志窗口
pause
bp printf
bphc 004014DF
// 寻找 call eax
find_calleax:
find cip,FFD0,0x200
cmp $result,0
je find_jne
$calleax = $result
// 这一次是意外情况,跳过下一步操作
cmp $num, 0x9dA
jb gogo_0
// 判断找到的 call eax 对不对
$dis_len = 0
while_next_0:
$dis_next_len = dis.len(cip + $dis_len)
cmp $dis_next_len, 0
je find_jne_go
$dis_len = $dis_len + $dis_next_len
cmp cip+$dis_len, $calleax
jb while_next_0
cmp cip+$dis_len, $calleax
je gogo_0
jmp find_jne
gogo_0:
// 下断点
bph $calleax
run
bphc $calleax
$num = $num + 1
log "call eax: {0}", $num
cmp $num, $stop_num
je opt_xxx
opt_back:
// 进入 call eax
StepInto
StepInto
StepOver
// pause
// 定位真正的代码位置
find cip,9D5F5E5A595B58,0x100
cmp $result,0
je find_jne
$start = $result + 0x7
find $start,5053515256579C,0x100
cmp $result,0
je find_jne
$stop = $result
$size = $stop - $start
// 打印真正代码的16进制机器码
log "# {mem;$size@$start}"
// 打印真正代码的汇编指令
log "> "
out_asm_log:
cmp $start, $stop
jge find_jne
log "> {$start} {disasm@$start}"
$start = $start + dis.len($start)
jmp out_asm_log
// pause
// 寻找 jne
find_jne:
// pause
cmp $num, 0xa40
je find_jne_go
cmp $num, 0x380
jg find_jne_0f85
find_jne_go:
find cip+2,75??,0x200
cmp $result,0
je find_calleax
$jnenext = $result
for_next_1:
$dis_len = 0
while_next_1:
$dis_next_len = dis.len(cip + $dis_len)
$dis_len = $dis_len + $dis_next_len
cmp cip+$dis_len, $jnenext
jb while_next_1
cmp cip+$dis_len, $jnenext
je gogo_1
find $jnenext+2,75??,0x200
cmp $result,0
je gogo_1
$jnenext = $result
jmp for_next_1
gogo_1:
bph $jnenext + 2
// pause
run
bphc $jnenext + 2
jmp find_calleax
// 另一种形式的 jne
find_jne_0f85:
find cip,0F85????????,0x200
cmp $result,0
je find_jne_go
$jnenext = $result
$dis_len = 0
while_next_2:
$dis_next_len = dis.len(cip + $dis_len)
cmp $dis_next_len, 0
je find_jne_go
$dis_len = $dis_len + $dis_next_len
cmp cip+$dis_len, $jnenext
jb while_next_2
cmp cip+$dis_len, $jnenext
je gogo_2
jmp find_jne_go
gogo_2:
$jnenext = $jnenext + 6
bph $jnenext
run
bphc $jnenext
jmp find_calleax
// 执行结束
opt_xxx:
msg "Get It!"
pause
jmp opt_back
指令修复
获取到的指令看起来很不舒服,因为有太多无用的地址定位指令和其他数据
再写个idapython
脚本,获取所有逻辑验证指令(附件code_dmp.py
)
# coding=utf-8
import os, sys
import idc
import idautils
import idaapi
def GetCode(start):
ea = start
asm_code = ''
hex_data = ''.decode('hex')
stop = idc.MaxEA()
# stop = 0x04ED0B11
while ea <= stop:
intr = idc.GetDisasm(ea)
# print(intr)
if 'call' in intr or \
'pop esi' in intr or \
'sub esi' in intr or \
'add esi' in intr :
ea = idc.NextHead(ea)
elif 'jmp' in intr:
jmp_len = idc.GetOpnd(ea, 0)
ea = idc.LocByName(jmp_len)
else:
asm_code += intr + '\n'
buf = idc.GetManyBytes(ea, ItemSize(ea))
hex_data += buf
# print(hex_data.encode('hex'))
print(hex(ea))
ea = idc.NextHead(ea)
file_handle =open('code_dmp.txt',mode='w')
file_handle.write(asm_code)
file_handle.close()
file_handle =open('code_dmp.bin',mode='wb')
file_handle.write(hex_data)
file_handle.close()
ea = idc.ScreenEA()
GetCode(ea)
合成PE文件
通过以上的操作就获取到了这2880次解密出的所有逻辑验证代码了,但是还不够完美,要做到脱壳后能独立运行
于是在这段指令前加上解密第一步的函数头
push ebp
mov ebp, esp
sub esp, 2Ch
把2880次解密完后,剩下的所有指令再手动抠出来,补到后面
现在验证代码已经全拿出来了(在附件code_dump.bin
中),而且这些代码都是地址无关的运算,唯一需要的地址是在esi
中保存,那就需要看看esi
中保存的是什么了
esi指向的数据表
通过调试发现,esi 05EF5C91
指向了一张表,esi+0x1B0
的位置是输入的序列号,esi-0x11
的位置存的是用户名
当前,我们还原的时候需要的是把用户名和序列号复制进去前的数据表,重新调试再通过一通硬件断点的操作找到了这张表,就最后一行保存序列号的地方不一样
函数结尾
调试发现结尾的地方是在比较esi+0x1B0
处和edi
处的用户名字符串,如果相等就提示成功(没有截调试的图,因为要调到2880次之后需要半小时)
其实edi
指向的就是esi-0x11
的位置,重写一下这段指令
两种合成方式
直接把解出来的16进制机器码粘贴到原PE的对应位置,但是发现esi
指向的数据是在第一次解密后才赋值的,这个地址是动态的,所以还需单独把esi
指向的数据表提前写进PE。另外后面验证完成后打印提示字符串的部分指令也要抠过来,所以我选择了第二种方式
自己写c++代码实现输入和输出功能,中间将16进制机器码当作ShellCode
来调用,只需要将这段ShellCode
的比较结果传回来就行,于是把比较结果ecx
保存在eax
当作函数返回值,补上函数结尾
生成新的PE
逻辑和原来的一样,只是提示的字符串不同
部分代码,全部代码见附件FixCrackMe.cpp
int main(void)
{
// 修改 shellcode 数据段为可读可写可执行
HANDLE hProc = GetCurrentProcess();
DWORD dwIdOld;
VirtualProtectEx(hProc,&shellcode, 35834, PAGE_EXECUTE_READWRITE, &dwIdOld);
// 获取输入
char username[32] = {0};
printf("Input user name: ");
scanf("%s", username);
strncpy((char*)esiData, username, 16);
char passwd[64] = {0};
printf("Input password: ");
scanf("%s", passwd);
int len = strlen(passwd);
HexStrToByte(passwd, esiData+0x11+0x1b0, len);
int (*pfun1)() = (int (*) (void))&shellcode;
// 给esi寄存器赋值,因为验证函数是从这个寄存器中获取数据地址
// esi是加密需要的数据表地址,esi+0x1b0是用户输入的序列号的16进制数据
// esi-0x11是用户输入的用户名字符串
__asm
{
mov esi, offset esiData+0x11
call pfun1
cmp eax, 0
jne OUTERR
call success
jmp END
OUTERR:
call error
};
END:
system("pause");
return 0;
}
效果完全一样(还原后的PE见附件FixCrackMe.cpp
)
算法分析
验证的开始将输入与esi
指向的数据表通过一定规则异或
验证的结尾再将esi
指向的数据表中的snail3896q3405%\0
字符串分别与特定下标的数进行异或,结果保存在esi+0x1B0
,如果算出的结果等于输入的字符串则验证成功
中间那一大堆操作就是在算下标。而这一大堆操作基本是:计算几个16元一次方程的结果,将结果重新存起来,作为下标在表里找到新的数据,再次带入几个16元一次方程中计算结果,这样的操作一共经过9次运算,得到下标。最后再到表里获得值,这个值作为下标执行2
中的操作
随便截个图感受下吧:
解题思路
因为没做出来这里只说思路,也许不对
- 按照算法分析部分
2
的操作逆推出密钥异或表里几个数的下标,作为几个16元一次方程的值,用z3解方程算出未知数
- 然后再用得到的未知数作为方程的值,算新的未知数,重复逆运算9次,最终算出第一步的下标。难受的是,每一次逆算完方程还要去表里找到对应的下标
- 最后再用
1
的操作异或一下表中的数据,就解出来了
分析完算法距离比赛结束还有12个小时,此时我的内心戏
A:没有什么事是熬夜解决不了的,如果有就战到天亮
B:省省吧,明天还要搬一天的砖呢,毁灭吧,赶紧的,累了
A:那做这么多天的题不能白做了,之前的夜不能白熬呀
B:那,就明天中午休息的时候发一篇总结贴,混个精华或优秀也值呀
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
最后于 2019-10-29 18:02
被KevinsBobo编辑
,原因: 原附件中少了cpp文件