首页
社区
课程
招聘
[原创]2021 KCTF 春季赛 第六题 寻回宝剑 wp
2021-5-18 13:20 6672

[原创]2021 KCTF 春季赛 第六题 寻回宝剑 wp

2021-5-18 13:20
6672

0x00 简单分析

初步观察

题目是windows64位应用程序.

 

观察PE文件的Section, 发现有8个节, 最后一个节.cop有可执行权限, 说明应该有加壳或者其他处理.

 

载入调试器看看EntryPoint代码.

好家伙!往EntryPoint上下翻翻都是清一色的代码块.

 

简单单步跟踪了一下, 总结混淆方案共有4种.

混淆方案1:jmp指令简单变形

1
2
3
4
5
6
7
8
9
10
push rax                       
push rax                       
pushfq                         
call $+5           
pop rax                        
xor rax,D1E15                  
mov qword ptr ss:[rsp+10],rax  
popfq                          
pop rax                        
ret

混淆方案2:jmp指令复杂变形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
push rax
push rax
pushfq
push rax
push rax
pushfq
call $+5  
pop rax
xor rax, 0xE4EB3
mov qword ptr ss:[rsp+0x10], rax
popfq
pop rax
pop rax
add rax, 0x8242E
mov qword ptr ss:[rsp+0x10], rax
popfq
pop rax
ret

混淆方案3:垃圾指令push reg/pop reg

1
2
3
4
5
6
7
8
9
10
push rdx
push rdx
pop rdx
pop rdx
push rcx
push rdx
pop rdx
push rbx
pop rbx
pop rcx

混淆方案4:call指令变形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
push rax
push rax
pushfq
push rax
push rax
pushfq
call $+5 
pop rax
xor rax, 0xDC85F
mov qword ptr ss:[rsp+0x10], rax
popfq
pop rax
pop rax
add rax, 0x51438
mov qword ptr ss:[rsp+0x10], rax
popfq
pop rax
jmp 0x00000001400979D6

整理思路

整个文件大小将近1M了, 可见混淆数量之多.
如果想靠肉眼顶着混淆看代码, 属实不太现实.
看来必须要先写代码对PE文件的混淆做优化, 再来盘程序逻辑了.

0x01 优化题目的混淆

准备工作

针对题目里的4种混淆方案, 需要给出对应的去混淆方法.
不需要优化得很彻底, 只需要能不太费力地使用肉眼看代码逻辑即可.

 

干到这里留了个心眼.
既然咱们需要patch题目文件优化混淆, 题目会不会有CRC之类的校验代码段, 如果有的话还得先要patch代码校验.
先简单手动修改一处混淆(随便找了个混淆方案1的地方改成jmp指令), 保存文件, 运行.
yes, 运行是没问题, 看来可以放心patch题目文件了.

 

要patch文件, 首先需要把文件读入到内存中来, Section展开不展开都行.
这里需要注意的是, 题目的ImageBase是默认0x140000000, 申请的地址要满足后面几位也最好都是0, 要不然混淆方案中的xor计算会出错.

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
static uintptr_t g_nFileSize = 0;
static LPBYTE g_lpFileBuffer = 0;
 
void ReadPEFile()
{
    HANDLE hFile = NULL;
    HANDLE hMapFile = NULL;
    LPBYTE lpFileBuffer = NULL;
 
    hFile = CreateFileA("D:\\Projects\\KCTF2021Spring\\CrackNote\\6\\KCTF.exe",           // open MYFILE.TXT
        GENERIC_READ,              // open for reading
        FILE_SHARE_READ,           // share for reading
        NULL,                      // no security
        OPEN_EXISTING,             // existing file only
        FILE_ATTRIBUTE_NORMAL,     // normal file
        NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        std::cout << "Could not create file." << std::endl;
        goto EXIT_PROC;
    }
 
    g_nFileSize = GetFileSize(hFile, NULL);
    if (g_nFileSize == INVALID_FILE_SIZE)
    {
        std::cout << "GetFileSize error." << std::endl;
        goto EXIT_PROC;
    }
 
 
    hMapFile = CreateFileMappingA(hFile, NULL, PAGE_READONLY,
        0, 0, NULL);
    if (hMapFile == NULL)
    {
        std::cout << "Could not create file-mapping object." << std::endl;
        goto EXIT_PROC;
    }
 
 
    lpFileBuffer = (LPBYTE)MapViewOfFile(hMapFile,  // Handle to mapping object.
        FILE_MAP_READ,                        // Read/write permission
        0,                                    // Max. object size.
        0,                                    // Size of hFile.
        0);                                   // Map entire file.
    if (lpFileBuffer == NULL)
    {
        std::cout << "MapViewOfFile error." << std::endl;
        goto EXIT_PROC;
    }
 
   g_lpFileBuffer = new BYTE[g_nFileSize + 0x100000000];
   g_lpFileBuffer = (LPBYTE)(((QWORD)g_lpFileBuffer & 0xFFFFFFFF00000000) + 0x100000000);
    if (g_lpFileBuffer == NULL)
    {
        std::cout << "Alloc error." << std::endl;
        goto EXIT_PROC;
    }
    memcpy(g_lpFileBuffer, lpFileBuffer, g_nFileSize);
 
 
EXIT_PROC:
    if (lpFileBuffer != NULL)
    {
        UnmapViewOfFile(lpFileBuffer);
        lpFileBuffer = NULL;
    }
 
    if (hMapFile != NULL)
    {
        CloseHandle(hMapFile);
        hMapFile = NULL;
    }
 
    if (hFile != INVALID_HANDLE_VALUE)
    {
        CloseHandle(hFile);
        hFile = INVALID_HANDLE_VALUE;
    }
}

去混淆方案1

