首页
社区
课程
招聘
[原创]实现一个压缩壳,并给它加点“料”
2022-8-12 21:20 122858

[原创]实现一个压缩壳,并给它加点“料”

2022-8-12 21:20
122858

一、前言

学完科锐第三阶段壳的课程内容之后,我发现,实现压缩壳,必须对PE格式十分熟悉,其次,解压缩代码需要编写shellcode,也是十分麻烦的环节。有了两者的结合,我们才能写好一个真正的压缩壳。

二、设计思路

首先上一张图,让大家直观地感受到一个壳程序是如何运行起来的。
左边是壳PE,壳程序有一个PE头,节表1是空节,用来存放解压缩后的原程序PE,节表2此时存储的是压缩后的原PE。节表3则是壳代码节,壳PE运行起来后,首先就是进入入口点,运行节表3的代码,解压缩节表2,然后将结果覆盖PE头+节表1的位置,修复完导入表、重定位表,jmp到原程序的入口点处即可。
图片描述
原理不变,我这里加了点“料”,新增了节4和节5,存储了相关的信息,让压缩壳的脱壳过程变难,往后看就知道了。

三、壳代码实现

1.为了生成一个新的壳PE,我们一步步来,首先是PE头,俗话说靠山吃山,靠水吃水,这个PE头我就直接拿原程序的PE头来代替了,只不过需要改一些数据:

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
NumberOfSections --节表数量,要改为5
AddressOfEntryPoint --入口点,要改为节表3里的代码入口
SizeOfImage --PE在内存中的大小,要改为新的PE的内存大小
pSecHdr --节表头,要拓展为5个节表
 
void CPacker::GetNewPeHdr()
{
    //拷贝原PE的PE头
    m_dwNewPeHdrSize = m_pNtHdr->OptionalHeader.SizeOfHeaders;
    m_pNewPeHdr = new BYTE[m_dwNewPeHdrSize];
    CopyMemory(m_pNewPeHdr, m_pDosHdr, m_dwNewPeHdrSize);
 
    //修改
    auto pDosHdr = (PIMAGE_DOS_HEADER)m_pNewPeHdr;
    auto pNtHdr = (PIMAGE_NT_HEADERS)(m_pNewPeHdr + pDosHdr->e_lfanew);
    auto pSecHdr = (PIMAGE_SECTION_HEADER)
        ((LPBYTE)&pNtHdr->OptionalHeader + pNtHdr->FileHeader.SizeOfOptionalHeader);
 
    pNtHdr->FileHeader.NumberOfSections = 5;
    pNtHdr->OptionalHeader.AddressOfEntryPoint = m_newSecHdr[2].VirtualAddress;
    pNtHdr->OptionalHeader.SizeOfImage = m_newSecHdr[4].VirtualAddress + m_newSecHdr[4].Misc.VirtualSize;
    //清空DataDirectory目录
    ZeroMemory(pNtHdr->OptionalHeader.DataDirectory, sizeof(pNtHdr->OptionalHeader.DataDirectory));
 
    //修改新的节表头
    CopyMemory(pSecHdr, m_newSecHdr, sizeof(m_newSecHdr));
 
}

2.新PE头里一些需要修改的数据,比如SizeOfImage,我们目前还没有,需要等我们构造出节表1、2、3、4、5之后,才知道。接下来,先构造节表1的表头,节表1是个空节,它的大小只要够存放原程序的节表即可,多给一点也没关系,我这里直接给了SizeOfImage。(由于是空节,所以这里并不需要考虑节表1的数据内容)

1
2
3
4
5
6
7
8
//空节
    strcpy((char*)m_newSecHdr[0].Name, ".cr42");
    m_newSecHdr[0].Misc.VirtualSize = m_pNtHdr->OptionalHeader.SizeOfImage;
    m_newSecHdr[0].VirtualAddress = m_pSecHdr[0].VirtualAddress;
    m_newSecHdr[0].SizeOfRawData = 0;
    m_newSecHdr[0].PointerToRawData = 0;
    m_newSecHdr[0].Characteristics =
        IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;

3.接着是节表2,这个节要存放原PE的压缩数据,先设计表头(使用了PointerToRelocations和PointerToLinenumbers这两个没啥用的字段,存放压缩大小信息,留着给后面shellcode用)

