首页
论坛
专栏
课程
16

[原创]看雪.京东 2018CTF 第十二题 破解之道 Writeup

poyoten 2018-7-9 14:52 541

运行情况

运行情况如下:

>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函数为strchr14000E46C函数为strcat。因为出现.dll必然是拼接dll文件名嘛(看到此处惊出一身冷汗,难道此题里还藏有dll?)。

 

主要操作就是把输入用'9'进行分段,猜测程序输入一共三段,第一段已经校验,最后以9结尾,此操作最后把最后的9替换成了\x00。所以flag格式为KXCTF20189xxxxxxxxx9xxxxxxxxx9

 

但是这flag形式也不是绝对的,只是想的最简单的一种,再结合dll名只取了5字节,那有可能是KXCTF20189xxxxx9xxxxxxxxxxxxx9。但是如果中间的9前面或(和)是最后9后面添加了几个未参加单独检验的字节,最后靠输入的hash跑来肯定也有可能的。所以后两个9的位置未定。初步只能假设KXCTF20189xxxxx9xxxxxxxxxxxxx9的形式来简化求解。

 

下面代码不太容易看也看不明白。需要上动态了。

动态调试

用前面的flag形式作为输入只要手动过了全输入串的hash检查就可以继续往下了。具体过程不说了。
1400053C6处附近是获取LoadLibraryapi地址(至于GetModuleHandleA那条路似乎是走不通的);14000548B处是加载拼接的dll;140005516处是获取GetProcAdressapi地址;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!SoftpubCleanupntdll!DbgUiContinueDbgUiContinue确实是13字节,为什么上面没有跑出结果呢。苦苦思索不得其解,就放着了。

 

后来想到dll名可能是大写。尝试一下,check通过。

 

所以最后的flag为KXCTF20189NTDLL9DbgUiContinue9



快讯:科锐逆向工程师培训(第32期,10月10日开学)!

最新回复 (2)
ccfer 2018-7-10 18:54
2
你这从wintrust都能搞出事情,也是独秀了
poyoten 2018-7-10 20:58
3
ccfer 你这从wintrust都能搞出事情,也是独秀了
没办法,开始没注意dll名大小写问题,跑不出函数,还以为有其它东西。就跟了进了别的路径看有什么信息。
返回