混淆方案1等价于jmp指令.
jmp到的地址等于call $+5下一行的地址异或上后面的异或值常量.
这种混淆方案在反汇编代码中随便拉都是, 看来可以先进行一波特征搜索patch掉这些"肉眼可见"的jmp变异.
这里需要注意的是, .text节和.cop节都是代码段, 两个代码段之间会有很多来回的跳转. 没有展开Section的话, 需要注意FA和VA的转换.

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
void AntiObfuscation1()
{
    LPBYTE lpStart = 0;
    LPBYTE lpEnd = 0;
    DWORD dwCount = 0;
 
    //.text节
    lpStart = g_lpFileBuffer+0x400;
    lpEnd = lpStart + 0x3000;
    dwCount = 0;
    while (true)
    {
        //50509CE800000000584835????????48894424109D58C3
        LPBYTE lpResult = (LPBYTE)FindHexBytes(lpStart, lpEnd, "50509CE800000000584835????????48894424109D58C3");
        if (lpResult == (LPVOID)-1) {
 
            break;
        }
        else {
            dwCount++;
            lpStart = lpResult + 1;
 
            QWORD dwFindFA = (QWORD)lpResult;
            QWORD dwFindVA = dwFindFA - 0x400 + 0x1000;
 
            QWORD dwCallNextFA = (QWORD)lpResult + 8;
            QWORD dwCallNextVA = dwCallNextFA -  0x400 + 0x1000 ;// 修复内存地址
 
            QWORD dwXor = Mem_RDword(lpResult + 11);
 
            QWORD dwNextAddrVA = dwXor ^ dwCallNextVA;
            QWORD dwNextAddrFA = 0;
 
            BYTE aryNop[25] = {
                0x90, 0x90, 0x90, 0x90, 0x90,
                0x90, 0x90, 0x90, 0x90, 0x90,
                0x90, 0x90, 0x90, 0x90, 0x90,
                0x90, 0x90, 0x90, 0x90, 0x90,
                0x90, 0x90, 0x90, 0x90, 0x90,
            };
 
            //混淆方案1的opcode size 固定是23
            Mem_WBytes2((LPVOID)dwFindFA, aryNop, 23);
 
            BYTE bHook[] = { 0xE9, 00, 00, 00, 00 };
            Mem_WDword(bHook + 1, (DWORD)(dwNextAddrVA - dwFindVA - 5));
            Mem_WBytes2((LPVOID)dwFindFA, bHook, 5);
        }
    }
    D("patch .text %d addr", dwCount);
 
 
    //.cop节
    lpStart = g_lpFileBuffer + 0x6200;
    lpEnd = lpStart + 0xF0000;
    dwCount = 0;
    while (true)
    {
        //50509CE800000000584835????????48894424109D58C3
        LPBYTE lpResult = (LPBYTE)FindHexBytes(lpStart, lpEnd, "50509CE800000000584835????????48894424109D58C3");
        if (lpResult == (LPVOID)-1) {
 
            break;
        }
        else {
            dwCount++;
            lpStart = lpResult + 1;
 
            QWORD dwFindFA = (QWORD)lpResult;
            QWORD dwFindVA = dwFindFA - 0x6200 + 0xB000;
 
            QWORD dwCallNextFA = (QWORD)lpResult + 8;
            QWORD dwCallNextVA = dwCallNextFA - 0x6200 + 0xB000;// 修复内存地址
 
            QWORD dwXor = Mem_RDword(lpResult + 11);
 
            QWORD dwNextAddrVA = dwXor ^ dwCallNextVA;
            QWORD dwNextAddrFA = 0;
 
            BYTE aryNop[25] = {
                0x90, 0x90, 0x90, 0x90, 0x90,
                0x90, 0x90, 0x90, 0x90, 0x90,
                0x90, 0x90, 0x90, 0x90, 0x90,
                0x90, 0x90, 0x90, 0x90, 0x90,
                0x90, 0x90, 0x90, 0x90, 0x90,
            };
 
            //混淆方案1的opcode size 固定是23
            Mem_WBytes2((LPVOID)dwFindFA, aryNop, 23);
 
            BYTE bHook[] = { 0xE9, 00, 00, 00, 00 };
            Mem_WDword(bHook + 1, (DWORD)(dwNextAddrVA - dwFindVA - 5));
            Mem_WBytes2((LPVOID)dwFindFA, bHook, 5);
        }
    }
    D("patch .cop  %d addr", dwCount);
}

去混淆方案2

混淆方案2等价于jmp指令.
但是混淆方案2的代码在内存中并不是连续的, 不好直接搜索特征来去除.
这里想到个办法, 使用x64dbg的TraceInto/TraceOver功能先跑出代码来, 再针对输出的log文件进行文本分析得到混淆方案2的地址.

 

x64dbg的TraceInto/TraceOver功能, 要输出到log的内容填上0x{p:cip} {i:cip}, 选择好输出log文件的路径, 设置步数500000(只能多不能少, 至少要跑完输出错误提示吧), 不用设置停止条件, 即可开始trace.

 

trace出来的文件大概是这样.

 

一眼望去, 代码里全是"去混淆方案1"中patch的jmp和成对出现的垃圾指令"push reg"/"pop reg".
写个python脚本优化掉(方法见"去混淆方案3").
去掉jmp和push reg/popreg的log文本文件中, 可以见到混淆方案2的代码已经连续出现了.

 

这里使用notepad++的插件python script写个脚本, 输出针对混淆方案2优化需要用到的几个重要参数(起始地址,callnext返回地址,异或运算常量,加法运算常量).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
a = editor.getText()
lst=a.split("\n")
 
lstdel=[]
 
for index in range(len(lst)-18):
#for index in range(20):
    i1=lst[index][-8:]
    i2=lst[index+1][-8:]
    i3=lst[index+2][-6:]
    i17=lst[index+17][-3:]
    #print(i1,i2,i3,i17)
    if(i1=="push rax" and i2=="push rax" and i3=="pushfq" and i17=="ret"):
        eva=lst[index][:18]
        cnva=lst[index+7][:18]
        xi=lst[index+8].rfind(" ");
        x=lst[index+8][xi:]
        ai=lst[index+13].rfind(" ");
        a=lst[index+13][ai:]
        print(eva, cnva, x, a)
 
