首页
社区
课程
招聘
windows下程序加壳、内存加载运行的实验
2023-4-16 16:41 18688

windows下程序加壳、内存加载运行的实验

2023-4-16 16:41
18688

源码地址点击此处

第一次在此版发帖,更多的是涉及内存加载执行。

该方案比较简单,只适用于测试和简单情景下的应用,测试中能够运行大多数体积和功能比较简单的32位/64位程序,兼容性和稳定性有待测试和提高。

(一)原理

源码包含两个工程,加壳程序PeShell和脱壳程序PeUnshell。

加壳程序: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;
}

加壳命令行的写法:

1
2
3
4
5
6
7
8
PeShell -be sogou.exe sbiedll.dll -c params.dat -o out.exe
 
PeShell -bd sogou.exe sbiedll.dll -c params.dat -o out.dll
 
PeShell -e sogou.exe -c params.dat -o out.exe
 
PeShell -d sogou.exe -c params.dat -o out.dll

参数的解释:
-be:b代表bind,绑定的意思;e代表executable,可执行的。
-bd:b代表bind,绑定的意思;d代表dll,即动态重定位文件。
-o: 输出文件,即加壳后的程序。
-c: 参数。

加壳程序比较复杂的代码是添加区段。pe文件的区段结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

其中,VirtualSize是区段大小,VirtualAddress是对齐后的虚拟地址,SizeOfRawData是文件对齐后的大小,PointerToRawData是区段在文件中相对于文件头的偏移地址。这里采用的思路是,一般情况下,段表结构PIMAGE_SECTION_HEADER中有空余的项,找到后空项后,计算和重新修改该段表项即可。其中,
VirtualAddress字段=上一个段表的值+上一个段表内存页面对齐后的大小,
PointerToRawData = 上一个段表的值+上一个段表扇区对齐后的大小,
SizeOfRawData是段文件对齐后大小,
VirtualSize是段实际大小,
Characteristics必须是代码或者数据段,否则不会被装入内存,
IMAGE_FILE_HEADER中NumberOfSections字段加1,
IMAGE_OPTIONAL_HEADER32中SizeOfImage字段要加上该段对齐后的大小。

上述功能中使用的结构体如下:

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
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
 
typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
 
    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
  

至此,加壳程序工作基本完成。

解壳和内存加载执行:即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;
    }
 
#ifdef _MYDEBUG
    wsprintfA(szout, "image base:%x,size:%x", imagebase, dwSizeOfImage);
    MessageBoxA(0, szout, szout, MB_OK);
#endif
 
    //使用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)
    {
#ifdef _MYDEBUG
        wsprintfA(szout, "VirtualAlloc address:%x error", imagebase);
        MessageBoxA(0, szout, szout, MB_OK);
#endif
        chBaseAddress = (char*)lpVirtualAlloc(0, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        if (NULL == chBaseAddress)
        {
#ifdef _MYDEBUG
            wsprintfA(szout, "VirtualAlloc address:%x error", imagebase);
            MessageBoxA(0, szout, szout, MB_OK);
#endif
            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);
#ifdef _MYDEBUG
        wsprintfA(szout, "VirtualProtect address:%x error", imagebase);
        MessageBoxA(0, szout, szout, MB_OK);
#endif
        return NULL;
    }
 
    ret = SetImageBase(chBaseAddress);
 
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)chBaseAddress;
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(chBaseAddress + dos->e_lfanew);
 
#ifdef _MYDEBUG
    wsprintfA(szout, "pe type:%x", nt->FileHeader.Characteristics);
    MessageBoxA(0, szout, szout, MB_OK);
#endif
 
    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函数。

本文写的比较粗糙,实际上,本文的核心内容在此处的三个函数上:MapFile,RelocationTable,ImportTable。通过此三个函数,完成了PE文件的内存加载和执行。由于windows平台开发兼容性做的比较好,32位和64位的数据结构基本相同,开发时未作适配,32位即可兼容64位程序。
本程序具体细节不在表述(因为我也是一个初学者,只是照着葫芦画瓢),有兴趣的同学自己看代码体会吧。

(二)测试

测试时,保证peshell.exe,peunshell.exe或者peunshell.dll,加壳运行的exe或者dll在同一个目录,通过命令运行peshell.exe即可。测试中,大部分体积比较小的32位和64位windows PE程序都可以加载执行,但是稍微大一些的程序无法正确运行。原因百思不得其解,望各位赐教。


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

最后于 2024-2-3 10:47 被satadrover编辑 ,原因:
收藏
点赞10
打赏
分享
最新回复 (8)
雪    币: 19270
活跃值: (28900)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-4-16 16:52
2
1
感谢分享
雪    币: 2576
活跃值: (437)
能力值: ( LV2,RANK:85 )
在线值:
发帖
回帖
粉丝
wyfe 2023-4-16 17:04
3
0
杀毒软件会误报吧?
雪    币: 5123
活跃值: (3215)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
樂樂 2023-4-16 17:32
4
0
感谢分享,学习一下
雪    币: 2478
活跃值: (3002)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
satadrover 2023-4-16 17:37
5
0
会的
雪    币: 10082
活跃值: (1724)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wzmooo 2023-4-20 11:32
6
0
x64的exe dll呢
雪    币: 2478
活跃值: (3002)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
satadrover 2023-4-20 12:23
7
0

兼容32位和64位程序

最后于 2023-11-28 23:24 被satadrover编辑 ,原因:
雪    币: 25
活跃值: (25)
能力值: (RANK:0 )
在线值:
发帖
回帖
粉丝
加密狗复制 2023-6-12 16:59
8
0
雪    币: 983
活跃值: (359)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
ranshu 2023-6-18 19:56
9
0
谢谢。
游客
登录 | 注册 方可回帖
返回