首页
社区
课程
招聘
[原创]ACprotect 1.? 脱壳 + 写x64dbg插件
发表于: 2021-4-9 02:08 7614

[原创]ACprotect 1.? 脱壳 + 写x64dbg插件

2021-4-9 02:08
7614

目标程序https://bbs.pediy.com/thread-30330.htm
平台win7
用ExeinfoPe查壳,显示为AC protect 1.?
用x64dbg运行,调试器直接就退出了,显然有反调试,开始分析

反调试

x64dbg先设置异常,如下图 (注:新版本,不同的异常,可以单独设置)
Unkonwn exceptions可以理解为,除了已经设置过的异常,另外的其他异常
如果没有单独设置异常,Unkonwn exceptions表示的是全部(00000000~FFFFFFFF)
所以下图的意思是,全部异常(Unknown exceptions),都不忽略(暂停于第一次机会),都自己处理(异常处理者调试器)

 

既然退出了,先在退出相关的api下断点
所有模块中搜索,带exit的全部F2下断点,然后运行

 

停在RtlReportSilentProcessExit上,此时看栈的返回地址
760FE5A1转到反汇编,是TerminateProcess调用的RtlReportSilentProcessExit
00649E20跟进,调用了TerminateProcess

 

0064D3D8跟进,call上面有个比较,等于0,就可以跳过调用TerminateProcess的函数
能看出是CreateToolhelp32Snapshot的地址,此处只是当用于判断的标志(实际是遍历进程,对比父进程ID)
0064D3C6下硬件断点,重新运行,停下时,把值清0或修改jcc标志,继续F9运行

 

此时,会停在异常,栈中能看出有seh链,跟进函数
①处,eip+1 ②处,删除硬件断点
这个异常自己处理,把0064B396设置为新的运行点(其实也可以直接F9)
如果用的不是x64dbg的新版本,就在回调函数中的jcc下F2断点,运行,双击修改jcc标志
注:这个803异常,不一定能遇到,时有时无
继续F9运行

 

遇到异常,seh跟进,只有eip+2
把0064B090设置为新的运行点,继续F9运行

 

此时会遇到异常(注:随机2种情况:栈中没有返回地址 或 栈中有返回地址)
1.栈中没有返回地址
代码向上翻,找能跳过eip的jcc,找到后下硬件断点,可以多下几个断点
注:可能有不少都满足条件,但是要找比较靠上的(低地址的)

还有3个硬件断点可用,所以我下了3个地址能跳过的
①处,翻找的过程中,已经能发现其他的反调试方式了,先不理会

 

下好断点后,重新运行,重复最初的步骤,直到停在断点0064B599,按F9继续执行
只要还停在0064B599上,就继续F9,此处的断点不重要,因为上面的call是取随机数的
为了方便和腾出一个硬件断点,直接删除断点,F9运行
还会遇到之前说的2种情况的异常(栈中没有返回地址 或 栈中有返回地址)

 

栈中没有返回地址(此时停在0064B5BB上,如果再F9运行的话,就是没返回地址的)
eax不等于0时,会跳走,双击修改zf标志为1 (此处的代码,下面会总结)
再F9运行,就会变成 栈中有返回地址 的情况

 


2.栈中有返回地址

跟进第一个返回地址0064B949,向上找能跳过0064B949地址的jcc
找到2个,0064B87E上面的call是取随机数,所以这个jcc忽略
①处,看到调用了IsDebuggerPresent,所以在0064B893下硬件断点
注:②处,其实还有别的情况,只不过极少能遇到(写笔记时,一直没复现出来)

上面下好断点后,重新运行程序,重复步骤到断点上
遇到0064B5BB和0064B893时,就改跳转的标志,然后F9运行
之后有可能再次遇到803异常,按之前的方式处理,再F9运行

遇到异常,此时选项里设置异常

不暂停(忽略所有异常)
异常处理者为被调试对象(异常处理交给程序)
F9运行,程序就能正常跑起来了

小总结:
壳的反调试主要2个部分
1.用CreateToolhelp32Snapshot遍历进程,对比父进程ID
2.用IsDebuggerPresent拿Flags 和 壳自己拿peb中的Flags
IsDebuggerPresent

windbg查看peb

 

反调试部分结束
为了方便之后的调试,我会给x64dbg写个插件,自动处理这2种反调试
写插件的部分,最后再写,先写脱壳的部分,下面的内容,默认已经没有反调试了

获取IAT

