针对易语言静态编译的代码进行轻微混淆处理,避免被插件一键识别部分关键函数。处理过的内容参考下方。
注意该轻微膨胀/混淆只能用来对抗现有的“一键识别”工具,不能和加密壳的效果比。
如果你能拿到这类工具的源码缝缝补补,应该也能让它重新识别。
原理
对已知的部分特征进行魔改,大部分时候都是对简单操作进行膨胀,然后开辟新的内存空间跳转过去。
因为生成代码计算绝对地址比较麻烦,所以依赖 CALL 指令自动入栈的返回地址来计算正确跳转位置。
处理过的特征
只处理了一小部分特征。手动整起来太麻烦了,感觉不如利用特征码自动标记然后上加密壳批量处理了。
从源码构建
首先确保安装有 VS 2022、CMake、Git for Windows 这三个程序。CMake 安装时需要选择将 cmake.exe
注册到系统。
:: 克隆仓库
git clone https://github.com/FlyingRainyCats/ELangPatcher.git
cd ELangPatcher
:: 更新子模组
git submodule update --init --recursive
:: 开始构建
cmake -Bcmake-build-vs2022 -G "Visual Studio 17 2022" -A Win32
cmake --build cmake-build-vs2022 --config Release
使用方法
将可执行文件拖放到 ELangPatcher.exe
即可处理。
可以指定额外参数:
--suffix _p
指定新的后缀,写出到新的文件。
-b, --backup
是否备份原始文件,默认启用。
--fake-stub
插入假的特征码内容到文件,默认启用。
-h, --help
查看帮助信息
示例输出:
* (2). "M:\Programs\E_5.1\tools\ELangPatcher.exe" --suffix _ -- "M:\Projects\e-AntiWnd\测试3_5.1_静态编译.exe"
ELang Patcher v0.1 by FlyingRainyCats (爱飞的猫 @52pojie.cn)
INFO: processing: M:\Projects\e-AntiWnd\测试3_5.1_静态编译.exe
INFO: [PatchDllFunctionInvokeCall] found (offset=0x0001ac80, call_delta=0xffffffda)
INFO: [PatchEWndV02] found (offset=0x00013a40, ecx=0x004b3658, call_delta=0xffffc787)
INFO: [PatchEWndUltimate] found (offset=0x00038adf, data=0x004816a0, wnd_data_offset=0x000816a0)
- stub added: 0x0048074d (file offset: 0008074d)
INFO: [PatchWndEventHandlerMain] found (fn_end=0001adb6, offset=0x0001ad10, inst=0x0001ad3a(p: 0x0041ad3a), delta=0xffff9ab1)
INFO: [PatchKernelInvokeCall#0] found (offset=0x0001ac90, len=003b, replace_len=002c)
INFO: [PatchKernelInvokeCall#1] found (offset=0x0001acd0, len=0031, replace_len=0025)
INFO: [ELibInvokeCall#0] found (offset=0x0001ac60, args=[1], ecx=0x004b3658, call_delta=0xffff7dbf)
INFO: [ELibInvokeCall#1] found (offset=0x0001b0a0, args=[1], ecx=0x004b3658, call_delta=0xffff718f)
INFO: [LoadInitWindow#0] found (offset=0x0001b020, args=[4], ecx=0x004b3658, call_delta=0xffff8b33)
INFO: [UnknownCtrlRelated#0] found (offset=0x0001b040, args=[6], ecx=0x004b3658, call_delta=0xffff9a4b)
INFO: [PatchLoadWndCall] found (offset=0x000015cb, ebx=0x004017e0, call_delta=0x0000012a)
INFO: [PatchLoadWndCall] found (offset=0x00001007, ebx=0x004017e0, call_delta=0x000006f7)
INFO: [PatchLoadWndCall] found (offset=0x000016aa, ebx=0x004017e0, call_delta=0x00000054)
INFO: [PatchSuspiciousCallWithParam] found (offset=0x000012f1, push_value=0x52010048, call_delta=0x00000457)
INFO: [PatchSuspiciousCallWithParam] found (offset=0x000013a1, push_value=0x52010048, call_delta=0x000003a7)
INFO: [PatchSuspiciousCallWithParam] found (offset=0x00001449, push_value=0x00000006, call_delta=0x000002ed)
INFO: [PatchSuspiciousCallWithParam] found (offset=0x000014d9, push_value=0x00000006, call_delta=0x0000025d)
INFO: [PatchSuspiciousCallWithParam] found (offset=0x00001584, push_value=0x00000006, call_delta=0x000001b2)
INFO: [PatchSuspiciousCallWithParam] found (offset=0x00001626, push_value=0x00000006, call_delta=0x00000110)
INFO: [PatchSuspiciousCallWithParam] found (offset=0x00001686, push_value=0x52010048, call_delta=0x000000aa)
INFO: [PatchSuspiciousCallWithParam] found (offset=0x00001707, push_value=0x52010048, call_delta=0x00000011)
INFO: [PatchELangLoaderInitStub] found (offset=0x000016ed)
INFO: [MiscAddFakeEWndStub] add stub (len=140 bytes)
集成到易语言
- 打开易语言目录;
- 打开该目录下的
tools
目录;
- 将
ELangPatcher.exe
放入该目录;
- 打开
link.ini
配置文件;
- 找到结尾的
post_link_action
区域,并添加新的操作。
- 静态编译后自动处理,参考添加
post_link_action1="$(E_TOOLS)\ELangPatcher.exe" $(TARGET)
;
- 如果有自动加壳,你需要调整序号,让
ELangPatcher.exe
先执行;
部分代码展示
混淆 cld/fninit 特征 (LibELangPatch/ELangInitFnGen.cpp
):
本质上就是抽取 cld; fninit; call xxxx
这串代码到别的地方,并插入随机垃圾代码避免现有特征码定位:
/**
* 0040116D | FC | cld <-- addr start
* 0040116E | DBE3 | fninit
* 00401170 | E8 ECFFFFFF | call exe.401161 <-- call_delta = 0x0xFFFFFFEC
* @param call_delta This can be `{}` if the call is empty.
* @return
*/
std::vector<uint8_t> GenerateELangLoaderInit(std::optional<uint32_t> call_delta);
class ELangLoaderInitGen : public CodeGenHelper {
public:
explicit ELangLoaderInitGen(std::optional<uint32_t> call_delta) {
auto regs = shuffled<Reg32>({eax, edx, ecx});
fillWithJunkSlideInst(rand_int(1, 5), regs);
shuffle_exec({
[&]() { cld(); genJunk(regs); },
[&]() { fninit(); genJunk(regs); },
});
fillWithJunkSlideInst(rand_int(1, 5), regs);
bool use_ret_trick{false};
if (call_delta) {
use_ret_trick = next_bool();
auto reg_ret_addr = pop_last_item(regs);
mov(reg_ret_addr, dword[esp]);
genJunk(regs);
if (use_ret_trick) {
add(dword[esp], 3);
genJunk(regs);
}
auto delta_signed = static_cast<int32_t>(*call_delta);
if (delta_signed > 0) {
add(reg_ret_addr, delta_signed);
} else {
sub(reg_ret_addr, -delta_signed);
}
genJunk(regs);
if (use_ret_trick) {
jmp(reg_ret_addr);
} else {
call(reg_ret_addr);
}
}
regs = shuffled<Reg32>({eax, edx, ecx});
if (!use_ret_trick) {
genJunk(regs);
add(dword[esp], 3);
}
genJunk(regs);
ret();
std::vector<uint8_t> junk(rand_int(4, 10));
std::generate(junk.begin(), junk.end(), mt_);
db(junk.data(), junk.size());
}
};
std::vector<uint8_t> GenerateELangLoaderInit(std::optional<uint32_t> call_delta) {
return ELangLoaderInitGen{call_delta}.vec();
}
扩充 .text
段,若无剩余空间则建立新的 .txt2
段储存代码 (src/PEParser.h
):
inline uint8_t *ExpandTextSection(uint32_t size) {
auto p_nt_header = INNER_PIMAGE_NT_HEADERS((uint8_t *) (exe_data_.data()) + PIMAGE_DOS_HEADER(exe_data_.data())->e_lfanew);
auto p_file_header = &p_nt_header->FileHeader;
auto section_count = std::size_t(p_file_header->NumberOfSections);
auto section = (PIMAGE_SECTION_HEADER) ((uint8_t *) (p_nt_header) + offsetof(INNER_IMAGE_NT_HEADERS, OptionalHeader) + p_nt_header->FileHeader.SizeOfOptionalHeader);
auto &image_size = GetNtOptionalHeader()->SizeOfImage;
for (std::size_t i = 0; i < section_count; i++) {
if (strcmp((char *) section->Name, ".text") == 0) {
if (section->Misc.VirtualSize + size < section->SizeOfRawData) {
auto offset = section->Misc.VirtualSize;
section->Misc.VirtualSize += size;
return &exe_data_.at(section->PointerToRawData + offset);
}
} else if (strcmp((char *) section->Name, ".txt2") == 0) {
exe_data_.resize(exe_data_.size() + size);
auto offset = section->SizeOfRawData;
section->SizeOfRawData += size;
image_size -= section->Misc.VirtualSize;
section->Misc.VirtualSize = helper::round_up_to_section_size(section->SizeOfRawData);
image_size += section->Misc.VirtualSize;
return &exe_data_.at(section->PointerToRawData + offset);
}
section++;
}
auto last_section = §ion[-1];
p_file_header->NumberOfSections++;
memset(section, 0, sizeof(*section));
memcpy(section->Name, ".txt2", 6);
section->PointerToRawData = exe_data_.size();
section->SizeOfRawData = size;
section->Misc.VirtualSize = helper::round_up_to_section_size(size);
image_size = helper::round_up_to_section_size(image_size) + section->Misc.VirtualSize;
section->VirtualAddress = helper::round_up_to_section_size(last_section->VirtualAddress + last_section->Misc.VirtualSize);
section->Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_CODE;
exe_data_.resize(exe_data_.size() + size);
return &exe_data_.at(exe_data_.size() - size);
}
下载
更新记录
- v0.1.1: 修正
GenerateVArgsProxyCode
的代码生成(如 取余数
传参),感谢 谁的坏叔叔
报告。
- v0.1: 初版发布
结语
代码写得比较乱,大概率不会有后续更新了…
拿 Xbyak
生成字节码,调试错误的时候也是磕磕碰碰。因为都是手动插入的垃圾指令,早期写得那些代码生成的混淆“相对温和”。后期整得有点走火入魔,也尝试了下隐藏常数。
一开始就想着对抗下 “一键 PUSH”,这个目的倒是达到了。不过其它的特征… 手动根本就处理不完。
在 .text
外的内容如果有匹配上特征码也会尝试进行魔改。读者有兴趣可以限制特征码检索为 .text
区段的代码。
不过,这些混淆/膨胀的手段在 IDA 的帮助下分析起来倒是不怎么费劲就是…
碎碎念
- 易语言的编译顺序太“稳定”了,可以通过定位目标函数附近的函数来快速定位。
- 没处理过特征的函数依然可以手动检索特征码找到。
- 再做下去感觉需要自动化识别函数、反编译、然后随机混淆/膨胀了。
致谢