MFC-first

上上周末大病一场,直到周日才恢复的七七八八的,帮着学弟打了一场CTF,顺便就把很久以前都懒得坑的MFC逆向也给啃了,这里记录一下过程

记一次MFC的逆向

Junk Instruction

从题目看出可能是和junk code相关的内容。这次我像往常一样直接瞎找函数,终于不奏效了(之前的时候真的神奇,MFC直接直觉断点居然能找到合适的函数。。)于是跟着网上的教程试着做了一次定位。

定位函数

之前我找的时候都是瞎找,这个思路就不太对。。应该想办法找到当前控件的注册函数才对。控件上注册的函数一般来说都是关键的函数,这道题目中就是这个check button。
在MFC中有一个消息映射表的概念,其中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct AFX_MSGMAP{
AFX_MSGMAP * pBaseMessageMap;
AFX_MSGMAP_ENTRY * lpEntries;
}
struct AFX_MSGMAP_ENTRY{
UINT nMessage; //Windows Message
UINT nCode //Control code or WM_NOTIFY code
UINT nID; //control ID (or 0 for windows messages)
UINT nLastID; //used for entries specifying a range of control id's
UINT nSig; //signature type(action) or pointer to message
AFX_PMSG pfn; //routine to call (or specical value)
}

其中这个AFX_MSGMAP_ENTRY中的最后一个成员AFX_PMSG就是一个函数指针,指向了当前控件绑定的函数。同时,这个nID成员描述的是当前控件的ID,利用这个ID就能确定我们所寻找的控件。然后这个AFX_MSGMAP结构体则会记录一个指向AFX_MSGMAP_ENTRY的指针,于是查找控件的注册函数的思路可以缩小为:

  • 找到AFX_MSGMAP
  • 找到控件的ID

寻找控件ID

这个过程挺容易找到的,只要利用工具Resource Hacker这个工具即可。将当前的MFC导入到HR中,然后便可检查其中各种控件的元素。从这个题目中我们能够找到如下的位置:

这个地方写了这个Check Button对应的ID为1001,于是这个控件的ID我们就找好了

寻找AFX_MSGMAP

这个地方需要一点技巧。注意到这个结构体的第一个成员是AFX_MSGMAP * pBaseMessageMap。这个成员会指向一个叫做GetMessageMap的函数地址,函数的内容很简单:

1
2
3
4
5
.text:0040FE48 GetMessageMap   proc near               ; DATA XREF: .rdata:0055174C↓o
.text:0040FE48 ; .rdata:stru_57F674↓o
.text:0040FE48 mov eax, offset off_551898
.text:0040FE4D retn
.text:0040FE4D GetMessageMap endp

就是将某一个AFX_MSGMAP的地址返回。同时当前的AFX_MSGMAP_ENTRY*往往会指向相邻的地址,因为内存中通常是如下排列的:

1
2
3
4
AFX_MSGMAP
AFX_MSGMAP_ENTRY1
AFX_MSGMAP_ENTRY2
...

利用这个几个特征,就能够写一个IDA的idc脚本。这里直接是用了参考博客中给出来的脚本:

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
#include <idc.idc>

static NotEndAddr(pAddr){
auto i=0;
for (i=0;i<6;i++){
if (Dword(i*4+pAddr)!=0)
return 1; //not end
}
return 0; //reach the end
}
static isMsgMap(checkAddr,startVa,endVa){
auto tmp1=Dword(checkAddr);
auto tmp2=Dword(checkAddr+4);

auto pAddr=checkAddr+8;
if (tmp2==checkAddr+8){

while(NotEndAddr(pAddr)){
if(Dword(pAddr+20)<startVa||Dword(pAddr+20)>endVa){
// Message("Invalid Addr at %0x.\n",pAddr);
return 0;
}

pAddr=pAddr+24;
}
return 1;
}
return 0;
}

static main(){
auto startRdataVa=0x0044E880; //the start addr of .rdata
auto size=0x0000DAA8; //the size of .rdata

auto startValidVa=0x00400000; //check the addr is valid or not
auto endValidVa=0x0046A000;

auto i=0;
for(i=0;i<size;i=i+4){
if(isMsgMap(i+startRdataVa,startValidVa,endValidVa)){
Message("Found Possible MessageMap at %0x.\n",i+startRdataVa);
}
}
Message("Finish searching.\n");

return 0;
}