print("finish.")

输出的地址特别多, 整理填入C代码中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LPCSTR g_aryFix[] = {
"0x00000001400B5E68", "0x000000014007FBD8", "0x6665", "0x36F74",
"0x000000014005AF49", "0x000000014002BB97", "0x28376", "0xCFCD7",
"0x00000001400E05AD", "0x0000000140040B81", "0xD7FA9", "0xFFFFFFFFFFFE178B",
"0x000000014000151A", "0x000000014008F26F", "0xDB257", "0x78C45",
"0x0000000140029C72", "0x000000014008F92C", "0xEB504", "0x328B9",
"0x000000014001E0A6", "0x00000001400F1BA1", "0x6545B", "0x28976",
"0x0000000140052153", "0x0000000140012134", "0x485D5", "0xFFFFFFFFFFFF07DE",
"0x0000000140083D5A", "0x000000014000F6CA", "0x43749", "0x46ACE",
"0x000000014002C47E", "0x00000001400C2EFA", "0xA5AD1", "0x89CB0",
"0x00000001400E7AEC", "0x00000001400370EC", "0xFEA16", "0xFFFFFFFFFFFD4BAC",
"0x00000001400C0C09", "0x00000001400F44A9", "0x2F746", "0xFFFFFFFFFFF323E7",
"0x000000014001533A", "0x0000000140020B70", "0x15CD7", "0x5E0A2",
"0x00000001400572A6", "0x0000000140059016", "0xFBD1", "0xFFFFFFFFFFFDF0F0",
"0x0000000140022F8A", "0x00000001400767A4", "0x4BE4C", "0x88F4",
//... ...
};

修复混淆方案2的代码(注意FA和RVA的转换):

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
void AntiObfuscation2()
{
    for (int i = 0; i < sizeof(g_aryFix) / sizeof(g_aryFix[0]) / 4 ;i++)
    {
        QWORD dwFindRVA = strtoull(g_aryFix[i * 4 ], 0, 16) - 0x140000000;
        QWORD dwFindFA = 0;
        if (dwFindRVA <  0x3400)
        {
            dwFindFA = (QWORD)g_lpFileBuffer + dwFindRVA - 0x1000 + 0x400;
        }
        else
        {
            dwFindFA = (QWORD)g_lpFileBuffer + dwFindRVA - 0xB000 + 0x6200;
        }
 
        QWORD dwCallNextRVA= strtoull(g_aryFix[i * 4+1], 0, 16) - 0x140000000;
 
        QWORD dwXor = strtoull(g_aryFix[i * 4 + 2], 0, 16);
        QWORD dwAdd = strtoull(g_aryFix[i * 4 + 3], 0, 16);
 
        QWORD dwNextAddrRVA = (dwXor ^ dwCallNextRVA) + dwAdd;
 
        BYTE bHook[] = { 0xE9, 00, 00, 00, 00 };
        Mem_WDword(bHook + 1, (DWORD)(dwNextAddrRVA - dwFindRVA - 5));
        Mem_WBytes2((LPVOID)dwFindFA, bHook, 5);
    }
}

去混淆方案3

混淆方案3是垃圾指令, 成对的push reg和pop reg.
写python脚本在log文本中搜索几次, 即可清理干净.
这里内存中就不patch了, 不是很影响看代码.
对python脚本输出的文件可以进行"去混淆方案2"和"去混淆方案4"操作.

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
a = editor.getText()
 
lst=a.split("\n")
print(len(lst));
 
lstdel=[]
 
for index in range(len(lst)):
    if(lst[index][-22:-19] == "jmp"):
        lstdel.append(index)
 
lstdel.reverse()
print(len(lstdel))
for index in range(len(lstdel)):
    del lst[lstdel[index]]
 
for fxcktime in range(10):
    lstdel=[]
    for index in range(len(lst)-1):
        i1=lst[index][-8:-4]
        i2=lst[index+1][-7:-4]
        reg1=lst[index][-3:]
        reg2=lst[index+1][-3:]
        if(i1=="push" and i2=="pop" and reg1==reg2):
            #print(i1,reg1,i2,reg2)
            lstdel.append(index)
            lstdel.append(index+1)
    lstdel.reverse()
    for index in range(len(lstdel)):
        del lst[lstdel[index]]
 
fo=open("D:\\Projects\\KCTF2021Spring\\CrackNote\\6\\crack\\crack\\pyoutput.txt","w")
for index in range(len(lst)):
    fo.write(lst[index]+"\n")
fo.close()
 
 
print("finish.");

去混淆方案4

混淆方案4等价于call指令.
混淆方案4和混淆方案2的差别, 就是最后一行一个是ret, 一个是jmp.

push addr+ret = jmp addr
push addr1+jmp addr2= call addr2 (addr1是call的返回地址)

 

在对trace的log进行完"去混淆指令3"和"去混淆指令2"操作后, 混淆方案4的代码就会"浮出水面", 在文本中连续.
继续写python脚本提取输出要处理的信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
a = editor.getText()
lst=a.split("\n")
 
lstdel=[]
 
for index in range(len(lst)-18):
    i1=lst[index][-8:]
    i2=lst[index+1][-8:]
    i3=lst[index+2][-6:]
    i17=lst[index+16][-7:]
    #print(i1,i2,i3,i17)
    if(i1=="push rax" and i2=="push rax" and i3=="pushfq" and i17=="pop rax"):
        eva=lst[index][:18]
        cnva=lst[index+7][:18]
        callva=lst[index+17][:18]
        xi=lst[index+8].rfind(" ");
        x=lst[index+8][xi:]
        ai=lst[index+13].rfind(" ");
        a=lst[index+13][ai:]
        print(eva, cnva, x, a, callva)
 