重新加载程序
在GetProcAddress下硬件断点
停下后,注意eax有残留的函数名称信息
点击运行到用户代码

 

到程序代码后

1
2
3
4
5
6
7
8
00648E0E | FF | call dword ptr ss:[ebp+41C268]      |;GetProcAddress
00648E14 | E9 | jmp calories.648EC6                 |;eax == api地址
...
00648EC6 | 5A | pop edx                             |;jmp到这
00648EC7 | 89 | mov dword ptr ss:[ebp+edx],eax      |;eax存入某内存 //很像iat,实际是壳使用的api
00648ECB | 58 | pop eax                             |
00648ECC | 5B | pop ebx                             |
00648ECD | C3 | ret                                 |;函数返回

函数返回后
看到一些相同call,给eax和edx值,然后call

1
2
3
4
5
6
7
8
9
10
11
12
0064C091 | B8 | mov eax,calories.404372             |
0064C096 | BA | mov edx,calories.4044E1             |
0064C09B | E8 | call calories.648DF6                |
0064C0A0 | B8 | mov eax,calories.40437E             |
0064C0A5 | BA | mov edx,calories.4044E5             |
0064C0AA | E8 | call calories.648DF6                |
...
0064C1AE | B8 | mov eax,calories.404581             |
0064C1B3 | BA | mov edx,calories.40457D             |
0064C1B8 | E8 | call calories.648DF6                |
0064C1BD | 83 | cmp dword ptr ss:[ebp+4020F1],0     |
0064C1C4 | 74 | je calories.64C1EA                  |

进call看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
00648DF6 | 53 | push ebx                            |
00648DF7 | 50 | push eax                            |
00648DF8 | 52 | push edx                            |
00648DF9 | 03 | add eax,ebp                         |;算出api名称的地址
00648DFB | 50 | push eax                            |
00648DFC | 53 | push ebx                            |
00648DFD | 50 | push eax                            |
00648DFE | 8B | mov eax,dword ptr ss:[ebp+41C268]   |
00648E04 | 80 | cmp byte ptr ds:[eax],CC            |;检测CC断点
00648E07 | 74 | je calories.648E19                  |
00648E09 | 90 | nop                                 |
00648E0A | 90 | nop                                 |
00648E0B | 90 | nop                                 |
00648E0C | 90 | nop                                 |
00648E0D | 58 | pop eax                             |;api名称弹到eax中,所以eax有残留
00648E0E | FF | call dword ptr ss:[ebp+41C268]      |;GetProcAddress 下面的之前分析过了
00648E14 | E9 | jmp calories.648EC6                 |
...
00648EC6 | 5A | pop edx                             |
00648EC7 | 89 | mov dword ptr ss:[ebp+edx],eax      |
00648ECB | 58 | pop eax                             |
00648ECC | 5B | pop ebx                             |
00648ECD | C3 | ret                                 |

不停的F9,观察eax
当eax不再是api名时,表示已经是另一段代码调用了GetProcAddress
ebx是api名了,返回到程序代码,再观察

