ELangPatcher - 一个简单的基于特征码的易语言静态编译程序膨胀与混淆工具

科技   科技   2024-03-28 14:42   北京  

作者坛账号:爱飞的猫


针对易语言静态编译的代码进行轻微混淆处理,避免被插件一键识别部分关键函数。处理过的内容参考下方。

注意该轻微膨胀/混淆只能用来对抗现有的“一键识别”工具,不能和加密壳的效果比。

如果你能拿到这类工具的源码缝缝补补,应该也能让它重新识别。

原理

对已知的部分特征进行魔改,大部分时候都是对简单操作进行膨胀,然后开辟新的内存空间跳转过去。

因为生成代码计算绝对地址比较麻烦,所以依赖 CALL 指令自动入栈的返回地址来计算正确跳转位置。

处理过的特征

只处理了一小部分特征。手动整起来太麻烦了,感觉不如利用特征码自动标记然后上加密壳批量处理了。

  • 对抗 查找易语言按钮事件 插件

  • 对抗 EWnd v0.2 插件的一键分析

  • 对抗 EWnd Ultimate 插件的一键分析

  • 对抗 E-Debug 程序/插件的一键分析

  • 对抗 E-Decompiler 插件的一键分析

  • 对抗 易语言逆向分析助手 的窗体信息分析

  • 对抗易语言初始化入口识别(cld; fninit; call xxxx

  • 对抗控件处理事件识别(重写了个简单的,混淆程度不高 call dword[ebp - 4]

  • 处理了找到的一些乱七八糟的特征…

从源码构建

首先确保安装有 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)

集成到易语言

  1. 打开易语言目录;

  2. 打开该目录下的 tools 目录;

  3. 将 ELangPatcher.exe 放入该目录;

  4. 打开 link.ini 配置文件;

  5. 找到结尾的 post_link_action 区域,并添加新的操作。

    1. 静态编译后自动处理,参考添加 post_link_action1="$(E_TOOLS)\ELangPatcher.exe" $(TARGET)

    2. 如果有自动加壳,你需要调整序号,让 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);
            }

下载

  • 程序: 

     ELangPatcher-0.1.1.zip 见左下角论坛原文
  • 源码:https://github.com/FlyingRainyCats/ELangPatcher | 

     ELangPatcher-0.1.1-src.zip 见左下角论坛原文
  • 百度网盘存档/测试程序 https://pan.baidu.com/s/1Jlbqt8K1fIG6PPbfrrEI_A?pwd=q565

更新记录

  • v0.1.1: 修正 GenerateVArgsProxyCode 的代码生成(如 取余数 传参),感谢 谁的坏叔叔 报告。

  • v0.1: 初版发布

结语

代码写得比较乱,大概率不会有后续更新了…

拿 Xbyak 生成字节码,调试错误的时候也是磕磕碰碰。因为都是手动插入的垃圾指令,早期写得那些代码生成的混淆“相对温和”。后期整得有点走火入魔,也尝试了下隐藏常数。

一开始就想着对抗下 “一键 PUSH”,这个目的倒是达到了。不过其它的特征… 手动根本就处理不完。

在 .text 外的内容如果有匹配上特征码也会尝试进行魔改。读者有兴趣可以限制特征码检索为 .text 区段的代码。

不过,这些混淆/膨胀的手段在 IDA 的帮助下分析起来倒是不怎么费劲就是…

碎碎念

  • 易语言的编译顺序太“稳定”了,可以通过定位目标函数附近的函数来快速定位。

  • 没处理过特征的函数依然可以手动检索特征码找到。

  • 再做下去感觉需要自动化识别函数、反编译、然后随机混淆/膨胀了。

    • 要考虑的东西太多了,不适合我。

致谢

  • fjqisba 老师的易语言逆向专栏

  • 易语言程序分析笔记 - 看雪/PlaneJun


-官方论坛

www.52pojie.cn


👆👆👆

公众号设置“星标”,不会错过新的消息通知
开放注册、精华文章和周边活动等公告

吾爱破解论坛
吾爱破解论坛致力于软件安全与病毒分析的前沿,丰富的技术版块交相辉映,由无数热衷于软件加密解密及反病毒爱好者共同维护,留给世界一抹值得百年回眸的惊艳,沉淀百年来计算机应用之精华与优雅,任岁月流转,低调而奢华的技术交流与探索却是亘古不变。
 最新文章