关于程序壳
一般分两种,加密壳和压缩壳,具体一般都是对text段进行操作,压缩或者加密,之后通过增加额外段,把程序入口设置到额外段,额外段的内容位text解密或解压一类。本文以64位程序为例,做一个简单的加壳程序。有C++编码经验的也可以直接参考项目,本文代码均已上传,项目地址如下
https://github.com/Synex93/CppPacker.git
手动实现流程
手动通过010进行加入一个自己的段,并且修改pe文件的初始位置为自己的段,自己的段直接jmp到text中,实际不做压缩也不做加密,只是做一个壳的正常加载流程。后面可以基于这些操作进行魔改实现上面所说的加密和压缩壳的操作。源码如下
#include <stdio.h>
#include <windows.h>
int main()
{
SetConsoleOutputCP(65001);
printf("666,你成功的运行了我");
}
编译后,首先需要修改段数量,Nt Headers->File Hader->NumberOfSections,我这里为19,增加1就是改为20
此时在IMAGE_SECTION_HEADER SectionHeaders中会发现多出了一个
这里面比较重要的字段和含义分别如下
1.Name 表示该区段的名字
2.VirtualSize 表示在内存中的大小(一般内存对齐为0x1000)
3.virtualaddress 虚拟地址 即上一个区段的VirtualAddress + 上一个区段经内存对齐粒度对齐后的大小
4.sizeofdata 表示在文件中的大小(一般文件对齐为0x200)
5.pointertorawdata 当前区段的偏移位置,也是起始位置 一般 上一个区段的PointerToRawData + 上一个区段的SizeOfRawData
这里需要注意的是pointertorawdata,如果写一般情况,上一个区段的下一个位置,但是这个位置通常是其他的数据,已经存在,为了不出现冲突,我们需要给移到文件最后面,并且这个数值需要文件对其FileAlignment,通常为200h的倍数,这里需要找到文件末尾,并且一直增加00找到一个是200h的倍数在填写过来。
还需要给读写执行权限
之后在NTHADERS -> OPTIONALHEADER64->SizeOfimage修改一下将他改为最后一个区段的内存地址+内存大小,之前设置的内容为
VirtualAddress (RVA): 43000h
VirtualSize: 1000h (4096 字节)
这里需要修改为44000h
后面如果我们需要关闭随机基址ASLR,我们手动加的是直接jmp,不进行计算基址了,需要在NtHeader->OptionHeader->DllCharacteristics->IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 改为0
之后我们修改函数入口,还是在OptionHeader,有一个AddressOfEntryPoint,直接修改为刚才咱们新增的区段,新增区段都是在最后面的,之前修改前的大小为43000h,我们只需要把43000h填上就可以了。(需要注意的还这里需要记住原值,因为这个位置是原始text的偏移,这里是1410h)
这里需要拿到基础基址和之前获取的text偏移位置1410h相加,基址在NtHeader->OptionHeader->ImageBase中
实际跳转地址为0x140000000+0x1410=0x140001410,也就是需要执行的汇编代码为
mov rax, 140001410h
jmp rax
转换为hex为48B81014004001000000FFE0,具体可以参考网站https://defuse.ca/online-x86-assembler.htm转换,或者直接x64dbg直接敲命令复制。这段hex,直接追加到新增段3F400h位置
保存通过x64dbg调试发现已经正常
直接运行也是没有问题