1
2
3
4
5
6
7
8
9
10
//压缩数据节
    strcpy((char*)m_newSecHdr[1].Name, ".data");
    m_newSecHdr[1].Misc.VirtualSize = GetAlign(m_dwComSecSize, m_pNtHdr->OptionalHeader.SectionAlignment);
    m_newSecHdr[1].VirtualAddress = m_newSecHdr[0].VirtualAddress + m_newSecHdr[0].Misc.VirtualSize;
    m_newSecHdr[1].SizeOfRawData = m_dwComSecSize;
    m_newSecHdr[1].PointerToRawData = m_pNtHdr->OptionalHeader.SizeOfHeaders;
    m_newSecHdr[1].Characteristics = IMAGE_SCN_MEM_READ;
 
    m_newSecHdr[1].PointerToRelocations = m_dwComSize; //压缩后大小
    m_newSecHdr[1].PointerToLinenumbers = m_dwSrcPeSize;//压缩前大小

然后将PE压缩,压缩前我把节表、导入表、重定位表保存并清空,到时候由shellcode进行还原。

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
bool CPacker::GetCompressData()
{
    COMPRESSOR_HANDLE hCompressor = NULL;
    BOOL Success = CreateCompressor(
        COMPRESS_ALGORITHM_XPRESS_HUFF,
        NULL,
        &hCompressor
    );
 
    m_pComData = new BYTE[m_dwSrcPeSize + 0x28];
    LPBYTE m_pSrcPeTmp = new BYTE[m_dwSrcPeSize];
    CopyMemory(m_pSrcPeTmp, m_pSrcPe, m_dwSrcPeSize);
 
    PIMAGE_DOS_HEADER m_pDosHdrTmp = (PIMAGE_DOS_HEADER)m_pSrcPeTmp;
    PIMAGE_NT_HEADERS m_pNtHdrTmp = (PIMAGE_NT_HEADERS)(m_pSrcPeTmp + m_pDosHdrTmp->e_lfanew);
    PIMAGE_SECTION_HEADER m_pSecHdrTmp = (PIMAGE_SECTION_HEADER)
        ((LPBYTE)&m_pNtHdrTmp->OptionalHeader + m_pNtHdrTmp->FileHeader.SizeOfOptionalHeader);
 
    //1.在压缩前,把节表保存并清空
    nSecNum = m_pNtHdrTmp->FileHeader.NumberOfSections;
    CopyMemory(m_pSaveSecHdr, m_pSecHdrTmp, nSecNum * 40);
    ZeroMemory(m_pSecHdrTmp, nSecNum * 40);
 
    //2.把导入表保存并清空
    m_pImportAddr = m_pNtHdrTmp->OptionalHeader.DataDirectory[1].VirtualAddress;
    nImpSize = m_pNtHdrTmp->OptionalHeader.DataDirectory[1].Size;
    ZeroMemory(&(m_pNtHdrTmp->OptionalHeader.DataDirectory[1]), 8);
 
    //3.把重定位表保存并清空
    m_pRelocAddr = m_pNtHdrTmp->OptionalHeader.DataDirectory[5].VirtualAddress;
    nRelocSize = m_pNtHdrTmp->OptionalHeader.DataDirectory[5].Size;
    ZeroMemory(&(m_pNtHdrTmp->OptionalHeader.DataDirectory[5]), 8);
 
 
 
    Success = Compress(
        hCompressor,
        m_pSrcPeTmp,
        m_dwSrcPeSize,
        m_pComData,
        m_dwSrcPeSize + 0x28,
        &m_dwComSize
    );
 
 
    return true;
}

4.接着是节表3,该节表存放的是解压缩PE、还原导入表、重定位表,运行原程序的至关重要的shellcode,先设计表头

1
2
3
4
5
6
7
//代码节
    strcpy((char*)m_newSecHdr[2].Name, ".text");
    m_newSecHdr[2].Misc.VirtualSize = GetAlign(m_dwCodeSecSize, m_pNtHdr->OptionalHeader.SectionAlignment);
    m_newSecHdr[2].VirtualAddress = m_newSecHdr[1].VirtualAddress + m_newSecHdr[1].Misc.VirtualSize;
    m_newSecHdr[2].SizeOfRawData = m_dwCodeSecSize;
    m_newSecHdr[2].PointerToRawData = m_newSecHdr[1].PointerToRawData + m_newSecHdr[1].SizeOfRawData;
    m_newSecHdr[2].Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;

