一个很有质量的比赛,题目都是花了心思,且有专门的人维护的,很用心,点赞

引言

最近学习了一些 win32 的皮毛,正好有个LCTF的比赛,故专门找逆向来做,连web老本行都没做(其实是不会做,题目很有水准),本来想做几题的,没想到逆向第一题就这么有质量,近乎花了一天时间才做出来,不得不说这个比赛很用心

use_your_IDA

拿到题目丢PEiD,并无异样

一个win32 Console 程序 尝试运行,要求输入flag,输入即flag,很常规的逆向套路

既然没壳,直接上IDA和Ollydbg看看,动静结合嘛 上IDA流程图

首先看到printf了欢迎词,然后调用了两个函数sub_40108Fsub_4010A1,随后判断内存里Key_word的值是否为零,为零就成功打印Congratulation!,不是则打印Wrong! 不要问我这些函数你打开之后不是这个名字,当然是因为我分析了功能之后修改了函数名啊。。。 通过在Ollydbg里面修改寄存器和跳转,到达测试函数单步调试然后查看效果就好了,这个方法比纯静态的方法快,也能更直观地观察到函数作用 首先看一下第一个函数sub_40108F

sub_40108F      proc near
.text:0040108F                 push    ebp
.text:00401090                 mov     ebp, esp
.text:00401092                 sub     esp, 13h
.text:00401095                 mov     eax, esp
.text:00401097                 call    sub_43B3E8
.text:0040109C                 add     esp, 13h
.text:0040109F                 pop     ebp
.text:004010A0                 retn

申请了0x13即19个字节的栈空间,然后将栈地址作为参数传入sub_43B3E8 再来看看函数sub_43B3E8

.text:0043B3E8 sub_43B3E8      proc near           
;存各个寄存器,最后一个存eax,里面有一个刚才申请的栈地址  
.text:0043B3E8                 push    ebx
.text:0043B3E9                 push    ecx
.text:0043B3EA                 push    edx
.text:0043B3EB                 push    eax
;获取句柄,然后循环读取输入
.text:0043B3EC                 push    0FFFFFFF6h      ; nStdHandle
.text:0043B3EE                 call    GetStdHandle
.text:0043B3F3                 mov     hConsoleInput, eax
.text:0043B3F8 loc_43B3F8:        
.text:0043B3F8                 push    0               ; pInputControl
.text:0043B3FA                 push    offset NumberOfCharsRead ; lpNumberOfCharsRead
.text:0043B3FF                 push    0FFh            ; nNumberOfCharsToRead
.text:0043B404                 push    offset byte_43F322 ; lpBuffer
.text:0043B409                 push    hConsoleInput   ; hConsoleInput
.text:0043B40F                 call    ReadConsoleA
.text:0043B414                 sub     NumberOfCharsRead, 2
.text:0043B41B                 jb      short loc_43B3F8;读取结束

.text:0043B41D                 xor     ecx, ecx ;标志位重置以及ecx清零
.text:0043B41F                 pop     ebx  ;弹出栈地址至ebx
.text:0043B420
.text:0043B420 loc_43B420:                    
.text:0043B420                 mov     al, byte_43F322[ecx] ;byte_43F322为读取输入字符的Buffer的地址,所以这句话是为了将输入第一个字符读入
.text:0043B426                 mov     [ecx+ebx], al    ;将字符存入栈
.text:0043B429                 inc     ecx
.text:0043B42A                 cmp     ecx, NumberOfCharsRead
.text:0043B430                 jb      short loc_43B420 ;循环至字符读完为止
.text:0043B432                 mov     byte ptr [ecx+ebx], 0    ;将栈里面的字符串末尾加个0
.text:0043B436                 mov     eax, ecx
.text:0043B438                 pop     edx
.text:0043B439                 pop     ecx
.text:0043B43A                 pop     ebx
.text:0043B43B                 retn ;返回
.text:0043B43B sub_43B3E8      endp

有了上次解题的经验可以看出此处有溢出漏洞,栈只有19个字节,但是读取起码有255个字节,但是目前还不知道怎么用,姑且留着备用

在分析下一个函数sub_4010A1之前来看一下影响Key_word的代码在哪 Key_word初始化为2

