加壳程序:PeShell工程。该工程根据命令行参数,将需要加壳的程序和文件先用zip压缩,后用xor加密,生成一段新的数据,接着在PEUnshell.exe程序中创建一个新的段区,名称默认为"ldata",最后将这段数据写入在PEUnshell.exe文件的新的"ldata"段中。这块加密数据的内存影像(从低地址到高地址)为:
文件个数(int型),密钥(char型,16字节,用的当前时间的md5值),文件大小1(int),文件名1(char型,64字节),文件大小2(int),文件名2(char型,64字节),。。。,,文件大小n(int),文件名n(char型,64字节)。
代码实现:
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
unsigned char
*
Crypto::makeDataBlock(
int
flag, const char filename[MAX_FILE_COUNT][
256
],
int
cnt,
int
& dstdatasize) {
int
ret
=
0
;
int
filesize
=
0
;
for
(
int
i
=
0
; i < cnt; i
+
+
)
{
int
fz
=
FileHelper::getfilesize(filename[i]);
filesize
+
=
fz;
printf(
"file name:%s size:%d\r\n"
, filename[i], fz);
}
int
dstbufsize
=
filesize
+
0x1000
;
unsigned char
*
dstblock
=
new unsigned char[dstbufsize];
*
(
int
*
)dstblock
=
flag;
/
/
if
(cnt
=
=
1
&& strstr((char
*
)filename[
0
],
".exe"
) )
/
/
{
/
/
*
(
int
*
)dstblock
=
ONLY_ONE_EXE;
/
/
}
/
/
else
if
(cnt
=
=
1
&& strstr((char
*
)filename[
0
],
".dll"
))
/
/
{
/
/
*
(
int
*
)dstblock
=
ONLY_ONE_DLL;
/
/
}
/
/
else
if
(cnt >
1
)
/
/
{
/
/
int
flagexe
=
0
;
/
/
int
flagdll
=
0
;
/
/
for
(
int
i
=
0
;i < cnt; i
+
+
)
/
/
{
/
/
if
(strstr((char
*
)filename[i],
".dll"
)) {
/
/
flagdll
=
1
;
/
/
}
else
if
(strstr(filename[i],
".exe"
))
/
/
{
/
/
flagexe
=
1
;
/
/
}
/
/
}
/
/
/
/
if
(flagexe && flagdll)
/
/
{
/
/
*
(
int
*
)dstblock
=
ONE_EXE_AND_ONE_DLL;
/
/
}
/
/
else
{
/
/
*
(
int
*
)dstblock
=
SOME_OTHER_FILES;
/
/
}
/
/
}
/
/
else
{
/
/
return
0
;
/
/
}
unsigned char
*
key
=
dstblock
+
4
;
getkey(key);
*
(
int
*
)(dstblock
+
4
+
CRYPT_KEY_SIZE)
=
cnt;
unsigned char
*
dstbuf
=
dstblock
+
4
+
CRYPT_KEY_SIZE
+
4
;
int
dstbuflimit
=
dstbufsize
-
4
-
CRYPT_KEY_SIZE
-
4
;
for
(
int
i
=
0
; i < cnt; i
+
+
)
{
lstrcpyA((char
*
)dstbuf, filename[i]);
PathStripPathA((char
*
)dstbuf);
dstbuf
+
=
FILENAME_LEN;
dstbuflimit
-
=
FILENAME_LEN;
char
*
lpdata
=
0
;
ret
=
FileHelper::fileReader(filename[i], &lpdata, &filesize);
if
(ret >
0
)
{
unsigned
long
cmpresssize
=
dstbuflimit
-
4
;
ret
=
Compress::compressdata((unsigned char
*
)lpdata, filesize, dstbuf
+
4
, &cmpresssize);
delete[] lpdata;
if
(ret !
=
0
)
{
delete dstblock;
printf(
"compress file:%s error:%u\r\n"
, filename[i], GetLastError());
return
0
;
}
*
(
int
*
)(dstbuf)
=
cmpresssize;
dstbuf
+
=
4
;
dstbuf
+
=
cmpresssize;
dstbuflimit
-
=
4
;
dstbuflimit
-
=
cmpresssize;
}
else
{
delete dstblock;
printf(
"read file:%s error\r\n"
, filename[i]);
return
0
;
}
}
dstdatasize
=
dstbuf
-
dstblock;
CryptData(dstblock
+
4
+
CRYPT_KEY_SIZE, dstdatasize
-
4
-
CRYPT_KEY_SIZE, key, CRYPT_KEY_SIZE);
/
/
revertkey(key);
return
dstblock;
}
参数的解释:
-be:b代表bind,绑定的意思;e代表executable,可执行的。
-bd:b代表bind,绑定的意思;d代表dll,即动态重定位文件。
-o: 输出文件,即加壳后的程序。
-c: 参数。
其中,VirtualSize是区段大小,VirtualAddress是对齐后的虚拟地址,SizeOfRawData是文件对齐后的大小,PointerToRawData是区段在文件中相对于文件头的偏移地址。这里采用的思路是,一般情况下,段表结构PIMAGE_SECTION_HEADER中有空余的项,找到后空项后,计算和重新修改该段表项即可。其中,
VirtualAddress字段=上一个段表的值+上一个段表内存页面对齐后的大小,
PointerToRawData = 上一个段表的值+上一个段表扇区对齐后的大小,
SizeOfRawData是段文件对齐后大小,
VirtualSize是段实际大小,
Characteristics必须是代码或者数据段,否则不会被装入内存,
IMAGE_FILE_HEADER中NumberOfSections字段加1,
IMAGE_OPTIONAL_HEADER32中SizeOfImage字段要加上该段对齐后的大小。
解壳和内存加载执行:即PEUnshell工程。该工程支持编译为PEUnshell.exe和PEUnshell.dll,可以根据命令行参数,将程序隐藏在exe或者dll中,并且都可以正确的释放和加载运行。在程序运行时(PEUnshell.exe是在WinMain函数入口处,PEUnshell.dll是在DllEntry入口处)在自己的内存加载映像中查找"ldata"区段,然后按照加壳中定义的数据结构,将这段压缩加密内存数据解密解压后,写入当前目录,并将其中后缀名为exe的程序启动起来。启动方法是内存加载执行。内存加载执行主要分为三个部分,段加载,导入表填充,重定位。
其中,程序释放程序跟加壳程序压缩加密和内存布局是相反的。
内存加载执行代码如下:
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
int
LoadPE::RunPE(char
*
pFileBuff, DWORD dwSize)
{
int
ret
=
0
;
char szout[
1024
];
DWORD dwSizeOfImage
=
GetSizeOfImage(pFileBuff);
DWORD imagebase
=
GetImageBase(pFileBuff);
if
(imagebase <
=
0
)
{
imagebase
=
DEFAULT_PE_BASE_ADDRESS;
}
wsprintfA(szout,
"image base:%x,size:%x"
, imagebase, dwSizeOfImage);
MessageBoxA(
0
, szout, szout, MB_OK);
/
/
使用MEM_RESERVE分配类型参数 Windows会以
64
KB为边界计算该区域的起始地址 跟PE文件加载边界一致
/
/
使用MEM_COMMIT分配类型参数 区域的起始和结束地址都被计算到
4KB
边界
/
/
VirtualAlloc 当程序访问这部分内存时RAM内存才会被真正分配
char
*
chBaseAddress
=
(char
*
)lpVirtualAlloc(imagebase, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if
(NULL
=
=
chBaseAddress)
{
wsprintfA(szout,
"VirtualAlloc address:%x error"
, imagebase);
MessageBoxA(
0
, szout, szout, MB_OK);
chBaseAddress
=
(char
*
)lpVirtualAlloc(
0
, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if
(NULL
=
=
chBaseAddress)
{
wsprintfA(szout,
"VirtualAlloc address:%x error"
, imagebase);
MessageBoxA(
0
, szout, szout, MB_OK);
return
NULL;
}
}
RtlZeroMemory(chBaseAddress, dwSizeOfImage);
ret
=
MapFile(pFileBuff, chBaseAddress);
/
/
Reloc::recovery((DWORD)chBaseAddress);
ret
=
RelocationTable(chBaseAddress);
/
/
ImportFunTable::recover((DWORD)chBaseAddress);
ret
=
ImportTable(chBaseAddress);
DWORD dwOldProtect
=
0
;
if
(FALSE
=
=
lpVirtualProtect(chBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect))
{
lpVirtualFree(chBaseAddress, dwSizeOfImage, MEM_DECOMMIT);
lpVirtualFree(chBaseAddress,
0
, MEM_RELEASE);
wsprintfA(szout,
"VirtualProtect address:%x error"
, imagebase);
MessageBoxA(
0
, szout, szout, MB_OK);
return
NULL;
}
ret
=
SetImageBase(chBaseAddress);
PIMAGE_DOS_HEADER dos
=
(PIMAGE_DOS_HEADER)chBaseAddress;
PIMAGE_NT_HEADERS nt
=
(PIMAGE_NT_HEADERS)(chBaseAddress
+
dos
-
>e_lfanew);
wsprintfA(szout,
"pe type:%x"
, nt
-
>FileHeader.Characteristics);
MessageBoxA(
0
, szout, szout, MB_OK);
if
(nt
-
>OptionalHeader.Subsystem
=
=
IMAGE_SUBSYSTEM_WINDOWS_GUI)
{
}
else
if
(nt
-
>OptionalHeader.Subsystem
=
=
IMAGE_SUBSYSTEM_WINDOWS_CUI)
{
gType
=
3
;
ghPEModule
=
(HMODULE)chBaseAddress;
gPEImageSize
=
dwSizeOfImage;
ret
=
CallConsoleEntry(chBaseAddress);
lpVirtualFree(chBaseAddress, dwSizeOfImage, MEM_DECOMMIT);
lpVirtualFree(chBaseAddress,
0
, MEM_RELEASE);
return
ret;
}
if
(nt
-
>FileHeader.Characteristics &
0x2000
)
{
gType
=
2
;
gPEImageSize
=
dwSizeOfImage;
ghPEModule
=
(HMODULE)chBaseAddress;
ret
=
recoverEAT(chBaseAddress);
ret
=
CallDllEntry(chBaseAddress);
return
ret;
}
else
{
gType
=
1
;
ghPEModule
=
(HMODULE)chBaseAddress;
gPEImageSize
=
dwSizeOfImage;
ret
=
CallExeEntry(chBaseAddress);
lpVirtualFree(chBaseAddress, dwSizeOfImage, MEM_DECOMMIT);
lpVirtualFree(chBaseAddress,
0
, MEM_RELEASE);
return
ret;
}
return
TRUE;
}
pe文件中的OptionalHeader.ImageBase字段可以修改,故程序编译时指定的加载地址并不是必须的。
通过内存分配,MapFile函数映射区段,RelocationTable重定位,导入表ImportTable,VirtualProtect修改程序内存属性可读写可执行,SetImageBase设置OptionalHeader.ImageBase字段后,程序内存映射基本完毕。接下来,如果是exe程序,那么直接跳转地址:加载地址 + OptionalHeader.AddressOfEntryPoint,如果是dll程序,直接带参数DLL_PROCESS_ATTACH执行DllMainEntry函数,如果是console程序,则带参数执行main函数。
测试时,保证peshell.exe,peunshell.exe或者peunshell.dll,加壳运行的exe或者dll在同一个目录,通过命令运行peshell.exe即可。测试中,大部分体积比较小的32位程序都可以加载执行,但是稍微大一些的程序无法正确运行。原因百思不得其解,望各位赐教。