至于节表3的数据内容,也就是shellcode,等我把节表、PE设计完,我再说
5.接着是节表4,节表4是我额外增加的一个,用来存储步骤3中保存的原节表表头、原导入表、原重定位表,首先设计节表4表头

1
2
3
4
5
6
7
//存放原节表、导入表、重定位表的节
    strcpy((char*)m_newSecHdr[3].Name, ".info");
    m_newSecHdr[3].Misc.VirtualSize = GetAlign(m_dwTableSecSize, m_pNtHdr->OptionalHeader.SectionAlignment);
    m_newSecHdr[3].VirtualAddress = m_newSecHdr[2].VirtualAddress + m_newSecHdr[2].Misc.VirtualSize;
    m_newSecHdr[3].SizeOfRawData = m_dwTableSecSize;
    m_newSecHdr[3].PointerToRawData = m_newSecHdr[2].PointerToRawData + m_newSecHdr[2].SizeOfRawData;
    m_newSecHdr[3].Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;

然后把刚才保存的原节表表头、原导入表、原重定位表信息,按顺序写入到缓冲区里。

1
2
3
4
5
6
7
8
9
10
11
12
13
bool CPacker::GetTable()
{
    m_dwTableSize = nSecNum * 40 + 4 + 8 + 8;
    m_pTable = new BYTE[m_dwTableSize];
    RtlCopyMemory(m_pTable, &nSecNum, 4);
    RtlCopyMemory(m_pTable + 4, m_pSaveSecHdr, nSecNum * 40);
    RtlCopyMemory(m_pTable + 4 + nSecNum * 40, &m_pImportAddr, 4);
    RtlCopyMemory(m_pTable + 4 + nSecNum * 40 + 4, &nImpSize, 4);
    RtlCopyMemory(m_pTable + 4 + nSecNum * 40 + 4 + 4, &m_pRelocAddr, 4);
    RtlCopyMemory(m_pTable + 4 + nSecNum * 40 + 4 + 4 + 4, &nRelocSize, 4);
 
    return true;
}

6.还剩最后一个节表5,是一个空节,壳代码还原原PE时,要还原导入表,于是这个节的作用就体现出来了,shellcode在这里玩了一波偷梁换柱,直接毙掉了x64dbg的脱壳后导入表自动修复功能,等会介绍shellcode的时候你们就知道了。设计节表5的表头

1
2
3
4
5
6
7
//绕过x64搜索导入表的节(空节)
    strcpy((char*)m_newSecHdr[4].Name, ".imp");
    m_newSecHdr[4].Misc.VirtualSize = GetAlign(0x10000, m_pNtHdr->OptionalHeader.SectionAlignment);
    m_newSecHdr[4].VirtualAddress = m_newSecHdr[3].VirtualAddress + m_newSecHdr[3].Misc.VirtualSize;
    m_newSecHdr[4].SizeOfRawData = 0;
    m_newSecHdr[4].PointerToRawData = m_newSecHdr[3].PointerToRawData + m_newSecHdr[2].SizeOfRawData;
    m_newSecHdr[4].Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;