可以看到在函数sub_4010A1中对Key_word的操作只是加一减一,并不能将Key_word变成0 观察到最后一行有一个操作是将Key_word减二的操作,但是地址在42f90E,所以我们不难猜测,出题人的思路就是故意留出溢出漏洞,让我们找到正确的检验函数地址然后通过检验输入值,然后得到flag 通过简单地观察函数sub_4010A1,不难看出无论你输入啥,都会跳转到Wrong函数,然后输出Wrong!,程序退出,这也验证了我们的想法 那么,是不是有点熟悉的味道?

是的,看雪CTF的第二题(我上篇文章分析的题)的思路!

寻找突破口

经过一阵查找,发现了一个函数将内存改来改去,然后最后执行了一个将Key_word-2的操作,然后跳回到了我们原来的主函数比较Key_word的地方 确定是这里没错了

构造函数跳过来吧

地址是413142,由上面我们分析的溢出漏洞,我们可以在函数sub_40108Freturn处构造地址,很容易得出跳转的payload 19*"1"+'abcd'+B1A

.text:0040108F sub_40108F      proc near             
.text:0040108F                 push    ebp
.text:00401090                 mov     ebp, esp
.text:00401092                 sub     esp, 13h
.text:00401095                 mov     eax, esp
.text:00401097                 call    sub_43B3E8
.text:0040109C                 add     esp, 13h
.text:0040109F                 pop     ebp
.text:004010A0                 retn
.text:004010A0 sub_40108F      endp
1111111111111111111abcdB1A #其中B1A为地址413142对应的小端存储ASCLL码,abcd为40109F 处pop的ebp,然后才是return,所以要加上4位数给它pop,然后return就可以跳到我们想要的地方

当我们跳转到地址为413142的地方执行的时候,我们发现,这仍然是一个检验flag的函数,重点是,它离咱们的目标——Key_word-2 有3万+行汇编代码

emmmmmmmm...

先不管,我们先把这段超长的函数分析一下结构


.text:00413142                 mov     ebp, esp
.text:00413144                 sub     esp, 1Ch
.text:00413147                 mov     ebx, esp
.text:00413149                 movzx   eax, byte ptr [ebx+4]
.text:0041314D                 add     eax, 43h
.text:00413150                 mov     [ebx+4], al
.text:00413153                 movzx   eax, byte ptr [ebx+0Fh]
.text:00413157                 sub     eax, 45h
.text:0041315A                 mov     [ebx+0Fh], al
.text:0041315D                 movzx   eax, byte ptr [ebx+0Ch]
.text:00413161                 add     eax, 4Eh
.text:00413164                 mov     [ebx+0Ch], al
.text:00413167                 movzx   eax, byte ptr [ebx+2]
.text:0041316B                 sub     eax, 40h
.text:0041316E                 mov     [ebx+2], al
.text:00413171                 movzx   eax, byte ptr [ebx+6]
.text:00413175                 add     eax, 2Dh
.text:00413178                 mov     [ebx+6], al
.text:0041317B                 movzx   eax, byte ptr [ebx+0Dh]
.text:0041317F                 sub     eax, 3Dh
.text:00413182                 mov     [ebx+0Dh], al
.text:00413185                 movzx   eax, byte ptr [ebx+11h]
.text:00413189                 add     eax, 5Fh
.text:0041318C                 mov     [ebx+11h], al
.text:0041318F                 movzx   eax, byte ptr [ebx+2]
.text:00413193                 sub     eax, 24h
.text:00413196                 mov     [ebx+2], al
.text:00413199                 movzx   eax, byte ptr [ebx+10h]
.text:0041319D                 add     eax, 35h
.text:004131A0                 mov     [ebx+10h], al
.text:004131A3                 movzx   eax, byte ptr [ebx+0Eh]
.text:004131A7                 sub     eax, 15h
.text:004131AA                 mov     [ebx+0Eh], al
.text:004131AD                 movzx   eax, byte ptr [ebx+2]
.text:004131B1                 add     eax, 12h
.text:004131B4                 xor     eax, 2Fh
.text:004131B7                 mov     [ebx+2], al

接下来的函数全是这种将栈内的数取出来,加一个数或减去另外一个数,重复几次加减,然后异或一下再存回去原来的位置 逻辑简单是简单,主要还是太长的,几万行代码。。。我的天?