print("finish.")

输出的数据填入C代码中, 进行修复.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LPCSTR g_aryFixC[] = {
"0x000000014007A4C4", "0x0000000140089E51", "0xDC85F", "0x51438", "0x00000001400979D8",
"0x00000001400ADE90", "0x00000001400D4AEB", "0x4E2ED", "0xFFFFFFFFFFFD199D", "0x0000000140060882",
"0x00000001400C4678", "0x0000000140020DE5", "0xD2FE3", "0xFFFFFFFFFFFBE04E", "0x000000014005CC6E",
"0x00000001400C673D", "0x00000001400E7B10", "0xEB97C", "0x6DDA4", "0x000000014003F7D0",
"0x000000014003AF3E", "0x00000001400DDF75", "0x9704D", "0xFFFFFFFFFFFF5B08", "0x000000014003F7D0",
"0x00000001400839E3", "0x000000014007BECA", "0x36372", "0xA777F", "0x00000001400F90F3",
"0x00000001400543F0", "0x00000001400A164B", "0xAF7C5", "0x5CDEC", "0x0000000140078E09",
"0x00000001400998CB", "0x00000001400B928A", "0x2C92F", "0xE4AC", "0x00000001400D031B",
"0x000000014007C0AC", "0x000000014009B414", "0xC2AD8", "0xFFFFFFFFFFFC7598", "0x00000001400D42F1",
"0x000000014008FBE2", "0x0000000140047E12", "0x56408", "0x65F02", "0x00000001400728A0",
"0x000000014006B5D5", "0x00000001400194CA", "0x78E10", "0x3996", "0x0000000140078E09",
"0x0000000140077920", "0x00000001400A2374", "0x4FE21", "0xFFFFFFFFFFFCEA1A", "0x0000000140039068",
"0x00000001400943F5", "0x000000014008595F", "0xF3ADF", "0xFFFFFFFFFFFA3AAE", "0x00000001400DFCB3",
"0x000000014003768E", "0x000000014005087F", "0x45B11", "0xAB445", "0x000000014002650B",
//... ...
};

优化混淆方案4的方法:
1.起始地址jmp到返回地址-5
2.返回地址-5的地址,填入call指令

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
void AntiObfuscation3()
{
    for (int i = 0; i < sizeof(g_aryFixC) / sizeof(g_aryFixC[0]) / 5; i++)
    {
        QWORD dwFindRVA = strtoull(g_aryFixC[i * 5], 0, 16) - 0x140000000;
        QWORD dwFindFA = 0;
        if (dwFindRVA < 0x3400)
        {
            dwFindFA = (QWORD)g_lpFileBuffer + dwFindRVA - 0x1000 + 0x400;
        }
        else
        {
            dwFindFA = (QWORD)g_lpFileBuffer + dwFindRVA - 0xB000 + 0x6200;
        }
 
        QWORD dwCallNextRVA = strtoull(g_aryFixC[i * 5 + 1], 0, 16) - 0x140000000;
 
        QWORD dwXor = strtoull(g_aryFixC[i * 5 + 2], 0, 16);
        QWORD dwAdd = strtoull(g_aryFixC[i * 5 + 3], 0, 16);
 
        QWORD dwReturnRVA = (dwXor ^ dwCallNextRVA) + dwAdd;
 
        QWORD dwCallRVA= strtoull(g_aryFixC[i * 5+4], 0, 16) - 0x140000000;
 
        QWORD dwHookRVA = dwReturnRVA - 5 ;
        QWORD dwHookFA = 0;
        if (dwHookRVA < 0x3400)
        {
            dwHookFA = (QWORD)g_lpFileBuffer + dwHookRVA - 0x1000 + 0x400;
        }
        else
        {
            dwHookFA = (QWORD)g_lpFileBuffer + dwHookRVA - 0xB000 + 0x6200;
        }
 
        //BYTE nop[] = { 0x90,0x90,0x90,0x90,0x90 };
        //if (memcmp((LPBYTE)dwHookFA, nop, 5) != 0)
        //{
        //    printf("5 nops is not found.\r\n");
        //}
 
 
        BYTE bHook[] = { 0xE9, 00, 00, 00, 00 };
        Mem_WDword(bHook + 1, (DWORD)(dwHookRVA - dwFindRVA - 5));
        Mem_WBytes2((LPVOID)dwFindFA, bHook, 5);
 
 
        BYTE bHook2[] = { 0xE8, 00, 00, 00, 00 };
        Mem_WDword(bHook2 + 1, (DWORD)(dwCallRVA - dwHookRVA - 5));
        Mem_WBytes2((LPVOID)dwHookFA, bHook2, 5);
 
    }
}

0x02 分析程序的验证逻辑

去混淆过后的代码

经过优化代码混淆后, 看一下EntryPoint处代码.

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
0x0000000140071DE0 sub rsp, 0x28
0x00000001400A6A41 call 0x00000001400979D8
0x000000014000B2EA add rsp, 0x28
0x0000000140066D1D jmp 0x00000001400F4594
 