返回后

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
0064E319 | FF | call dword ptr ss:[ebp+41C268]     |;GetProcAddress
0064E31F | 3B | cmp ebx,dword ptr ss:[ebp+404028]  |;观察返回值eax去了哪里
0064E325 | 7C | jl calories.64E336                 |
0064E327 | 90 | nop                                |
0064E328 | 90 | nop                                |
0064E329 | 90 | nop                                |
0064E32A | 90 | nop                                |
0064E32B | 60 | pushad                             |;保存环境
0064E32C | 2B | sub eax,eax                        |
0064E32E | 88 | mov byte ptr ds:[ebx],al           |
0064E330 | 43 | inc ebx                            |
0064E331 | 38 | cmp byte ptr ds:[ebx],al           |
0064E333 | 75 | jne calories.64E32E                |
0064E335 | 61 | popad                              |;恢复环境
0064E336 | 0B | or eax,eax                         |
0064E338 | 0F | je calories.64E26C                 |;检查返回值eax是否为空
0064E33E | 3B | cmp eax,dword ptr ss:[ebp+41C278]  |;不知道在对比什么,不理会,看eax去向
0064E344 | 75 | jne calories.64E350                |;跳走
...
0064E350 | 56 | push esi                           |;到这
0064E351 | FF | push dword ptr ss:[ebp+404020]     |
0064E357 | 5E | pop esi                            |
0064E358 | 39 | cmp dword ptr ss:[ebp+4020E9],esi  |
0064E35E | 74 | je calories.64E375                 |;跳走
...
0064E375 | 80 | cmp byte ptr ss:[ebp+40A387],0     |;到这
0064E37C | 74 | je calories.64E3D2                 |
0064E37E | 90 | nop                                |
0064E37F | 90 | nop                                |
0064E380 | 90 | nop                                |
0064E381 | 90 | nop                                |
0064E382 | EB | jmp calories.64E38B                |;跳走
...
0064E38B | 8B | mov esi,dword ptr ss:[ebp+4040ED]  |;到这
0064E391 | 83 | add esi,D                          |
0064E394 | 81 | sub esi,calories.401FC7            |
0064E39A | 2B | sub esi,ebp                        |
0064E39C | 83 | cmp esi,0                          |
0064E39F | 7F | jg calories.64E3D2                 |
0064E3A1 | 90 | nop                                |
0064E3A2 | 90 | nop                                |
0064E3A3 | 90 | nop                                |
0064E3A4 | 90 | nop                                |
0064E3A5 | 8B | mov esi,dword ptr ss:[ebp+4040ED]  |
0064E3AB | 53 | push ebx                           |;保存环境
0064E3AC | 50 | push eax                           |;保存环境
0064E3AD | 0F | rdtsc                              |;取"随机值",ebx和eax被改变
0064E3AF | 8B | mov ebx,eax                        |
0064E3B1 | 58 | pop eax                            |;恢复环境 eax = api地址
0064E3B2 | 33 | xor eax,ebx                        |;异或加密 api地址
0064E3B4 | C6 | mov byte ptr ds:[esi],68           |;写机器码push
0064E3B7 | 89 | mov dword ptr ds:[esi+1],eax       |;加密后的 api地址写机器码
0064E3BA | C7 | mov dword ptr ds:[esi+5],243481    |
0064E3C1 | 89 | mov dword ptr ds:[esi+8],ebx       |;写解密的值
0064E3C4 | C6 | mov byte ptr ds:[esi+C],C3         |;写机器码 ret
0064E3C8 | 5B | pop ebx                            |;恢复环境
0064E3C9 | 8B | mov eax,esi                        |;eax = esi //esi是刚刚写的代码地址
0064E3CB | 83 | add dword ptr ss:[ebp+4040ED],D    |
0064E3D2 | 5E | pop esi                            |
0064E3D3 | 89 | mov dword ptr ds:[edi],eax         |;eax写入iat
0064E3D5 | 83 | add dword ptr ss:[ebp+404024],4    |

写入的代码,如图,可以看出是利用了ep处的空间
解密出api地址,通过ret跳到api

看iat的内存,转成地址类型查看,能看出被替换成了上面的函数

 

从上面的分析中,就可以获取真正的iat了,在0064E3AB下硬件断点,重新运行程序,停在这个断点
把0064E3AB到0064E3C9的代码,nop掉,eax中的api地址,就会直接写入edi的iat地址


获取IAT的部分结束
要dump的时候,按上面的方法处理一下就可以了

寻找oep

开始找oep,重载程序,esp定律,F9
停在

1
2
3
4
5
6
7
8
9
0065FAF2 | 61 | popad                              |
0065FAF3 | 55 | push ebp                           |;被偷的oep代码
0065FAF4 | 8B | mov ebp,esp                        |;被偷的oep代码
0065FAF6 | 90 | nop                                |
0065FAF7 | 90 | nop                                |
0065FAF8 | 90 | nop                                |
0065FAF9 | 60 | pushad                             |;执行完这一行,再esp定律,F9
0065FAFA | 60 | pushad                             |
0065FAFB | E8 | call calories.65FB00               |

停下

1
2
3
4
0065FE8E | 61 | popad                              |
0065FE8F | 60 | pushad                             |;eip停在这,popad和pushad抵消,继续F9
0065FE90 | E8 | call calories.65FE95               |
0065FE95 | 5E | pop esi                            |

停下

1
2
3
4
5
6
7
8
9
10
0065FECB | EB | jmp calories.65FECE                |;跳走
0065FECD | E9 | jmp FF3A24D1                       |
...
0065FECE | FF | jmp dword ptr ds:[65FED4]          |;到这,再跳走
..
0056949B | B9 | mov ecx,4                          |;到达oep
005694A0 | 6A | push 0                             |
005694A2 | 6A | push 0                             |
005694A4 | 49 | dec ecx                            |
005694A5 | 75 | jne calories.5694A0                |