在Ollydbg中运行trace来跟踪跳转后这个函数到底干嘛了(我不会说我一开始是用二分法分析函数的。。。)

注意到,前面都是一些所谓的加减异或来加密我们输入的数值,然后在地址 42EE4A处我们发现了跳转函数,让我们输出了Wrong!

跳转到此函数分析

0042EE36     mov ecx,0x3    ;三次循环
0042EE3B     mov edx,0x0    ;偏移置零
0042EE40     mov eax,dword ptr ds:[edx+0x43F058] ;将内存中某个位置的值取出放入eax
0042EE46     cmp dword ptr ds:[edx+ebx+0x1],eax     ;比较我们输入的前四位
0042EE4A     jnz use_your.0042F91A  ;不等于则跳转到Wrong
0042EE50     add edx,0x4
0042EE53     loopd short use_your.0042EE40  ;循环

不难看出,这段函数检验了我们输入的前12个输入 我们发现ds:[0x43F058]处的值是定值(可以结合查看静态IDA得到结论) 那么我们现在的思路就是逆向加密函数,然后再和结果配对

那么问题来了,这么长的函数我们怎么办

又没有哪个脚本可以直接定位到这个跳转函数不运行然后通过ollydbg里面的代码直接爆破

陷入僵局

破釜沉舟

既然这么多代码,结合我之前用python处理过很多数据,那我就将你当成数据处理的题目,把加减、异或等操作分析出来,然后用python进行运算,将输入进行爆破 其实我心里也没谱,毕竟这么新奇的做法,反正也当练手,无所谓了

  1. 将加密函数的代码从Olldbg中(也可以在IDA里面)复制出来,放人一个文本
  2. 写脚本分析文本内对每个栈内数据进行的加减、异或等操作
  3. 运算得出结果,与内存中提取出来的数据进行比对,不对则改变输入,继续运算,直到匹配为止
#coding:utf-8
import re
check_list = [0x666,0xF2 ,0x6E ,0xD1,0xB1 ,0x7E, 0x8B ,0x3E ,0x8E ,0xB1 ,0x67 ,0x6E ,0xE2,0x666,0x666,0x666,0x666,0x2F,0xB0,0xEC,0x00] #将内存里面的数值提出出来放入数组,0x666为无关值

def find(hexStr,realHex):
    file = open(r"your_path\check_lctf.txt","rb")
    lines = file.readlines()
    m_list=[]
    flag=0
    count=0
#分析算法模块
    for i in lines:
        if re.match(".*?movzx eax,byte ptr ds:\[ebx\+0x"+str(hex(hexStr))[2:].upper()+"\]",i):
            flag=1
            count+=1
        if re.match(".*?mov byte ptr ds:\[ebx\+0x"+str(hex(hexStr))[2:].upper()+"\],al",i):
            flag=0
        if flag:
            if flag>1:
                m_list.append(i)
            flag+=1
        if flag==0 and len(m_list):
            p_flag=0
            p_reg=""

            for t in m_list:
                pl = re.findall(".*?(push|pop) (eax|ebx)",t)
                if len(pl)>0:
                    if pl[0][0]=="push":
                        p_flag=1
                        p_reg=pl[0][1]
                        pl=[]
                        continue

                    if  p_flag and pl[0][0]=="pop" and p_reg==pl[0][1]:
                        p_flag=0
                        pl=[]
                #分析运算
                if p_flag==0:
                    lt = re.findall(".*?(sub|add|xor) eax,(0x[0-9A-Z]+)",t)
                    if len(lt)>0:
                        if lt[0][0]=="add":
                           # print "add ",hex(int(lt[0][1],16))
                            realHex+=int(lt[0][1],16)
                        if lt[0][0]=="sub":
                          #  print "sub ",hex(int(lt[0][1],16))
                            realHex-=int(lt[0][1],16)
                        if lt[0][0]=="xor":
                          #  print "xor ",hex(int(lt[0][1],16))
                            realHex^=int(lt[0][1],16)
                        realHex %= 0x100
            m_list=[]
    file.close()
    return realHex
l=[]
for j in range(1,13):#爆破输入范围
    print "Processing : ",j
    for i in range(256):#输入字符范围
        if find(j,i)==check_list[j]:
            print "Find answer: check_list[",j,"]","=>",i
            l.append(hex(i)[2:])
            break