0x00000001400F4596 mov qword ptr ss:[rsp+0x8], rbx
0x00000001400574EC mov qword ptr ss:[rsp+0x10], rsi
0x000000014003AE64 push rdi
0x00000001400339F9 sub rsp, 0x30
0x00000001400B5EDB mov ecx, 0x1
0x000000014006C19E call 0x0000000140060882
0x0000000140052C16 test al, al
0x000000014002C7AC je 0x000000014002C7C5
0x00000001400FA123 xor sil, sil
0x00000001400503B8 mov byte ptr ss:[rsp+0x20], sil
0x00000001400F5532 call 0x00000001400F90F3
0x000000014005EB9A mov bl, al
0x000000014003B118 mov ecx, dword ptr ds:[0x00000001400066A0]
0x0000000140082181 cmp ecx, 0x1
0x00000001400F4B13 je 0x00000001400F4B2C
0x0000000140044567 test ecx, ecx
0x000000014000E80D jne 0x000000014000E826
0x000000014001938F mov dword ptr ds:[0x00000001400066A0], 0x1
0x00000001400768A6 lea rdx, ds:[0x0000000140004658]
0x00000001400D8E77 lea rcx, ds:[0x0000000140004640]
0x00000001400A404C call 0x00000001400D031B
0x000000014001A610 test eax, eax
0x00000001400BF926 je 0x00000001400BF93F
0x00000001400BBCE9 lea rdx, ds:[0x0000000140004638]
0x000000014003E227 lea rcx, ds:[0x0000000140004620]
0x000000014002145F call 0x00000001400D42F1
0x000000014007CF86 mov dword ptr ds:[0x00000001400066A0], 0x2
0x000000014004C6F2 mov cl, bl
0x0000000140077917 call 0x00000001400728A0
0x00000001400BC76A call 0x0000000140039068
0x0000000140046F51 mov rbx, rax
0x00000001400E073E cmp qword ptr ds:[rax], 0x0
0x00000001400A2DD5 je 0x00000001400A2DEE
0x0000000140019E29 call 0x00000001400DFCB3
0x0000000140019E30 mov rbx, rax
0x00000001400C7DA6 cmp qword ptr ds:[rax], 0x0
0x000000014007A883 je 0x000000014007A89C
0x00000001400C07AE call 0x000000014002650B                 ;_get_initial_narrow_environment
0x000000014008E432 mov rdi, rax
0x0000000140041D1A call 0x00000001400EED90                 ;__p___argv
0x0000000140041D23 mov rbx, qword ptr ds:[rax]
0x0000000140088748 call 0x0000000140068129                 ;__p___argc
0x0000000140092400 mov r8, rdi
0x000000014003D3AC mov rdx, rbx
0x0000000140059887 mov ecx, dword ptr ds:[rax]
0x00000001400A313B call 0x0000000140083814                 ;main

这代码看着就眼熟多了, 一股浓浓的VS201X编译器的味道.

 

main函数给定位到了:0x0000000140083814
下次trace就可以从main函数开始.

 

暂时想不到什么很好的办法能把代码全部按顺序修复好让ida能F5, 不过这反汇编代码已经看起来非常舒适了.

 

又多再看了几眼, 程序编译的时候没有开优化, 反汇编代码看起来又2更亲切了.

 

多亏笔者在武汉科锐培训三阶段的时候, 练就了看汇编代码还原C代码的"神功", 就当成是培训时间一次正常的作业吧.
^_^

main函数里程序的验证逻辑

首先是输出字符串到控制台和输入flag.
输入输出使用的std::cout和std::cin.

 

输入之后首先判断了输入字符串的长度(0x54=84).

1
2
3
4
5
6
7
8
0x0000000140083333 cmp rax, 0x54
0x0000000140035698 je 0x00000001400356B1
 
;输出错误提示
0x0000000140048E00 lea rcx, ds:[0x000000014000449C]
0x0000000140089A5C call 0x00000001400CD482
 
0x00000001400356B1 ....

接着为一块大小为0x150的局部变量数组初始化数值为0.

1
2
3
4
5
0x0000000140064B92 xor edx, edx
0x0000000140013BE4 lea rax, ss:[rsp+0xEC0]
0x000000014002469D mov rcx, rax
0x000000014004DA5D mov r8d, 0x150
0x000000014007C4C8 call 0x00000001400C2EA1 ;memset

观察对改数组的访问得到数组成员size为8字节, 算出总共42个成员, 数组定义如下.

1
QWORD ary[42] = {0};

循环往数组内写入数据.
写入数值=取一个字符转数字*0x2A+再取一个字符转数字.

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
;循环遍历输入的flag
0x00000001400462B9 mov qword ptr ss:[rsp+0xA8], 0x0        ;循环变量
0x0000000140054381 cmp qword ptr ss:[rsp+0xA8], 0x2A       ;循环次数=0x2A
0x000000014001603F jge 0x0000000140016058
0x0000000140036738 lea rax, ss:[rsp+0x1010]
0x00000001400B433E mov rcx, qword ptr ss:[rsp+0xA8]
0x00000001400F7366 shl rcx, 0x1
0x00000001400F9958 add rax, rcx
0x00000001400F5555 mov rcx, rax
0x0000000140014AC7 call 0x0000000140018FA3           ;计算数值
0x0000000140014ACC mov rcx, qword ptr ss:[rsp+0xA8]
0x00000001400787FE mov qword ptr ss:[rsp+rcx*8+0xEC0], rax     ;写入数组
0x00000001400AFE42 cmp qword ptr ss:[rsp+0xA8], 0x0
0x0000000140022C16 je 0x0000000140022C2F
0x00000001400297BA mov rax, qword ptr ss:[rsp+0xA8]
0x00000001400BCBD3 add rax, 0x1   ;循环变量+=1
0x000000014007F965 mov qword ptr ss:[rsp+0xA8], rax
0x0000000140054381 cmp qword ptr ss:[rsp+0xA8], 0x2A
0x000000014001603F jge 0x0000000140016058
;...进入下一次循环
 
;计算数值函数
0x000000014005828F sub rsp, 0x38
0x00000001400AB750 mov qword ptr ss:[rsp+0x30], rcx
0x00000001400466C5 mov rax, qword ptr ss:[rsp+0x30]
0x000000014003423A mov cl, byte ptr ds:[rax]
0x0000000140048352 call 0x00000001400706B9       ;字符转数字
0x00000001400BA991 imul rax, rax, 0x2A
0x00000001400EA00D mov rdx, qword ptr ss:[rsp+0x30]
0x0000000140039BC9 mov cl, byte ptr ds:[rdx+0x1]
0x000000014005C2D0 mov qword ptr ss:[rsp+0x28], rax
0x000000014008807A call 0x0000000140087B21       ;字符转数字
0x0000000140037CF1 mov rdx, qword ptr ss:[rsp+0x28]
0x00000001400A7DF8 add rdx, rax
0x00000001400D4395 mov rax, rdx
0x0000000140057C71 add rsp, 0x38
0x00000001400DA66E ret
 