其中的startRdataVa, size, startValidVa, endValidVa需要根据时机情况更改。此脚本能够找到几个类似的的地址,此时用我们之前提到的特征对其进行过滤:

  • 第一个地址指向的是pBaseMessageMap,也就是GetMessageMap,只会简单的返回一个地址
  • 检查Entry的时候,能够找到我们ID对应的注册函数(分析的时候建议导入结构体分析)

分析逻辑

找到了消息处理函数之后,本来以为大功告成,后来发现里面塞满了junk code…好的把继续。

junk code

点开函数,里面的基本 code 基本如下:

这些call 然后 pop之类的,全部都是junk code,分析之后发现一个特征:当插入了junk code 为22222222h结尾的场合,下一句就是正确的代码段,所以这里利用这一个特点写了个py脚本patch(其实我觉得应该用IDApython好一点。。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fd = open("Junk_Instruction.exe",'rb')
content = fd.read()
nop = b"\x90"
junk1 = "E8 00 00 00 00 58 89 45 D8 E8 03 00 00 00 EA EB 09 5B 43 53 B8 11 11 11 11 C3 E8 07 00 00 00 BB 33 33 33 33 EB 0D BB 11 11 11 11 5B BB A1 2B 40 00 53 C3 BB 22 22 22 22".split(" ")
junk1_byte = bytes([int(i,16) for i in junk1])
junk1_nop = len(junk1) * nop
content = content.replace(junk1_byte, len(junk1) * nop)
index = 0
spe = b"\x22\x22\x22\x22"
while content.find(spe, index)!=-1:
index = content.find(spe, index) - len(junk1) + 4
print("find at %x"%index)
content = content.replace(content[index:index+len(junk1)], nop * len(junk1))
index += len(junk1)

fd.close()
fd = open("Junk_Instruction_.exe",'wb')
fd.write(content)
fd.close()

处理之后,逻辑就清晰了
最后吐槽一下,这个代码段我居然在网上找到了一摸一样的。。。

最终处理

junk code去除之后,整体的逻辑就很简单了:

整体逻辑翻译一下,伪代码大约如下:

1
2
3
4
5
6
7
8
9
10
11
12
input = GetInput()
input.reverse()
key_str = "qwertyuiop"
map = [0] * 256
GetMap(map)
UseMapGetInput(map, input)
for i,j in input, check_str:
if i != j:
print("ERROR!")
return

print("CORRECT!")

这个GetMap是一个用"qwertyuiop"和数字生成映射表的一个函数,但是这也就意味着函数本身的内容是不变的,与输入无关。并且之后的UseMapGetInput就是将map进一步映射,然后简单与input异或,所以我们可以用最简单的复现算法的方式,将这个input 的内容复现出来。脚本如下:

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
def gen_map(string):
ans = [0] * 256
tmp = [0] * 256
v4 = 0
for i in range(256):
ans[i] = i
tmp[i] = ord(string[i % len(string)])
for j in range(256):
v4 = (tmp[j] + v4 + ans[j])%256
v3 = ans[j]
ans[j] = ans[v4]
ans[v4] = v3
return ans

def GetAnswer(maps, Input, length):
index = 0
sum = 0
ans = []
for i in range(length):
index = (index + 1) % 256
sum = (sum + maps[index]) % 256
v4 = maps[index]
maps[index] = maps[sum]
maps[sum] = v4
Input[i] ^= maps[(maps[sum] + maps[index]) % 256]
ans.append(chr(Input[i]))
result = i + 1
return result,ans

if __name__ == "__main__":
flag = [0x5B,0xD6,0xD0,0x26,0xC8,0xDD,0x19,0x7E,0x6E,0x3E, 0xCB, 0x16, 0x91, 0x7D, 0xFF, 0xAF, 0xDD, 0x76, 0x64, 0xB0, 0xF7, 0xE5, 0x89, 0x57, 0x82, 0x9F, 0xC,0,0x9E,0xD0,0x45,0xFA]
maps = gen_map("qwertyuiop")
res, ans = GetAnswer(maps, flag, len(flag))
if res == len(flag):
ans.reverse()
print(''.join(ans))
# flag{973387a11fa3f724d74802857d3e052f}

基本上就能得到答案了。加上前面逆向可以才到需要再答案最外加上flag{},于是可以得到flag

参考博客

http://www.cnblogs.com/h2zZhou/p/10593168.html