一个很有质量的比赛,题目都是花了心思,且有专门的人维护的,很用心,点赞
引言
最近学习了一些 win32 的皮毛,正好有个LCTF的比赛,故专门找逆向来做,连web老本行都没做(其实是不会做,题目很有水准),本来想做几题的,没想到逆向第一题就这么有质量,近乎花了一天时间才做出来,不得不说这个比赛很用心
use_your_IDA
拿到题目丢PEiD,并无异样
一个win32 Console 程序 尝试运行,要求输入flag,输入即flag,很常规的逆向套路
既然没壳,直接上IDA和Ollydbg看看,动静结合嘛 上IDA流程图
首先看到printf了欢迎词,然后调用了两个函数sub_40108F
和sub_4010A1
,随后判断内存里Key_word的值是否为零,为零就成功打印Congratulation!,不是则打印Wrong!
不要问我这些函数你打开之后不是这个名字,当然是因为我分析了功能之后修改了函数名啊。。。
通过在Ollydbg里面修改寄存器和跳转,到达测试函数单步调试然后查看效果就好了,这个方法比纯静态的方法快,也能更直观地观察到函数作用
首先看一下第一个函数sub_40108F
near
sub_40108F proc .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_40108F
的return
处构造地址,很容易得出跳转的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!
跳转到此函数分析
mov ecx,0x3 ;三次循环
0042EE36 mov edx,0x0 ;偏移置零
0042EE3B mov eax,dword ptr ds:[edx+0x43F058] ;将内存中某个位置的值取出放入eax
0042EE40 cmp dword ptr ds:[edx+ebx+0x1],eax ;比较我们输入的前四位
0042EE46 jnz use_your.0042F91A ;不等于则跳转到Wrong
0042EE4A add edx,0x4
0042EE50 loopd short use_your.0042EE40 ;循环 0042EE53
不难看出,这段函数检验了我们输入的前12个输入
我们发现ds:[0x43F058]
处的值是定值(可以结合查看静态IDA得到结论)
那么我们现在的思路就是逆向加密函数,然后再和结果配对
那么问题来了,这么长的函数我们怎么办
又没有哪个脚本可以直接定位到这个跳转函数不运行然后通过ollydbg里面的代码直接爆破
陷入僵局
破釜沉舟
既然这么多代码,结合我之前用python处理过很多数据,那我就将你当成数据处理的题目,把加减、异或等操作分析出来,然后用python进行运算,将输入进行爆破 其实我心里也没谱,毕竟这么新奇的做法,反正也当练手,无所谓了
- 将加密函数的代码从Olldbg中(也可以在IDA里面)复制出来,放人一个文本
- 写脚本分析文本内对每个栈内数据进行的加减、异或等操作
- 运算得出结果,与内存中提取出来的数据进行比对,不对则改变输入,继续运算,直到匹配为止
#coding:utf-8
import re
= [0x666,0xF2 ,0x6E ,0xD1,0xB1 ,0x7E, 0x8B ,0x3E ,0x8E ,0xB1 ,0x67 ,0x6E ,0xE2,0x666,0x666,0x666,0x666,0x2F,0xB0,0xEC,0x00] #将内存里面的数值提出出来放入数组,0x666为无关值
check_list
def find(hexStr,realHex):
file = open(r"your_path\check_lctf.txt","rb")
= file.readlines()
lines =[]
m_list=0
flag=0
count#分析算法模块
for i in lines:
if re.match(".*?movzx eax,byte ptr ds:\[ebx\+0x"+str(hex(hexStr))[2:].upper()+"\]",i):
=1
flag+=1
countif re.match(".*?mov byte ptr ds:\[ebx\+0x"+str(hex(hexStr))[2:].upper()+"\],al",i):
=0
flagif flag:
if flag>1:
m_list.append(i)+=1
flagif flag==0 and len(m_list):
=0
p_flag=""
p_reg
for t in m_list:
= re.findall(".*?(push|pop) (eax|ebx)",t)
pl if len(pl)>0:
if pl[0][0]=="push":
=1
p_flag=pl[0][1]
p_reg=[]
plcontinue
if p_flag and pl[0][0]=="pop" and p_reg==pl[0][1]:
=0
p_flag=[]
pl#分析运算
if p_flag==0:
= re.findall(".*?(sub|add|xor) eax,(0x[0-9A-Z]+)",t)
lt if len(lt)>0:
if lt[0][0]=="add":
# print "add ",hex(int(lt[0][1],16))
+=int(lt[0][1],16)
realHexif lt[0][0]=="sub":
# print "sub ",hex(int(lt[0][1],16))
-=int(lt[0][1],16)
realHexif lt[0][0]=="xor":
# print "xor ",hex(int(lt[0][1],16))
^=int(lt[0][1],16)
realHex%= 0x100
realHex =[]
m_listfile.close()
return realHex
=[]
lfor 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
hex(i)[2:])
l.append(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函数
可以观察到,eax
跟413F42
比较,而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