;字符转数字函数(部分)
0x00000001400D5E04 sub rsp, 0x10
0x00000001400731D7 mov byte ptr ss:[rsp+0x7], cl
0x00000001400882CA movsx eax, byte ptr ss:[rsp+0x7]
0x00000001400938D7 cmp eax, 0x30
0x000000014009A8BF jl 0x000000014009A8D8
0x00000001400B54F4 movsx eax, byte ptr ss:[rsp+0x7]
0x000000014001A372 cmp eax, 0x39  ;'9'
0x00000001400794C2 jg 0x00000001400794DB
0x0000000140043279 movsx rax, byte ptr ss:[rsp+0x7]
0x0000000140069CAB sub rax, 0x30   ;'0'
0x000000014002E028 mov qword ptr ss:[rsp+0x8], rax
0x000000014008BBCC mov rax, qword ptr ss:[rsp+0x8]
0x00000001400831F7 add rsp, 0x10
0x000000014002974F ret

仔细分析字符转数字函数, 得出输入总共有42个字符, 分别是:

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-*/%=

 

下标就是这个字符转换出来的数值.

 

接着循环判断数组QWORD ary[42]中以从小到大顺序排序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x0000000140014ACC mov rcx, qword ptr ss:[rsp+0xA8]
0x00000001400787FE mov qword ptr ss:[rsp+rcx*8+0xEC0], rax
0x00000001400AFE42 cmp qword ptr ss:[rsp+0xA8], 0x0
0x0000000140022C16 je 0x0000000140022C2F
0x00000001400CD021 mov rax, qword ptr ss:[rsp+0xA8]
0x00000001400EA72C mov rax, qword ptr ss:[rsp+rax*8+0xEC0]
0x00000001400ABE1C mov rcx, qword ptr ss:[rsp+0xA8]
0x00000001400E81E3 sub rcx, 0x1
0x00000001400906DB cmp rax, qword ptr ss:[rsp+rcx*8+0xEC0]
0x00000001400A9B93 jg 0x00000001400A9BAC
0x00000001400297BA mov rax, qword ptr ss:[rsp+0xA8]
0x00000001400BCBD3 add rax, 0x1
0x000000014007F965 mov qword ptr ss:[rsp+0xA8], rax
0x0000000140054381 cmp qword ptr ss:[rsp+0xA8], 0x2A
0x000000014001603F jge 0x0000000140016058

这里说明flag偶数下标的数值是限制死的.
只能是"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-*/%="依次排序.

 

接着一个check, 循环判断了输入的偶数位和奇数位字符都不一样.

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
;初始化两个数组char ary[42],分别记录偶数位和奇数位每个字符是否出现
0x0000000140001A0A xor eax, eax
0x00000001400CCBF8 lea rcx, ss:[rsp+0xE90]
0x000000014004662E mov edx, eax
0x000000014004A3B8 mov r8d, 0x2A
0x000000014006EA24 mov qword ptr ss:[rsp+0x48], r8
0x0000000140098B05 mov dword ptr ss:[rsp+0x44], eax
                   ;call memset
0x00000001400C9BCF jmp qword ptr ds:[0x0000000140004BD8]
 
0x00000001400930C5 lea rcx, ss:[rsp+0xE60]
0x00000001400EE6C6 mov edx, dword ptr ss:[rsp+0x44]
0x00000001400450F9 mov r8, qword ptr ss:[rsp+0x48]
                   ;call memset
0x00000001400C9BCF jmp qword ptr ds:[0x0000000140004BD8]
 
;循环check
0x000000014007D9D5 mov qword ptr ss:[rsp+0xA0], 0x0 ;循环变量
0x0000000140002310 cmp qword ptr ss:[rsp+0xA0], 0x2A ;循环次数=2A
0x0000000140003980 jge 0x0000000140003999
;对0x2A一个求模一个整除, 等于是拿出了偶数位和奇数位数值
0x0000000140001F98 mov rax, qword ptr ss:[rsp+0xA0]
0x000000014002D5AC mov rax, qword ptr ss:[rsp+rax*8+0xEC0]
0x0000000140046C25 cqo
0x0000000140073E00 mov ecx, 0x2A
0x000000014009EA4F idiv rcx
0x000000014006EC35 mov qword ptr ss:[rsp+0x98], rdx
0x0000000140060863 mov rdx, qword ptr ss:[rsp+0xA0]
0x0000000140063300 mov rdx, qword ptr ss:[rsp+rdx*8+0xEC0]
0x0000000140017083 mov rax, rdx
0x00000001400C5CFF cqo
0x00000001400BCF6C idiv rcx
0x000000014008CCE3 mov qword ptr ss:[rsp+0x90], rax
0x00000001400BF3EF mov rax, qword ptr ss:[rsp+0x98]
;判断这个数值是否已经存在
0x000000014004B8F8 test byte ptr ss:[rsp+rax*1+0xE90], 0x1
0x00000001400ECE6A jne 0x00000001400ECE83
0x000000014005224C mov rax, qword ptr ss:[rsp+0x90]
0x0000000140015F89 test byte ptr ss:[rsp+rax*1+0xE60], 0x1
0x000000014006633F je 0x0000000140066358
;填入两个数组,记录当前已出现的数值
0x0000000140027ACE mov rax, qword ptr ss:[rsp+0x98]
0x00000001400E4C50 mov byte ptr ss:[rsp+rax*1+0xE90], 0x1
0x00000001400AC622 mov rax, qword ptr ss:[rsp+0x90]
0x00000001400B5248 mov byte ptr ss:[rsp+rax*1+0xE60], 0x1
0x0000000140037CD0 mov rax, qword ptr ss:[rsp+0xA0]
0x0000000140026ABB add rax, 0x1
0x00000001400258E6 mov qword ptr ss:[rsp+0xA0], rax
0x0000000140002310 cmp qword ptr ss:[rsp+0xA0], 0x2A
0x0000000140003980 jge 0x0000000140003999
;进入下一次循环