C++实现流程
对于PE文件的修改这里就不手动定位了,看大部分的文章和视频教程基本都是手动解析和手动定位位置,本章节直接使用现成的库lief进行解析PE与定位字段了。通过lief解析代码如下
std::unique_ptr<LIEF::PE::Binary> pe = LIEF::PE::Parser::parse(path);
解析后需要获取原始OEP位置,参考代码如下,这里是我们最终要跳转回的位置
uint32_t old_oep = static_cast<uint32_t>(pe->optional_header().addressof_entrypoint());
由于正常情况下都会开启ASLR(地址空间布局随机化),如果直接写入OEP位置肯定是不行的,需要后面单独进行计算偏移跳转,这里我们先创建一个新段,并随便插入点汇编代码进行占位,下面代码还设置了新段的权限,参考代码如下
std::vector<uint8_t> content = {
0x90, 0x90, 0x90, 0x90, 0xE9, 0x00, 0x00, 0x00, 0x00 };
LIEF::PE::Section section(".Syn3x");
section.content(content);
section.characteristics(
static_cast<uint32_t>(LIEF::PE::Section::CHARACTERISTICS::MEM_READ) |
static_cast<uint32_t>(LIEF::PE::Section::CHARACTERISTICS::MEM_EXECUTE) |
static_cast<uint32_t>(LIEF::PE::Section::CHARACTERISTICS::CNT_CODE));
LIEF::PE::Section *add_section = pe->add_section(section);
后面获取一下新增区段的跳转地址,并计算实际跳转偏移,并修改OEP
uint32_t section_rva = static_cast<uint32_t>(add_section->virtual_address());
uint32_t rel_offset = old_oep - content.size();
auto content_view = add_section->content();
std::vector<uint8_t> final_content(content_view.begin(), content_view.end());
std::memcpy(&final_content[5], &rel_offset, sizeof(uint32_t));
add_section->content(final_content);
pe->optional_header().addressof_entrypoint(section_rva);
后续的,在lief中构建保存中建议把tls构建改动的配置关闭,因为本身我们不会对tls做任何改动,并且本身程序很简单,似乎并没有涉及tls的部分,但是PE文件中又会存在tls部分数据,这部分数据可能是一个孤儿数据因为用不到,可能是一个默认值,但是在lief中他依旧回去寻找tls相关配置的一些地址做一些重新构建的行为,这里也不深入研究了,如果不想出现报错,建议关闭此选项,然后进行构建新的PE文件,上面的操作实际已经完整的实现了加壳过程,参考代码如下。
LIEF::PE::Builder::config_t config;
config.tls = false;
LIEF::PE::Builder builder{*pe, config};
builder.build();
std::string output_name = "a.Syn3x.exe";
builder.write(output_name);
这里把完整的构建源码也附上,简单注释也已标明
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <windows.h>
#include <iomanip>
#include <LIEF/LIEF.hpp>
#include <LIEF/PE.hpp>
int main(int argc, char **argv)
{
SetConsoleOutputCP(65001);
if (argc < 2)
{
std::cout << "第一个参数指定pe文件" << std::endl;
return 1;
}
std::string path = argv[1];
std::unique_ptr<LIEF::PE::Binary> pe = LIEF::PE::Parser::parse(path);
if (pe)
{
uint32_t old_oep = static_cast<uint32_t>(pe->optional_header().addressof_entrypoint());
std::vector<uint8_t> content = {
0x90, 0x90, 0x90, 0x90, 0xE9, 0x00, 0x00, 0x00, 0x00 };
LIEF::PE::Section section(".Syn3x");
section.content(content);
section.characteristics(
static_cast<uint32_t>(LIEF::PE::Section::CHARACTERISTICS::MEM_READ) |
static_cast<uint32_t>(LIEF::PE::Section::CHARACTERISTICS::MEM_EXECUTE) |
static_cast<uint32_t>(LIEF::PE::Section::CHARACTERISTICS::CNT_CODE));
LIEF::PE::Section *add_section = pe->add_section(section);
if (add_section != nullptr)
{
uint32_t section_rva = static_cast<uint32_t>(add_section->virtual_address());
uint32_t rel_offset = old_oep - (section_rva + content.size());
auto content_view = add_section->content();
std::vector<uint8_t> final_content(content_view.begin(), content_view.end());
std::memcpy(&final_content[5], &rel_offset, sizeof(uint32_t));
add_section->content(final_content);
pe->optional_header().addressof_entrypoint(section_rva);
}
LIEF::PE::Builder::config_t config;
config.tls = false;
LIEF::PE::Builder builder{*pe, config};
builder.build();
builder.write(path.insert(path.length() - 4, ".Syn3x"));
}
return 0;
}
加密壳实现
上面通过C++语言实现的只是普通的写了一个跳转,并不算是真正的壳,只是带着学习一下具体的加载流程,下面将对.text段进行操作,用简单的异或操作进行加密,在新增的段中,一般都不会去调用任何api,相当于在写硬编码,单纯异或操作对于汇编可能比较简单,这里就不通过写汇编的形式进行操作了。通过存根的方法这里单独写C代码,把C代码搞成硬编码塞进新的段中,这样相对直接写汇编相对友好,更方便的实现一些"高级操作"。这里先实现加解密的通用逻辑,这里实现的加解密实现如下,应该不是很难理解,这里不多说了
for (uint64_t i = 0; i < text_size; i++)
{
ptr[i] ^= key;
}
我们需要先创建一个子项目Stub,按照当前的思路,新段的解密代码是需要一个相当于ShellCode的东西,他必须在无任何依赖的情况下都可以执行,这部分硬编码我们使用C生成,如果感觉过于复杂,也可以直接写一段汇编代码进行,当前场景直接写汇编是比用C编译后导出方便的。Stub项目的代码如下,主要含义就是自动获取基址,本身开着随机地址,通过汇编去获取会更加方便直接,一些特定变量通过占位的形式先进行定义
#include <stdint.h>
#define PLACEHOLDER_RVA 0x1122334455667788ULL
#define PLACEHOLDER_SIZE 0x8877665544332211ULL
#define PLACEHOLDER_OEP 0xAABBCCDD11223344ULL
#define PLACEHOLDER_KEY 0xAABBCCDDEEFF1122ULL
__attribute__((section(".text"), naked)) void stub_entry()
{
uintptr_t image_base;
__asm__(
"movq %%gs:0x60, %%rax\n\t"
"movq 0x10(%%rax), %0\n\t"
: "=r"(image_base) : : "rax");
uint64_t text_rva = PLACEHOLDER_RVA;
uint64_t text_size = PLACEHOLDER_SIZE;
uint64_t old_oep = PLACEHOLDER_OEP;
uint64_t key = PLACEHOLDER_KEY;
uint8_t *ptr = (uint8_t *)(image_base + text_rva);
for (uint64_t i = 0; i < text_size; i++)
{
ptr[i] ^= key;
}
uintptr_t text = image_base + old_oep;
__asm__ __volatile__(
"jmp *%0"
:
: "r"(text));
}
环境采用cmake编译,编译参数如下,这里会直接生成一个obj文件,用的时候直接通过objcopy即可拿到刚才所说的ShellCode硬编码
cmake_minimum_required(VERSION 3.15)
project(StubProject C)
add_library(xor_stub OBJECT xor-stub.c)
if(MSVC)
target_compile_options(xor_stub PRIVATE
/GS-
/MT
/Od
/TC
)
else()
target_compile_options(xor_stub PRIVATE
-fno-stack-protector
-ffreestanding
-fno-pic
-fno-common
)
endif()
现在回去写加壳程序,和上面基本一样,只是多了两个步骤,首先是对text段进行加密,加密这部分代码如下
auto text_sec = pe->get_section(".text");
auto content = text_sec->content();
std::vector<uint8_t> encrypted(content.begin(), content.end());
for (auto &b : encrypted)
b ^= xor_key;
text_sec->content(encrypted);
text_sec->add_characteristic(LIEF::PE::Section::CHARACTERISTICS::MEM_WRITE);
第二个步骤就是对刚才生成的硬编码加载进新段,这里加载前还需要把对应的一些地址也搞进去,这里需要从Stub子项目编译成品中获取出对应的text硬编码,具体命令如下
objcopy -j .text -O binary .\build\Stub\CMakeFiles\xor_stub.dir\xor-stub.c.obj stub.bin
提取后,对应的提取和硬传参相关代码如下
std::ifstream stub_f("stub.bin", std::ios::binary);
std::vector<uint8_t> stub_code((std::istreambuf_iterator<char>(stub_f)), {});
patch_placeholder(stub_code, 0x1122334455667788ULL, text_sec->virtual_address());
patch_placeholder(stub_code, 0x8877665544332211ULL, text_sec->virtual_size());
patch_placeholder(stub_code, 0xAABBCCDD11223344ULL, pe->optional_header().addressof_entrypoint());
patch_placeholder(stub_code, 0xAABBCCDDEEFF1122ULL, xor_key);
LIEF::PE::Section new_sec(".Syn3x");
new_sec.content(stub_code);
new_sec.add_characteristic(LIEF::PE::Section::CHARACTERISTICS::MEM_READ);
new_sec.add_characteristic(LIEF::PE::Section::CHARACTERISTICS::MEM_EXECUTE);
new_sec.add_characteristic(LIEF::PE::Section::CHARACTERISTICS::MEM_WRITE);
auto added = pe->add_section(new_sec);
pe->optional_header().addressof_entrypoint(added->virtual_address());
这里应该不是特别难理解,里面有一个patch_placeholder函数,具体实现如下
void patch_placeholder(std::vector<uint8_t> &data, uint64_t placeholder, uint64_t real_value)
{
auto it = std::search(data.begin(), data.end(),
(uint8_t *)&placeholder, (uint8_t *)&placeholder + 8);
if (it != data.end())
{
std::memcpy(&*it, &real_value, 8);
return;
}
std::cerr << "error:" << std::hex << placeholder << std::endl;
}
其他区别基本没有了,这里把加壳程序源码也奉上
#include <iostream>
#include <vector>
#include <fstream>
#include <algorithm>
#include <cstring>
#include <LIEF/PE.hpp>
void patch_placeholder(std::vector<uint8_t> &data, uint64_t placeholder, uint64_t real_value)
{
auto it = std::search(data.begin(), data.end(),
(uint8_t *)&placeholder, (uint8_t *)&placeholder + 8);
if (it != data.end())
{
std::memcpy(&*it, &real_value, 8);
return;
}
std::cerr << "error:" << std::hex << placeholder << std::endl;
}
int main(int argc, char **argv)
{
if (argc < 2)
{
std::cout << "第一个参数指定pe文件" << std::endl;
return 1;
}
std::string path = argv[1];
uint8_t xor_key = 0xAB;
auto pe = LIEF::PE::Parser::parse(path);
auto text_sec = pe->get_section(".text");
auto content = text_sec->content();
std::vector<uint8_t> encrypted(content.begin(), content.end());
for (auto &b : encrypted)
b ^= xor_key;
text_sec->content(encrypted);
text_sec->add_characteristic(LIEF::PE::Section::CHARACTERISTICS::MEM_WRITE);
std::ifstream stub_f("stub.bin", std::ios::binary);
std::vector<uint8_t> stub_code((std::istreambuf_iterator<char>(stub_f)), {});
patch_placeholder(stub_code, 0x1122334455667788ULL, text_sec->virtual_address());
patch_placeholder(stub_code, 0x8877665544332211ULL, text_sec->virtual_size());
patch_placeholder(stub_code, 0xAABBCCDD11223344ULL, pe->optional_header().addressof_entrypoint());
patch_placeholder(stub_code, 0xAABBCCDDEEFF1122ULL, xor_key);
LIEF::PE::Section new_sec(".Syn3x");
new_sec.content(stub_code);
new_sec.add_characteristic(LIEF::PE::Section::CHARACTERISTICS::MEM_READ);
new_sec.add_characteristic(LIEF::PE::Section::CHARACTERISTICS::MEM_EXECUTE);
new_sec.add_characteristic(LIEF::PE::Section::CHARACTERISTICS::MEM_WRITE);
auto added = pe->add_section(new_sec);
pe->optional_header().addressof_entrypoint(added->virtual_address());
LIEF::PE::Builder::config_t config;
config.tls = false;
LIEF::PE::Builder builder{*pe, config};
builder.build();
builder.write(path.insert(path.length() - 4, ".Syn3x"));
return 0;
}
写在后面
关于本文加密壳
当前实现的加密壳还是有一个小问题的,通过ida打开加壳程序可以看的很完善,但是通过x64dbg进行调试打开会一直报一个权限错误的异常,这里因为只是为了学习原理和简单的实现一下,就没有深入的研究这个问题了,但是基本上是没有什么太大的问题。欢迎大佬指出问题。
压缩壳实现?
压缩壳的实现无非就是把新增段的逻辑替换成压缩text段,有一点比较重要的点,就是这里搞的壳都是不去修改一些系统调用的,都是基于当前PE可以直接运行的,如果是需要添加额外调用,类似于WINAPI这些内容,还有一些其他的库的调用,按照当前思路都是不行的,需要深入的研究PE文件,有兴趣的可以去搞一搞。
WINAPI调用?
程序的载入肯定都是要加载kernel32.dll的,winapi实际可以动态的去获取,然后通过下面流程来调用其他API,这样也可以直接丢入到这个段中
GetKernel32Address -> GetProcAddress -> LoadLibrary -> GetModuleHandle -> GetModuleHandle("dll")