2017-LCTF-use_your_IDA解析

151145228598.jpg

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

引言

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

use_your_IDA

拿到题目丢PEiD,并无异样

12-34-06.jpg

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

12-35-47.jpg

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

12-48-53.jpg

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

1
2
3
4
5
6
7
8
9
sub_40108F proc near
push ebp
mov ebp, esp
sub esp, 13h
mov eax, esp
call sub_43B3E8
add esp, 13h
pop ebp
retn

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
sub_43B3E8 proc near
;存各个寄存器,最后一个存eax,里面有一个刚才申请的栈地址
push ebx
push ecx
push edx
push eax
;获取句柄,然后循环读取输入
push 0FFFFFFF6h ; nStdHandle
call GetStdHandle
mov hConsoleInput, eax
loc_43B3F8:
push 0 ; pInputControl
push offset NumberOfCharsRead ; lpNumberOfCharsRead
push 0FFh ; nNumberOfCharsToRead
push offset byte_43F322 ; lpBuffer
push hConsoleInput ; hConsoleInput
call ReadConsoleA
sub NumberOfCharsRead, 2
jb short loc_43B3F8;读取结束
xor ecx, ecx ;标志位重置以及ecx清零
pop ebx ;弹出栈地址至ebx
loc_43B420:
mov al, byte_43F322[ecx] ;byte_43F322为读取输入字符的Buffer的地址,所以这句话是为了将输入第一个字符读入
mov [ecx+ebx], al ;将字符存入栈
inc ecx
cmp ecx, NumberOfCharsRead
jb short loc_43B420 ;循环至字符读完为止
mov byte ptr [ecx+ebx], 0 ;将栈里面的字符串末尾加个0
mov eax, ecx
pop edx
pop ecx
pop ebx
retn ;返回
sub_43B3E8 endp

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

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

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



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

20-33-51.jpg

寻找突破口

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

14-59-03.jpg

构造函数跳过来吧

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

1
2
3
4
5
6
7
8
9
10
sub_40108F proc near
push ebp
mov ebp, esp
sub esp, 13h
mov eax, esp
call sub_43B3E8
add esp, 13h
pop ebp
retn
sub_40108F endp

1
1111111111111111111abcdB1A #其中B1A为地址413142对应的小端存储ASCLL码,abcd为40109F 处pop的ebp,然后才是return,所以要加上4位数给它pop,然后return就可以跳到我们想要的地方

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

emmmmmmmm…

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
mov ebp, esp
sub esp, 1Ch
mov ebx, esp
movzx eax, byte ptr [ebx+4]
add eax, 43h
mov [ebx+4], al
movzx eax, byte ptr [ebx+0Fh]
sub eax, 45h
mov [ebx+0Fh], al
movzx eax, byte ptr [ebx+0Ch]
add eax, 4Eh
mov [ebx+0Ch], al
movzx eax, byte ptr [ebx+2]
sub eax, 40h
mov [ebx+2], al
movzx eax, byte ptr [ebx+6]
add eax, 2Dh
mov [ebx+6], al
movzx eax, byte ptr [ebx+0Dh]
sub eax, 3Dh
mov [ebx+0Dh], al
movzx eax, byte ptr [ebx+11h]
add eax, 5Fh
mov [ebx+11h], al
movzx eax, byte ptr [ebx+2]
sub eax, 24h
mov [ebx+2], al
movzx eax, byte ptr [ebx+10h]
add eax, 35h
mov [ebx+10h], al
movzx eax, byte ptr [ebx+0Eh]
sub eax, 15h
mov [ebx+0Eh], al
movzx eax, byte ptr [ebx+2]
add eax, 12h
xor eax, 2Fh
mov [ebx+2], al

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

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

15-33-30.jpg

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

15-33-47.jpg

跳转到此函数分析

1
2
3
4
5
6
7
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. 运算得出结果,与内存中提取出来的数据进行比对,不对则改变输入,继续运算,直到匹配为止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#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
(算法和程序就不优化了,毕竟比赛时间不允许,能用就好,轻喷)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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跟踪函数执行,发现有一处是检验咱们跳转来的地址的

1
2
cmp eax, 413F42h
jnz loc_42F91A ;不相等则跳转到Wrong函数

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

1
2
3
mov eax, [edx+ebx+0Ch] ;将栈底+4位置的数据,即我们构造的溢出地址,放进eax
add eax, 0A00h
push eax ;加了A00h后入栈

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

1
xor eax, 400h

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

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

1
2
3
4
5
6
7
8
9
10
11
mov eax,dword ptr ds:[edx+ebx+0x5] ;输入的第17至20位数据
push eax
...... ......
pop eax
and eax, 0FFFFFFh ;将第20个输入抹去(置零)
cmp eax, dword ptr unk_43F05C[edx] ;ds:[43F068]=00ECB02F
jnz Wrong ;不相等则Wrong
sub Key_word, 2
jmp loc_40106E ;跳回去咱们start函数判断Key_word是否为零的地方

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

运行脚本,输入如下

1
2
3
4
5
6
7
8
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

23-36-45.jpg

emmmmmmm…..

这么随便的flag么

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

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

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

。。。。

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

总结

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


----- 感谢阅读 -----