这里可以得到信息,奇数位的中每个字符都只出现一次.

 

接着下一个check, 嵌套的循环.
大概逻辑是, 相邻的偶数位和奇数位为一组, 对于每两组数据差得到的一个坐标都不能相同(这里形容得不太准确,没有太理解,直接把反汇编代码翻译成C代码写个check函数即可).

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
;初始化数组, size=1743
;@@@这里发现个问题, 后面给这个数组填数值的时候越界了
;@@@在这里纠结了很久, 照着代码抄下, 假装没看到数组访问越界就过了
0x000000014006B00F xor edx, edx
0x0000000140088714 lea rax, ss:[rsp+0xC0]
0x00000001400CA071 mov rcx, rax
0x0000000140062801 mov r8d, 0xD9E  ;1743
                    call memset
0x00000001400C9BCF jmp qword ptr ds:[0x0000000140004BD8]
 
;嵌套的循环开始, 两两比较
;for (int i = 0; i < 42; i++)
0x00000001400C86F5 mov qword ptr ss:[rsp+0x88], 0x0 
0x00000001400D320E cmp qword ptr ss:[rsp+0x88], 0x2A
0x0000000140056B2F jge 0x0000000140056B48
;for (int j = i+1; j < 42;j++)
0x00000001400CBE10 mov rax, qword ptr ss:[rsp+0x88]  
0x0000000140052E9D add rax, 0x1
0x0000000140078F7F mov qword ptr ss:[rsp+0x80], rax
0x00000001400286DB cmp qword ptr ss:[rsp+0x80], 0x2A
0x000000014000D91A jge 0x000000014000D933
 
0x00000001400F755C mov rax, qword ptr ss:[rsp+0x88]      
0x0000000140098656 mov rax, qword ptr ss:[rsp+rax*8+0xEC0]   
0x0000000140001584 cqo
0x00000001400A4ADC mov ecx, 0x2A
0x000000014001F9E4 idiv rcx
0x000000014009D9EA mov r8, qword ptr ss:[rsp+0x80]
0x00000001400EDA1E mov r8, qword ptr ss:[rsp+r8*8+0xEC0]
0x000000014000EA19 mov rax, r8
0x00000001400A04C3 mov qword ptr ss:[rsp+0x38], rdx          ;rsp_38 = ary[i]%42
0x0000000140037898 cqo
0x000000014008A550 idiv rcx
0x00000001400399F2 mov r8, qword ptr ss:[rsp+0x38]           ;r8 = ary[j]%42
0x00000001400D4106 sub r8, rdx                               ;r8 = ary[j]%42- ary[i]%42
0x00000001400901F7 mov qword ptr ss:[rsp+0x78], r8
0x000000014009DAAD mov rdx, qword ptr ss:[rsp+0x88]
0x000000014000E4E3 mov rdx, qword ptr ss:[rsp+rdx*8+0xEC0]
0x00000001400EE409 mov rax, rdx
0x00000001400C5EE9 cqo
0x0000000140020832 idiv rcx
0x000000014008C074 mov r8, qword ptr ss:[rsp+0x80]
0x000000014006079F mov r8, qword ptr ss:[rsp+r8*8+0xEC0]
0x0000000140013395 mov qword ptr ss:[rsp+0x30], rax
0x00000001400C5CE1 mov rax, r8
0x000000014002FFF8 cqo
0x000000014007539E idiv rcx
0x000000014006BF8A mov rcx, qword ptr ss:[rsp+0x30]
0x00000001400DB4A6 sub rcx, rax                            ;rcx = ary[j]/42- ary[i]/42
0x00000001400E127D mov qword ptr ss:[rsp+0x70], rcx
 
;if(ary[j]%42- ary[i]%42 > 0)
0x0000000140073B88 cmp qword ptr ss:[rsp+0x78], 0x0       
0x000000014006437E jge 0x0000000140064397
 
; 负数 则 取反  
0x00000001400BFF7E xor eax, eax
0x00000001400EACB4 mov ecx, eax
0x0000000140080A4A mov rdx, rcx                         ;
0x00000001400A7967 sub rdx, qword ptr ss:[rsp+0x78]          
0x00000001400517AC mov qword ptr ss:[rsp+0x78], rdx
0x000000014000B3C6 sub rcx, qword ptr ss:[rsp+0x70]
0x000000014008DA3A mov qword ptr ss:[rsp+0x70], rcx    
0x0000000140064397:
 
;ary[j]/42 -ary[i]/42 +0x29
0x00000001400B368C mov rax, qword ptr ss:[rsp+0x70]          
0x0000000140079E70 add rax, 0x29                  
0x00000001400DE855 mov qword ptr ss:[rsp+0x70], rax
 
;这里看出是个二维数组访问
;说明上面初始化的数组定义为char ary[?][42]
0x000000014007C0C8 imul rax, qword ptr ss:[rsp+0x70], 0x2A
0x00000001400DC9B8 lea rcx, ss:[rsp+0xC0]
0x0000000140038FE8 add rcx, rax
0x0000000140067202 mov rax, qword ptr ss:[rsp+0x78]
; 判断不为1
0x000000014008B8FD test byte ptr ds:[rcx+rax*1], 0x1
0x0000000140063B11 je 0x0000000140063B2A
 