寻找oep的部分结束
记录好信息,dump时使用

Dump

梳理一下现在的情况
反调试,已经通过插件处理
iat处理的地址和方法已经确定
oep的地址和被偷的oep代码也有了

 

重载程序
0064E3AB下硬件断点,停下时nop代码,获取iat
0056949B下硬件断点,停下时,恢复oep(0056949B前面修改机器码)
先设置00569498为新的eip,再dump,这样不用之后再修正PE格式中的入口点

检查oep是否正确,dump文件
IAT Autosearch获取iat的va和size
Get Imports获取iat,有2条无效的,直接删除就行了
fix dump修复刚刚dump出的文件

Dump的部分结束

修复Code Splicing

dump出的程序,运行出错,载入看看(异常设置为,全部都不忽略,自己处理)
F9运行,停在下图的情况,开始分析

 

到原程序(未脱壳的)的006461D9看看,如下图
其实就是把代码放到了申请的内存上,dump时会少一部分代码,所以运行出错
观察代码,非常有规律,下面会分析

来源https://bbs.pediy.com/thread-17253.htm

 

开始修复
在申请的内存上转到内存布局中

把这块内存转存成文件

用CFF_Explorer打开原来dump出来的程序(修复后的Calories_dump_SCY)
把转存出来的内存,加到文件中

内存属性修改成相同的

 

分析规律
原程序中

1
2
3
4
5
6
7
006460F5 | FF25 28E37400   | jmp dword ptr ds:[74E328]          |;开始
006460FB | FF25 2CE37400   | jmp dword ptr ds:[74E32C]          |
...
00647859 | FF25 C0F27400   | jmp dword ptr ds:[74F2C0]          |
0064785F | FF25 C4F27400   | jmp dword ptr ds:[74F2C4]          |;结束
 
0064785F - 006460F5 = 176A //尾 减 头
1
2
3
176Ah == 5994d
5994 + 6 = 6000            //6是最后一条指令的机器码字节数
6000 / 6 = 1000              //6是每一条指令的机器码字节数 //1000条jmp
1
2
3
4
5
6
006460F5跳过去的地址
0074F2C8 | 8B7E 0C         | mov edi,dword ptr ds:[esi+C]       |
0064785F跳过去的地址
00750A32 | 74 74            | je 750AA8                          |
 
00750A32 - 0074F2C8 = 176A //尾 减 头

观察总结
都是连续的
步长都是固定的
一一对应的
结论:每个jmp跳到目标地址的偏移,都是固定的

 

计算偏移值,找到dump程序中jmp对应的地址
从原程序中的这些jmp中,找一条分析一下对应的地址

1
2
3
4
5
6
7
8
9
10
11
006460FB | FF25 2CE37400    | jmp dword ptr ds:[74E32C]     |;[0074E32C]=0074F2CE
 
006460FB的jmp,会跳到申请内存中的0074F2CE
申请内存的信息
地址=00720000 ;基址
大小=00085000
分配类型=PRV
当前保护=-RW--
初始保护=-RW--
 
0074F2CE - 00720000=2F2CE

到dump出的程序中,看后来添加的节的信息

1
2
3
4
5
6
7
8
地址=0066A000 ;基址
大小=00085000
页面信息="12345678"
分配类型=IMG
当前保护=ERWC-
初始保护=ERWC-
 
0066A000 + 2F2CE = 6992CE

修改dump程序,获取机器码
原本的指令是间接jmp [xxxxxxxx]
修改成直接jmp xxxxxxxx
得到机器码 E9 CE 31 05 00 90 可以去修改文件了

 

va转fa (文件要修改的地址)

 

