运行情况
运行情况如下:
>CrackMe.exe
input like this:crackme.exe mykey
>CrackMe.exe 111111111111111
registration failed
看来是命令行参数的形式输入,并且有错误信息。
静态分析
主流程
入口是经典的VC代码:
.text:000000014000A1E8 sub rsp, 28h
.text:000000014000A1EC call sub_14000A70C
.text:000000014000A1F1 add rsp, 28h
.text:000000014000A1F5 jmp sub_14000A068
只不过这个sub rsp;add rsp
是那个哪个版本的VC,还是因为是64bit的是这样。
直接找出main
函数,其主要代码结构如下:
if ( argc == 2 )
{
just_like_memcpy(g_input, (const void *)0x104, (size_t)argv->key);
check_140002E00(&retaddr);
result = 0i64;
}
else
{
...
}
先判断是否带一个参数运行,如果不是就在else
里执行一大串的算术计算,最后打印程序运行形式信息;如果是,则将参数copy到全局变量中,进入140002E00
函数进行校验。
校验函数
校验函数代码量比较大,大致拉了下伪代码的滚动条,里面也有非常大量的算术计算。直接追踪保存着输入的全局变量g_input
。
第一次引用g_input
是在1400033A6
:
.text:00000001400033A6 lea rax, g_input
.text:00000001400033AD mov r9, r14
.text:00000001400033B0
.text:00000001400033B0 loc_1400033B0: ; CODE XREF: check_140002E00+5B7↓j
.text:00000001400033B0 inc r9
.text:00000001400033B3 cmp [rax+r9], r12b
.text:00000001400033B7 jnz short loc_1400033B0
是在计算输入的长度,后面紧接着对长度值进行一些计算,然后在140003458
检查长度值计算结果,实际就是检查长度为30,其它的计算是冗余代码。
紧接着是第一次引用。
.text:00000001400034A1 test r9, r9
.text:00000001400034A4 jz loc_1400039C6 ; length !=0
.text:00000001400034AA lea r14, g_input
.text:00000001400034B1 mov rsi, 0CBF29CE484222325h
.text:00000001400034BB mov r12, 100000001B3h
.text:00000001400034C5 mov r13, 0AF63B44C8601A894h
.text:00000001400034CF nop
.text:00000001400034D0
.text:00000001400034D0 loc_1400034D0: ; CODE XREF: check_140002E00+723↓j
.text:00000001400034D0 xor eax, eax
.text:00000001400034D2 mov [rsp+0E40h+var_E18], ax ; clear
.text:00000001400034D7 movzx eax, byte ptr [r14] ; get input 4 bytes
.text:00000001400034DB mov byte ptr [rsp+0E40h+var_E18], al ; input 1 byte
.text:00000001400034DF lea rdx, [rsp+0E40h+var_E18]
.text:00000001400034E4 mov rcx, rsi
.text:00000001400034E7 test al, al ; check null
.text:00000001400034E9 jz short loc_140003509
.text:00000001400034EB nop dword ptr [rax+rax+00h]
.text:00000001400034F0
.text:00000001400034F0 loc_1400034F0: ; CODE XREF: check_140002E00+707↓j
.text:00000001400034F0 movsx rax, al
.text:00000001400034F4 xor rax, rcx
.text:00000001400034F7 mov rcx, rax
.text:00000001400034FA imul rcx, r12 ; FNV_hash_64bits
.text:00000001400034FE lea rdx, [rdx+1]
.text:0000000140003502 movzx eax, byte ptr [rdx] ; get next byte.Just null byte
.text:0000000140003505 test al, al
.text:0000000140003507 jnz short loc_1400034F0
.text:0000000140003509
.text:0000000140003509 loc_140003509: ; CODE XREF: check_140002E00+6E9↑j
.text:0000000140003509 lea eax, [r8+1] ; hash check right count+1
.text:000000014000350D cmp rcx, r13
.text:0000000140003510 cmovnz eax, r8d
.text:0000000140003514 mov r8d, eax
.text:0000000140003517 inc r15d
.text:000000014000351A inc r14
.text:000000014000351D movsxd rax, r15d
.text:0000000140003520 cmp rax, r9
.text:0000000140003523 jb short loc_1400034D0
.text:0000000140003525 cmp r8d, 3
检查输入长度不为0,然后以64bits的FNV_hash
算法分别计算输入各字节的hash值,并与常量0xAF63B44C8601A894
比较,并累加正确次数,再在hash全部计算完后检查正确次数不小于3。
可以跑下这个hash,真实是计算输入中9
的个数:
def FNV_hash_64(str,flag=False):
p = 0x100000001B3
it = 0xCBF29CE484222325
for i in str:
it = ((it^ord(i))*p)&0xffffffffffffffff
if flag:
return it
else:
return struct.pack('>Q',it)
hash_table = []
for i in range(0x100):
hash_table.append(FNV_hash_64(chr(i),True))
print chr(hash_table.index(0xAF63B44C8601A894))
再下面又是大量计算与赋值,不过与输入没有直接关系,跳过。来到140003F2A
。代码不贴了,有点多。这里是还是用64bitsFNV_hash
算法分别计算输入前9字节的hash值,并与分别与9个常量比较,正确与否影响一个局部变量值,此局部变量参与下面的计算,影响流程。上脚本,上面已经有了单字符的hash表,跑出来结果为KXCTF2018
:
check_table = [ 0xAF64064C860233EA,0xAF64154C86024D67,
0xAF63FE4C86022652,0xAF64094C86023903,
0xAF63FB4C86022139,0xAF63AF4C8601A015,
0xAF63AD4C86019CAF,0xAF63AC4C86019AFC,
0xAF63B54C8601AA47]
for i in check_table:
print chr(hash_table.index(i))),
再次跳过计算,来到1400044BD
。此处又再一次地用了64bitsFNV_hash
算法计算并检验全部输入的hash值为0x4F8075587499C0FF
。
再下面就很难看了,有些9
、.dll
的常量和32bits的FNV_hash
算法计算。
虽然难看,但在140004FAB
处,结合此处的9
、.dll
,还是能猜出此部分的两个函数调用:14000BDD8
函数为strchr
,14000E46C
函数为strcat
。因为出现.dll
必然是拼接dll文件名嘛(看到此处惊出一身冷汗,难道此题里还藏有dll?)。
主要操作就是把输入用'9'进行分段,猜测程序输入一共三段,第一段已经校验,最后以9结尾,此操作最后把最后的9替换成了\x00
。所以flag格式为KXCTF20189xxxxxxxxx9xxxxxxxxx9
。
但是这flag形式也不是绝对的,只是想的最简单的一种,再结合dll名只取了5字节,那有可能是KXCTF20189xxxxx9xxxxxxxxxxxxx9
。但是如果中间的9
前面或(和)是最后9
后面添加了几个未参加单独检验的字节,最后靠输入的hash跑来肯定也有可能的。所以后两个9
的位置未定。初步只能假设KXCTF20189xxxxx9xxxxxxxxxxxxx9
的形式来简化求解。
下面代码不太容易看也看不明白。需要上动态了。
动态调试
用前面的flag形式作为输入只要手动过了全输入串的hash检查就可以继续往下了。具体过程不说了。
1400053C6
处附近是获取LoadLibrary
api地址(至于GetModuleHandleA
那条路似乎是走不通的);14000548B
处是加载拼接的dll;140005516
处是获取GetProcAdress
api地址;14000554D
处是从刚加载的dll中获取以第三部分输入为名的函数api地址;14000557E
处是调用新获取到api。这里除了GetProcAdress
的,其它api的获取都是通过PEB取得模块基址,再枚举函数名并用32bits的FNV_hash
值确定的。
上面是正确的流程,我整理下:
- 获取api地址
- 加载dll
- 取得函数地址
- 调用函数,如果返回低4字节是负数则跳到成功信息打印
加载5个字节名字的dll,我们的第一反应是ntdll.dll
,如果输入没有其它未单独校验的字节,那剩下的13字节就是函数名。
我dump出了ntdll的所有导出函数并挑出13字节的函数,进行了枚举,结果就是没结果。又尝试其它能调用返回负数的函数,字节数不够的进行爆破跑,还是没有结果。
于是我又跟踪了下如果取dll函数地址失败的路径。其流程与上面类似,只不过加载是wintrust.dll
,获取的函数名为SoftpubCleanup
。
搜索了下,查到这个文章https://bbs.pediy.com/thread-221970-1.htm,讲windows签名劫持的。里面提到了可使用的劫持函数wintrust!SoftpubCleanup
和ntdll!DbgUiContinue
。DbgUiContinue
确实是13字节,为什么上面没有跑出结果呢。苦苦思索不得其解,就放着了。
后来想到dll名可能是大写。尝试一下,check通过。
所以最后的flag为KXCTF20189NTDLL9DbgUiContinue9
。
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。