; 填1
0x00000001400F9BFC imul rax, qword ptr ss:[rsp+0x70], 0x2A 
0x000000014007F148 lea rcx, ss:[rsp+0xC0]
0x00000001400EB582 add rcx, rax
0x0000000140065E6F mov rax, qword ptr ss:[rsp+0x78]
0x00000001400F535F mov byte ptr ds:[rcx+rax*1], 0x1
 
; j++
0x0000000140013FC1 mov rax, qword ptr ss:[rsp+0x80]   
0x0000000140073E8D add rax, 0x1
0x00000001400B9738 mov qword ptr ss:[rsp+0x80], rax
 
0x00000001400286DB cmp qword ptr ss:[rsp+0x80], 0x2A
0x000000014000D91A jge 0x000000014000D933
;进入下一次循环

这里check逻辑虽然看得不是太懂, 但是突然产生个脑洞.
有点像n皇后问题, 使用回溯法写代码可以解决.
但是看这规模, 跑出flag来估计也要个很长时间, 不太现实.
继续看后面有没有check可以缩小穷举范围.

 

一看果然有发现:

1
2
3
4
5
;strncmp
0x0000000140039540 lea rcx, ss:[rsp+0x1010]
0x00000001400CE591 lea rdx, ds:[0x0000000140004521]
0x000000014004CACF mov r8d, 0x1C
0x000000014004FED9 call qword ptr ds:[0x0000000140004C98]

[rsp+0x1010]是输入的flag
[0x0000000140004521]是"02152S3X4Z5Q6C7T819/ADB%C*DL"

 

也就是说,flag的前面28个字符已经给定.
范围已经缩到最小, 可以写代码进行穷举了.

0x03 回溯法穷举flag

百度抄下个n皇后回溯法求解的代码, 修改下check函数和输入输出.

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
int n = 0;
 
#define BORDER 42
char board[BORDER] = { 0 };
 
char tb[200][42] = { 0 };
 
char x[2000] = { 0 };
char y[2000] = { 0 };
 
bool check(char row, char col)
{
    for (char i = 0; i < row; i++)
    {
        if (board[i] == col)
        {
            return false;
        }
    }
 
    short sum = 0;
    for (char i = 0; i < row; i++)
    {
        for (char j = i+1; j < row; j++)
        {
            int tmp1 = j - i;
            int tmp2 = board[j] - board[i];
 
            if (tmp2 < 0)
            {
                tmp1 = 0 - tmp1;
                tmp2 = 0 - tmp2;
            }
 
            tmp1 = tmp1 + 0x29;
 
            x[sum] = tmp1;
            y[sum] = tmp2;
            sum++;
        }
    }
 
 
    memset(tb, 0, sizeof(tb));
    for (short j = 0; j < sum; j++)
    {
        tb[x[j]][y[j]] = 1;
    }
 
 
    for (char i = 0; i < row; i++)
    {
        //(i, board[i])
        //(row, col)
 
        int tmp1 = row - i;
        int tmp2 = col - board[i];
 
        if (tmp2 < 0)
        {
            tmp1 = 0 - tmp1;
            tmp2 = 0 - tmp2;
        }
 
        tmp1 = tmp1 + 0x29;
 
        if (tb[tmp1][tmp2] == 1)
        {
            return false;
        }
    }
 
    return true;
}
 
void fxck(char row)
{
    if (row == BORDER)
    {
        //output
        n++;
        printf("%d: 02152S3X4Z5Q6C7T819/ADB%cC*DL", n, '%');
        for (int i = 14; i < BORDER; i++)
        {
            printf("%c%c", getccc(i), getccc(board[i]));
        }
 
        printf("\r\n");
        return;
    }
 
    for (char col = 0; col < BORDER; col++)
    {
        if (check(row, col))
        {
            board[row] = col;
            fxck(row + 1);
        }
    }
}
 
int main(int argc, const char** argv, const char** envp)
{
    //"02152S3X4Z5Q6C7T819/ADB%C*DL"
    LPCSTR yz = "25SXZQCT1/D%*L";
 
    for(int i = 0; i< 14;i++)
    {
        auto idx= getidx(yz[i]);
        board[i] = idx;
    }
 
    fxck(14);
 
    printf("n = %d \r\n", n);
    return 0;
}

运行出结果需要一段时间, 一两分钟吧, 不算太久.

1
2
3
4
5
6
7
8
Microsoft Windows [版本 10.0.19042.985]
(c) Microsoft Corporation。保留所有权利。
 
C:\Users\KuCha\Desktop\crack_login\x64\Release>crack_login.exe
1: 02152S3X4Z5Q6C7T819/ADB%C*DLEIFUG3HRIHJ6K7L0MBNKOJPPQ=RNS+TEUOVWWGXYYMZ9+4-8*F/-%V=A
n = 1
 
C:\Users\KuCha\Desktop\crack_login\x64\Release>

02152S3X4Z5Q6C7T819/ADB%CDLEIFUG3HRIHJ6K7L0MBNKOJPPQ=RNS+TEUOVWWGXYYMZ9+4-8F/-%V=A

0x04 总结

题目主打还是代码流, 很合笔者胃口.
题目算法验证部分最后一个循环check中, 有数组越界访问, 这个很坑, 不按题目算法这样写代码得不到flag.
混淆方法太少, 使得攻击者要恢复代码工作量不算太大.

 

附件文件:
crack_login.cpp是修复混淆和最后回溯穷举的代码.
KCTF_patch8.exe是优化部分混淆过后的题目可执行文件.
python script.rar是notepad++插件的python脚本.


 

本人目前待业中, 主要擅长windows的逆向, 研究最多的是代码流的防御和攻击手段, 有公司需要人嘛.
有意的大佬可以在CTF群里联系我, 群备注就是论坛名.


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-5-19 16:16 被KuCha128编辑 ,原因:
上传的附件:
收藏
点赞5
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回