写程序修改文件,毕竟量太多了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <Windows.h>
int main()
{
    HANDLE hFile = CreateFile(
        L"C:\\Users\\win7\\Desktop\\ceshi.exe",
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 
    SetFilePointer(hFile, 0x2072F5, NULL, FILE_BEGIN);
 
    unsigned char buf[6] = { 0xE9 ,0xCE ,0x31 ,0x05 ,0x00, 0x90 };
    DWORD retBytes;
    for (int i = 0; i < 1000; i++)
    {
        WriteFile(hFile, buf, 6, &retBytes, NULL);
    }
 
    CloseHandle(hFile);
    return 0;
}

修正Code Splicing后
程序运行成功,查壳为Delphi

 

脱壳部分结束

写x64dbg插件

要过掉2个地方
1.遍历进程 CreateToolhelp32Snapshot Process32First Process32Next
2.PEB的调试标志

 

看看OD插件HideOD是怎么做的,跟着学就行了

 

HookApi在Process32NextW入口写
xor eax,eax
ret 8
直接让函数返回0

 

段选择子拿线性地址
teb地址+30h拿peb,再+2,BeingDebugged清0
注:x64dbg提供了这种功能,不用自己拼gdt

 

方法有了,开始写插件
https://help.x64dbg.com/ 手册
x64dbg里面有pluginsdk
下载模板https://github.com/x64dbg/PluginTemplate

 

vs新建dll工程,添加模板中的文件到工程中
plugin.h
plugin.cpp
pluginmain.h
pluginmain.cpp
pluginconfig.h //pluginconfig.h.in这个文件改的

 

设置vs
预编译头改为不使用
包含目录和库目录,添加x64dbg的目录 如E:\x64dbg
目标文件扩展名改为.dp32或.dp64
输出目录改为插件目录 如E:\x64dbg\release\x32\plugins\
调试的命令改为x64dbg的程序 如E:\x64dbg\release\x32\x32dbg.exe

 

代码如下

1
2
3
4
5
pluginconfig.h
#define PLUGIN_NAME u8"-------- 插件名称 --------"  //要用utf-8
#ifndef PLUGIN_NAME
#error PLUGIN_NAME not defined
#endif // PLUGIN_NAME
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
plugin.cpp
#include "plugin.h"
 
enum
{
    CMD_1 = 1,
    CMD_2
};
 
void MyCB1(CBTYPE cbType, void* callbackInfo);
void MyCB2(CBTYPE cbType, void* callbackInfo);
int Process32NextW_Zero(LPVOID DllBase);
 
//Initialize your plugin data here.
bool pluginInit(PLUG_INITSTRUCT* initStruct)
{
    return true; //Return false to cancel loading the plugin.
}
 
//Deinitialize your plugin data here.
void pluginStop()
{
}
 
//Do GUI/Menu related things here.
void pluginSetup()
{
    //添加菜单
    _plugin_menuaddentry(hMenu, CMD_1, u8"功能1");
    _plugin_menuaddentry(hMenu, CMD_2, u8"功能2");
 
    //注册回调
    _plugin_registercallback(pluginHandle, CB_LOADDLL, MyCB1);
    _plugin_registercallback(pluginHandle, CB_CREATEPROCESS, MyCB1);
    _plugin_registercallback(pluginHandle, CB_MENUENTRY, MyCB2);
}
 
void MyCB1(CBTYPE cbType, void* callbackInfo)
{
    switch (cbType)
    {
    case CB_LOADDLL:
    {
        PLUG_CB_LOADDLL* pTemp = (PLUG_CB_LOADDLL*)callbackInfo;
        if (strcmp(pTemp->modname, "kernel32.dll") == 0)
        {
            int Ret = Process32NextW_Zero(pTemp->LoadDll->lpBaseOfDll);
            if (Ret == 0)
            {
                dprintf("Error:Process32NextW_Zero\n");
            }
        }
        break;
    }
    case CB_CREATEPROCESS:
    {
        Cmd("HideDebugger");
        break;
    }
    }
}
 
int Process32NextW_Zero(LPVOID DllBase)
{
    FARPROC pAddr = GetProcAddress((HMODULE)DllBase, "Process32NextW");
    if (pAddr == NULL)
    {
        dprintf("Error:GetProcAddress\n");
        return 0;
    }
 
    unsigned char buf[5] = { 0x33,0xC0,0xC2,0x08,0x00 };
    bool bRet = DbgMemWrite((duint)*pAddr, buf, sizeof(buf));
    if (bRet == false)
    {
        dprintf("Error:DbgMemWrite\n");
        return 0;
    }
 
    return 1;
}
 
void MyCB2(CBTYPE cbType, void* callbackInfo)
{
    switch (cbType)
    {
    case CB_MENUENTRY:
    {
        PLUG_CB_MENUENTRY* pTemp = (PLUG_CB_MENUENTRY*)callbackInfo;
        switch (pTemp->hEntry)
        {
        case CMD_1:
            MessageBox(NULL, TEXT("CMD_1"), NULL, NULL);
            break;
        case CMD_2:
            MessageBox(NULL, TEXT("CMD_2"), NULL, NULL);
            break;
        }
        break;
    }
    }
}

完结啦


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

收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//