print "".join(l)
print "".join(l).decode("hex")

。。。万万没想到,这破招,居然成了,感谢作者没有将运算搞复杂,不然我得写死。

我们可以借助python分析加密算法,跑个3分钟,得出前12位flag为W1ecom_to_L (算法和程序就不优化了,毕竟比赛时间不允许,能用就好,轻喷)

Processing :  1
Find answer: check_list[ 1 ] => 87
Processing :  2
Find answer: check_list[ 2 ] => 101
Processing :  3
Find answer: check_list[ 3 ] => 49
Processing :  4
Find answer: check_list[ 4 ] => 99
Processing :  5
Find answer: check_list[ 5 ] => 111
Processing :  6
Find answer: check_list[ 6 ] => 109
Processing :  7
Find answer: check_list[ 7 ] => 101
Processing :  8
Find answer: check_list[ 8 ] => 95
Processing :  9
Find answer: check_list[ 9 ] => 116
Processing :  10
Find answer: check_list[ 10 ] => 111
Processing :  11
Find answer: check_list[ 11 ] => 95
Processing :  12
Find answer: check_list[ 12 ] => 76
576531636f6d655f746f5f4c
We1come_to_L

然后我们再如法炮制,将We1come_to_L1111111abcdB1A输入调试,run trace跟踪函数执行,发现有一处是检验咱们跳转来的地址的

.text:0042F3A1                 cmp     eax, 413F42h
.text:0042F3A6                 jnz     loc_42F91A   ;不相等则跳转到Wrong函数

可以观察到,eax413F42比较,而eax是在这里存入的

.text:0042EE55                 mov     eax, [edx+ebx+0Ch]   ;将栈底+4位置的数据,即我们构造的溢出地址,放进eax
.text:0042EE59                 add     eax, 0A00h
.text:0042EE5E                 push    eax  ;加了A00h后入栈

接下来一百多行的代码都是push eax 进去再pop 出来再push进去 最后在42F3A1比较的eax就是加了A00h的溢出地址 我们构造的溢出地址是413142加上A00h后也不是413F42h啊 但是我们程序却没有在此跳转,说明我们构造的地址是对的 终于我们在42F10E处找到了一条异或的语句

.text:0042F10E                 xor     eax, 400h

。。。这个出题人还真是调皮

好了,我们继续跟踪函数执行

.text:0042F3AE                 mov eax,dword ptr ds:[edx+ebx+0x5] ;输入的第17至20位数据
.text:0042F3B2                 push eax

     ......                    ......
     
.text:0042F900                 pop     eax
.text:0042F901                 and     eax, 0FFFFFFh    ;将第20个输入抹去(置零)
.text:0042F906                 cmp     eax, dword ptr unk_43F05C[edx]   ;ds:[43F068]=00ECB02F
.text:0042F90C                 jnz     Wrong    ;不相等则Wrong
.text:0042F90E                 sub     Key_word, 2
.text:0042F915                 jmp     loc_40106E   ;跳回去咱们start函数判断Key_word是否为零的地方

可以看出只检验了17~19位输入,20位置零后再比较的,所以是啥都无所谓 而且我们还观察到,对1719位加密只有上面我们写脚本的那几万行汇编进行过,之后的函数都没有动,所以我们一样可以用之前的脚本爆破计算出17-19位flag,只需要把对应数据输入且for循环的范围从112改成17~19

运行脚本,输入如下

Processing :  17
Find answer: check_list[ 17 ] => 48
Processing :  18
Find answer: check_list[ 18 ] => 49
Processing :  19
Find answer: check_list[ 19 ] => 55
303137
017

然后我们整合一下flag——We1come_to_L1111017abcdB1A

emmmmmmm.....

这么随便的flag么

然后去比赛网站上看,提示多解的四位为ctf2 那么想当然的,我们可以得到We1come_to_Lctf2017abcdB1A

。。那么还是有4位是多解啊

然后怎么提交都不对,加比赛群找到出题人,出题人回应把payload去掉就好了

。。。。

所以最后提交的falg是We1come_to_Lctf2017B1A 本题至此结束

总结

  • 基础很重要,思路更重要
  • 逆向是一个体力活也是一个精细活,很佩服搞二进制的大佬们
  • 人生苦短,我选python