至此,新的壳PE结构我们就设计好了,然后将新PE头、节表234写入到新文件(节表1、5是空节,不用写入),就大功告成,加壳完毕!

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
bool CPacker::WriteNewPe(CString strNewPe)
{
    //创建文件
    HANDLE hFile = CreateFile(strNewPe,
        GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
 
    //写入PE头
    DWORD dwBytesWrited = 0;
    WriteFile(hFile, m_pNewPeHdr, m_dwNewPeHdrSize, &dwBytesWrited, NULL);
 
    //写入数据节
    WriteFile(hFile, m_pComSec, m_dwComSecSize, &dwBytesWrited, NULL);
 
    //写入代码节
    WriteFile(hFile, m_pCodeSec, m_dwCodeSecSize, &dwBytesWrited, NULL);
 
    //写入存放表数据节
    WriteFile(hFile, m_pTableSec, m_dwTableSecSize, &dwBytesWrited, NULL);
 
    CloseHandle(hFile);
 
    return true;
}

四、shellcode代码实现

由于壳PE可能是随机基址,所以执行shellcode时,一定要确保它的代码跟地址无关。我的shellcode是在VS上编译的,为了保证VS不会生成多余的代码,要修改以下几个设置:

1
2
3
4
5
1.使用Release版(Debug会加地址有关代码)
2.不使用main函数,自己定义一个,放在链接器->高级->入口点(main不是程序真正入口点)
3.关掉代码生成->安全检查
4.关掉增强指令集
5.关掉全程序优化

然后就是最麻烦的shellcode代码编写了。首先要清楚shellcode代码的功能:

1
2
3
1.解压缩节表2里面的压缩数据
2.将解压缩数据覆盖到PE头处,连带着空节表1也被覆盖
3.还原节表、导入表、重定位表

1.要解压缩,那么必定要调用库的API,如果直接调用的话,call的就是死地址,违背了地址无关原则。那么使用LoadLibrary然后GetProcAddress?显然也不行,LoadLibrary和GetProcAddress也是死地址,所以我们要自己实现LoadLibrary和GetProcAddress的功能。
首先通过_PEB来拿到kernel32的模块基址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HMODULE GetKernel32()
{
    HMODULE hKer;
    __asm {
 
        mov eax, dword ptr fs:[0x30]
        mov eax, dword ptr[eax + 0x0C]
        mov eax, dword ptr[eax + 0x0C]
        mov eax, dword ptr[eax]
        mov eax, dword ptr[eax]
        mov eax, dword ptr[eax + 0x18]
        mov hKer, eax
    }
 
    return hKer;
}

然后通过kernel32的导出函数表,拿到GetProcAddress函数地址,代码如下:

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
FARPROC MyGetProcAddress(HMODULE hMod, LPCSTR lpProcName) {
 
    IMAGE_DOS_HEADER* pDosHdr;
    IMAGE_NT_HEADERS* pNTHdr;
    IMAGE_EXPORT_DIRECTORY* pExpDir;
    DWORD pAddrTbl;
    DWORD pNameTbl;
    DWORD pOrdTbl;
 
    //解析dos头
    pDosHdr = (IMAGE_DOS_HEADER*)hMod;
 
    //nt头
    pNTHdr = (IMAGE_NT_HEADERS*)(pDosHdr->e_lfanew + (DWORD)hMod);
 
    //获取导出表
    pExpDir = (IMAGE_EXPORT_DIRECTORY*)(pNTHdr->OptionalHeader.DataDirectory[0].VirtualAddress + (DWORD)hMod);
 
    //导出函数地址表
    pAddrTbl = (DWORD)(pExpDir->AddressOfFunctions + (DWORD)hMod);
 
    //导出函数名称表
    pNameTbl = (DWORD)(pExpDir->AddressOfNames + (DWORD)hMod);
 
    //导出序号表
    pOrdTbl = (DWORD)(pExpDir->AddressOfNameOrdinals + (DWORD)hMod);
 
    //判断是序号还是名称
    if ((int)lpProcName & 0xffff0000) {
 
        //名称
        int i = 0;
        while (i < pExpDir->NumberOfNames) {
 
            //获取名称地址
            int nNameOff = (int)(*(DWORD*)(pNameTbl + i * 4) + (DWORD)hMod);
 
            //字符串比较
            if (((char*)nNameOff)[0] == 'G'&& ((char*)nNameOff)[1] == 'e'&& ((char*)nNameOff)[2] == 't'&&
                ((char*)nNameOff)[3] == 'P'&& ((char*)nNameOff)[4] == 'r'&& ((char*)nNameOff)[5] == 'o'&&
                ((char*)nNameOff)[6] == 'c'&& ((char*)nNameOff)[7] == 'A'&& ((char*)nNameOff)[8] == 'd'&&
                ((char*)nNameOff)[9] == 'd'&& ((char*)nNameOff)[10] == 'r'&& ((char*)nNameOff)[11] == 'e'&&
                ((char*)nNameOff)[12] == 's'&& ((char*)nNameOff)[13] == 's') {
 
                //找到了, 从导出序号表取出函数地址下标
                int nOrdinal = *(WORD*)(pOrdTbl + i * 2);
 
                //从导出地址表,下标寻址,获取导出函数地址
                int nFuncAddr = *(DWORD*)(pAddrTbl + nOrdinal * 4);
 
 
 
                //不是转发
                nFuncAddr += (int)hMod;
 
 
 
 
                //返回地址
                if (nFuncAddr != NULL) {
 
                    return (FARPROC)nFuncAddr;
                }
            }
 
            i++;
 
        }
    }
 
    else {
 
        //序号
        int nOrdinal = (DWORD)lpProcName - pExpDir->Base;
 
        //从导出地址表,下标寻址,获取导出函数地址
        int nFuncAddr = *(DWORD*)(pAddrTbl + nOrdinal * 4);
 
        //返回地址
        if (nFuncAddr != NULL) {
 
            return (FARPROC)(nFuncAddr + (DWORD)hMod);
        }
 
    }
 
 
    return 0;
}

有了GetProcAddress函数地址和kernel32的基址,同理就能拿到LoadLibrary函数地址了(注意定义字符串变量时,用单个字符一个一个排列,在汇编里面看就是db出来的,否则字符串会有一个常量区地址,影响shellcode的通用性)

1
2
char szLoadLibrary[] = { 'L', 'o','a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', '\0' };
    pEnv->pfnLoadLibraryA = (PFN_LoadLibraryA)pEnv->pfnGetProcAddress(hKer, szLoadLibrary);

2.有了LoadLibrary和GetProcAddress,就可以使用任何库函数了,解压缩便是小菜一碟。

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
//有了LoadLibrary和GetProcAddress,就可以使用任意函数了
    //获取解压缩相关函数
 
    char szCab[] = { 'C','a','b','i','n','e','t', '\0' };
    HMODULE hCab = pEnv->pfnLoadLibraryA(szCab);
 
    char szCreateDecompressor[] = { 'C','r','e','a','t','e','D','e','c','o','m','p','r','e','s','s','o','r', '\0' };
    pEnv->pfnCreateDecompressor = (PFN_CreateDecompressor)pEnv->pfnGetProcAddress(hCab, szCreateDecompressor);
 
    char szDecompress[] = { 'D','e','c','o','m','p','r','e','s','s', '\0' };
    pEnv->pfnDecompress = (PFN_Decompress)pEnv->pfnGetProcAddress(hCab, szDecompress);
 
    char szVirtualAlloc[] = { 'V','i','r','t','u','a','l','A','l','l','o','c', '\0' };
    pEnv->pfnVirtualAlloc = (PFN_VirtualAlloc)pEnv->pfnGetProcAddress(hKer, szVirtualAlloc);
 
    char szVirtualProtect[] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t','\0' };
    pEnv->pfnVirtualProtect = (PFN_VirtualProtect)pEnv->pfnGetProcAddress(hKer, szVirtualProtect);
 
 
//解压缩
 
    LPBYTE pPEBuff = (LPBYTE)env.pfnVirtualAlloc(NULL, dwDeComSize, MEM_COMMIT, PAGE_READWRITE);
 
    DECOMPRESSOR_HANDLE hDecompressor;
    BOOL bSuccess = env.pfnCreateDecompressor(
        COMPRESS_ALGORITHM_XPRESS_HUFF,
        NULL,
        &hDecompressor
    );
 
    DWORD dwDecompressedBufferSize = 0;
    bSuccess = env.pfnDecompress(
        hDecompressor,
        pComData,
        dwComSize,
        pPEBuff,
        dwDeComSize,
        &dwDecompressedBufferSize
    );

3.解压缩完毕,得到了原PE,接下来就是将原PE覆盖到现在的PE头+节表1的地方,同时还原节表头、导入表和重定位表,相当于LoadPE的功能了

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#define VirtualProtect pEnv->pfnVirtualProtect
DWORD MyLoadLibrary(LPBYTE pPEBuff, Environment* pEnv, LPBYTE pTableBuf, LPBYTE pMyImpBuf) {
 
    DWORD dwImageBase;
    HANDLE hFile;
    HANDLE hFileMap;
    LPVOID pPEBuf;
    IMAGE_DOS_HEADER* pDosHdr;
    IMAGE_NT_HEADERS* pNTHdr;
    IMAGE_SECTION_HEADER* pSecHdr;
    DWORD dwNumOfSecs;
    IMAGE_IMPORT_DESCRIPTOR* pImpHdr;
    DWORD dwSizeOfHeaders;
    IMAGE_IMPORT_DESCRIPTOR hdrZeroImp;
    HMODULE hDll;
    DWORD dwOep;
    DWORD dwOldProc;
    IMAGE_BASE_RELOCATION* pReloc;
    DWORD dwOfReloc;
    DWORD dwOff;
 
    //RtlZeroMemory(&hdrZeroImp, sizeof(IMAGE_IMPORT_DESCRIPTOR));
 
    pPEBuf = pPEBuff;
 
    //解析
    //dos 头
    pDosHdr = (IMAGE_DOS_HEADER*)pPEBuf;
 
    //nt头
    pNTHdr = (IMAGE_NT_HEADERS*)(pDosHdr->e_lfanew + (DWORD)pPEBuf);
 
    //还原节表
    DWORD nSecNum = *(DWORD*)pTableBuf;
    mymemcpy((void*)((DWORD)&pNTHdr->OptionalHeader + pNTHdr->FileHeader.SizeOfOptionalHeader),
        pTableBuf + 4, nSecNum * 40);
 
    //还原导入表
    mymemcpy(&(pNTHdr->OptionalHeader.DataDirectory[1].VirtualAddress),
        pTableBuf + 4 + nSecNum * 40, 4);
    mymemcpy(&(pNTHdr->OptionalHeader.DataDirectory[1].Size),
        pTableBuf + 4 + nSecNum * 40 + 4, 4);
 
    //还原重定位表
    mymemcpy(&(pNTHdr->OptionalHeader.DataDirectory[5].VirtualAddress),
        pTableBuf + 4 + nSecNum * 40 + 4 + 4, 4);
    mymemcpy(&(pNTHdr->OptionalHeader.DataDirectory[5].Size),
        pTableBuf + 4 + nSecNum * 40 + 4 + 4 + 4, 4);
 
    //选项头信息
    dwSizeOfHeaders = pNTHdr->OptionalHeader.SizeOfHeaders;
 
 
    //自己的模块基址
    dwImageBase = (DWORD)GetModuleBase();
 
    dwOff = dwImageBase - pNTHdr->OptionalHeader.ImageBase; //新旧ImageBase的偏移差
 
    dwOep = pNTHdr->OptionalHeader.AddressOfEntryPoint + dwImageBase;
    //节表
    dwNumOfSecs = pNTHdr->FileHeader.NumberOfSections;
    pSecHdr = (IMAGE_SECTION_HEADER*)((DWORD)&pNTHdr->OptionalHeader + pNTHdr->FileHeader.SizeOfOptionalHeader);
 
 
 
 
    //拷贝PE头
    VirtualProtect((LPVOID)dwImageBase, pNTHdr->OptionalHeader.SizeOfHeaders, PAGE_EXECUTE_READWRITE, &dwOldProc);
    mymemcpy((void*)dwImageBase, pPEBuf, dwSizeOfHeaders);
    VirtualProtect((LPVOID)dwImageBase, pNTHdr->OptionalHeader.SizeOfHeaders, dwOldProc, &dwOldProc);
 
 
    //按照节表,拷贝节区数据
    int i = 0;
    IMAGE_SECTION_HEADER* dwSecTmp = pSecHdr;
    while (i < dwNumOfSecs) {
 
        //目标
        DWORD dwDstMem = dwImageBase;
        dwDstMem += dwSecTmp->VirtualAddress;
 
        //
        DWORD dwSrcFile = (DWORD)pPEBuf + dwSecTmp->PointerToRawData;
 
        //拷贝
        VirtualProtect((LPVOID)dwDstMem, dwSecTmp->SizeOfRawData, PAGE_EXECUTE_READWRITE, &dwOldProc);
        mymemcpy((void*)dwDstMem, (void*)dwSrcFile, dwSecTmp->SizeOfRawData);
        VirtualProtect((LPVOID)dwImageBase, pNTHdr->OptionalHeader.SizeOfHeaders, dwOldProc, &dwOldProc);
 
        i++;
        dwSecTmp = (IMAGE_SECTION_HEADER*)((char*)dwSecTmp + sizeof(IMAGE_SECTION_HEADER));
    }
 
 
    //获取导入表
    if (pNTHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress != 0) {
 
        pImpHdr = (IMAGE_IMPORT_DESCRIPTOR*)(dwImageBase + pNTHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
 
 
        //处理导入表
        IMAGE_IMPORT_DESCRIPTOR* pImpHdrTmp = pImpHdr;
        DWORD dwNum = 0;
        int nImpNum = 0;
 
        while (true) {
 
            //判断结束,全0项结束
            if (memcmp(pImpHdrTmp, &hdrZeroImp, sizeof(IMAGE_IMPORT_DESCRIPTOR)) == 0) {
 
                break;
            }
 
            //判断字段, 为空则结束
            if (pImpHdrTmp->Name == NULL || pImpHdrTmp->FirstThunk == NULL) {
 
                break;
            }
 
            //加载dll
            hDll = pEnv->pfnLoadLibraryA((LPCSTR)(dwImageBase + pImpHdrTmp->Name));
 
            //获取导入地址表, IAT
            DWORD dwIAT = pImpHdrTmp->FirstThunk + dwImageBase;
            DWORD dwINT = dwIAT;
            //获取导入名称表, INT
            if (pImpHdrTmp->OriginalFirstThunk != NULL) {
 
                dwINT = pImpHdrTmp->OriginalFirstThunk + dwImageBase;
            }
 
            //遍历导入名称表
            while (*(DWORD*)(dwINT) != 0) {
 
                if ((*(DWORD*)pImpHdrTmp) >> 31) {
 
                    //序号导入, 获取序号
                    dwNum = *(DWORD*)pImpHdrTmp;
                    dwNum = (dwNum << 16) >> 16;
 
                }
                else {
 
                    //名称导入
                    dwNum = *(DWORD*)pImpHdrTmp;
                    dwNum += dwImageBase;
                    dwNum += 2;
 
                }
 
                //获取函数地址后,先不要把地址直接写入IAT
                //而是先将.imp节地址写入IAT(每16个字节写一次)
                //在.imp节里写代码指令push 函数地址 retn
                //如果函数名是_acmdln,那么这里就不要混淆导入表
                if (((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[0] == '_' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[1] == 'a' &&
                    ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[2] == 'c' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[3] == 'm' &&
                    ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[4] == 'd' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[5] == 'l' &&
                    ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[6] == 'n') {
 
                    *(DWORD*)dwIAT = (DWORD)pEnv->pfnGetProcAddress(hDll, (LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2));
                }
                else {
 
                    DWORD dwMyAddr = (DWORD)pMyImpBuf + nImpNum * 0x10;
                    DWORD dwFuncAddr = (DWORD)pEnv->pfnGetProcAddress(hDll, (LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2));
                    *(DWORD*)dwIAT = dwMyAddr;
                    *(BYTE*)dwMyAddr = 0x68;    //push
                    *(DWORD*)(dwMyAddr + 1) = dwFuncAddr;  //真实函数地址
                    *(BYTE*)(dwMyAddr + 5) = 0xC3//retn
                }
 
 
                dwIAT += 4;
                dwINT += 4;
                nImpNum++;
 
            }
 
 
            pImpHdrTmp = (IMAGE_IMPORT_DESCRIPTOR*)((char*)pImpHdrTmp + sizeof(IMAGE_IMPORT_DESCRIPTOR));
        }
    }
 
    if (pNTHdr->OptionalHeader.DataDirectory[5].VirtualAddress != 0) {
 
        //定位重定位表
        pReloc = (IMAGE_BASE_RELOCATION*)(pNTHdr->OptionalHeader.DataDirectory[5].VirtualAddress + dwImageBase);
        dwOfReloc = pNTHdr->OptionalHeader.DataDirectory[5].Size;
 
 
        int nSize = 0;
 
        while (nSize < dwOfReloc) {
 
            //数组首地址
            int nOff = (DWORD)pReloc + 8;
 
            //数组元素个数
            int nCnt = (pReloc->SizeOfBlock - 8) >> 1;
 
            //遍历数组
            int j = 0;
            while (j < nCnt) {
 
                //取出一项
                int nDataOff = *(WORD*)(nOff + j * 2);
 
                //判断是否是有效重定位项
                if (nDataOff & 0x00003000) {
 
                    //修正
                    nDataOff = nDataOff & 0x0fff//页偏移
                    nDataOff = nDataOff + pReloc->VirtualAddress;
                    nDataOff = nDataOff + dwImageBase;
 
                    *(int*)nDataOff = *(int*)nDataOff + dwOff;
                }
 
                j++;
            }
 
            //处理下一个分页
            nSize += pReloc->SizeOfBlock;
            pReloc = (IMAGE_BASE_RELOCATION*)((char*)pReloc + pReloc->SizeOfBlock);
 
        }
    }
 
 
 
    return dwOep;
}

这里重点要介绍的,就是还原导入表的过程,也是整篇文章的核心主题,加点“料”。我在遍历还原导入表时,并没有直接将API的地址填入到IAT里,而是将节表5的地址,从起始位置开始,每隔16个字节,将地址填入到IAT里,然后在对应的节表5地址上填入push 真实函数地址 + retn的汇编指令。这样一来,原PE程序运行调用API时,就会跳到节表5里面,再从节表5里面跳到真实API地址,直接干掉了x64dbg的脱壳导入表自动修复功能。嘿嘿~~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//获取函数地址后,先不要把地址直接写入IAT
                //而是先将.imp节地址写入IAT(每16个字节写一次)
                //在.imp节里写代码指令push 函数地址 retn
                //如果函数名是_acmdln,那么这里就不要混淆导入表
                if (((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[0] == '_' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[1] == 'a' &&
                    ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[2] == 'c' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[3] == 'm' &&
                    ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[4] == 'd' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[5] == 'l' &&
                    ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[6] == 'n') {
 
                    *(DWORD*)dwIAT = (DWORD)pEnv->pfnGetProcAddress(hDll, (LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2));
                }
                else {
 
                    DWORD dwMyAddr = (DWORD)pMyImpBuf + nImpNum * 0x10;
                    DWORD dwFuncAddr = (DWORD)pEnv->pfnGetProcAddress(hDll, (LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2));
                    *(DWORD*)dwIAT = dwMyAddr;
                    *(BYTE*)dwMyAddr = 0x68;    //push
                    *(DWORD*)(dwMyAddr + 1) = dwFuncAddr;  //真实函数地址
                    *(BYTE*)(dwMyAddr + 5) = 0xC3//retn
                }

五、总结

最后来看看效果,对扫雷进行加壳,加壳后的程序可以正常运行

先用x64找到真实入口点,进行一波dump操作
图片描述
dump后无法直接运行
图片描述
然后去x64里使用自动搜索修复导入表的功能,可以看到,IAT这里存放的压根就不是真实API的地址,所以x64也无法识别出来。
图片描述
搞定,收工!


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2022-8-23 12:21 被橘喵Cat编辑 ,原因:
收藏
点赞11
打赏
分享
打赏 + 80.00雪花
打赏次数 1 雪花 + 80.00
 
赞赏  Editor   +80.00 2022/08/25 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (10)
雪    币: 8882
活跃值: (3329)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chengdrgon 2022-8-13 11:40
2
0
感谢分享
雪    币: 3654
活跃值: (3828)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 2022-8-16 14:42
3
0
感谢分享
雪    币: 74
活跃值: (528)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wodexinren 2022-8-17 11:08
4
0
感谢分享
雪    币: 3077
活跃值: (2428)
能力值: ( LV8,RANK:147 )
在线值:
发帖
回帖
粉丝
Roger 1 2022-8-18 10:21
5
0
看样子仅支持32位程序,楼主可以改一改,支持64位
雪    币: 422
活跃值: (2614)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
t0hka1 1 2022-8-18 15:40
6
0
感谢分享
雪    币: 3191
活跃值: (1843)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
江楠boy 2022-8-20 11:14
7
0
感谢分享
雪    币: 1161
活跃值: (1360)
能力值: ( LV5,RANK:78 )
在线值:
发帖
回帖
粉丝
luoye_ATL 2022-8-22 17:27
8
0
感谢分享
雪    币: 972
活跃值: (1353)
能力值: ( LV6,RANK:97 )
在线值:
发帖
回帖
粉丝
橘喵Cat 1 2022-8-23 20:29
9
0
Roger 看样子仅支持32位程序,楼主可以改一改,支持64位
还没学64位汇编
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
yanghaoi 2022-9-1 07:06
10
0
老哥可以甩一份github嘛 学不太费QAQ
雪    币: 129
活跃值: (1243)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
琳宇 2022-9-1 18:05
11
0
这个壳怕不是很难遇到了。UPX典型
游客
登录 | 注册 方可回帖
返回