软件安全 | shellcode

Overview

这篇文章主要记录如何在Windows x64环境下编写和调用shellcode

Shellcode

Shellcode通常是指一个原始的可执行代码的有效载荷,攻击者通常会使用这段代码来获取被攻陷系统上的交互Shell的访问权限,Shellcode代码的编写有多种形式,在这里我们尝试手动写汇编shellcode

shellcode通常会与漏洞利用并肩使用,或是被恶意代码用于执行进程代码的注入,通常情况下shellcode无法独立运行,必须依赖父进程或是Windows文件加载器才能够被运行,我们在这里写一个简单的弹窗(MessageBox)

由于Windows上的pwn我打的很少,所以这次编写shellcode也是在摸索,有不对的地方还请执教

假设场景

因为shellcode通常与漏洞伴随,我们在这里假设一些前置条件:

  • 程序有漏洞,可以控制程序的执行流
  • 我们有办法读出kernel32.dll的基地址
  • 其余情况皆为默认,默认不加载user32.dll
  • 有足够的空间输入我们的shellcode(当然我会尽量写的短一点)
  • 我们传入shellcode的地方有可执行权限

我这里有一个模板程序,简化了真实场景

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

unsigned char shellcode_bytes[] = {};    // 写入自己的shellcode
int main() {
    SIZE_T sc_size = sizeof(shellcode_bytes);

    // 在运行时申请可执行内存并复制 shellcode
    void *exec_mem = VirtualAlloc(NULL, sc_size,
                                  MEM_COMMIT | MEM_RESERVE,
                                  PAGE_READWRITE);
    if (!exec_mem) {
        fprintf(stderr, "VirtualAlloc failed: %lu\n", GetLastError());
        return 1;
    }

    // 复制 bytes,然后设置为可执行(DEP 兼容)
    memcpy(exec_mem, shellcode_bytes, sc_size);

    DWORD oldProt;
    if (!VirtualProtect(exec_mem, sc_size, PAGE_EXECUTE_READ, &oldProt)) {
        fprintf(stderr, "VirtualProtect failed: %lu\n", GetLastError());
        VirtualFree(exec_mem, 0, MEM_RELEASE);
        return 1;
    }

    // 强制刷新指令缓存(通常不必要,但保险)
    FlushInstructionCache(GetCurrentProcess(), exec_mem, sc_size);

    // 调用 shellcode
    typedef void (*sc_t)(void);
    sc_t sc = (sc_t)exec_mem;
    sc();

    // 释放内存
    VirtualFree(exec_mem, 0, MEM_RELEASE);
    return 0;
}
C

查找DLL库函数地址

注意我们的假设第2条,我们有办法读出kernel32.dll的基地址,那么我们就有办法找到LoadLibraryA,从而找到user32.dll,接着找到MessageBox()

要获取这些函数的地址,我们需要先运行一下以下脚本:

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

typedef void(*MYPROC)(LPTSTR);

int main(int argc, char *argv[]) {
    HINSTANCE LibAddr, KernelAddr;
    MYPROC ProcAddr;

    LibAddr = LoadLibrary("user32.dll");
    printf("user32.dll loaded at address: 0x%p\n", LibAddr);

    KernelAddr = LoadLibrary("kernel32.dll");
    printf("kernel32.dll loaded at address: 0x%p\n", KernelAddr);

    ProcAddr = (MYPROC)GetProcAddress(LibAddr, "MessageBoxA");
    printf("MessageBoxA address: 0x%p\n", ProcAddr);

    ProcAddr = (MYPROC)GetProcAddress(KernelAddr, "ExitProcess");
    printf("ExitProcess address: 0x%p\n", ProcAddr);

    ProcAddr = (MYPROC)GetProcAddress(KernelAddr, "LoadLibraryA");
    printf("LoadLibraryA address: 0x%p\n", ProcAddr);

    system("pause");
    return 0;
}
C

这个脚本主要是加载user32.dllkernel32.dll,然后找到MessageBoxA()ExitProcess()LoadLibraryA()的地址

运行之后的结果如下

注意此处的地址会因为ALSR在不同机器和不同机器启动时间不同,但是在机器开机后所有程序的这些地址是不变的,这里涉及到Windows自己的机制,同时user32.dllkernel32.dll是共享dll,这就导致他们对所有程序来说,虚拟地址都是不变的,这也就导致我们可以先获取这些地址,在shellcode中直接硬编码就行了,每次开机重新运行这个代码,patch我们的shellcode就可以修复了

整体思路

在我们获取了这些库函数的地址之后,我们就需要一个清晰的思路来编写我们的shellcode

我们的目标是调用MessageBoxA()函数,而这个函数在user32.dll中,不一定每个程序都会加载这个库,那我们就需要首先调用LoadLibraryA("user32.dll")来加载这个库,并获取到user32.dll的基地址,然后就能根据偏移找到MessageBoxA()函数的地址了

最后调用ExitProcess()退出

LoadLibraryA("user32.dll")   ->
MessageBoxA(null, "text", "title", flags)  -> 
ExitProcess(0)
C

x64的调用方式

在编写shellcode之前,我们还需要理解x64的调用方式:

参数类型第 5 个和更高位置第 4 个第3 个最左侧
浮点堆栈XMM3XMM2XMM1XMM0
整数堆栈R9R8RDXRCX
聚合(8、16、32 或 64 位)和 __m64堆栈R9R8RDXRCX
其他聚合,作为指针堆栈R9R8RDXRCX
__m128,作为指针堆栈R9R8RDXRCX

我们要关注的是整数的调用方式,可以看出第一个参数是rcx,第二个参数是rdx,一次类推

在知道了这个之后我们就可以快乐的编写shellcode

编写shellcode

LoadLibraryA

首先是LoadLibraryA()函数,

HMODULE LoadLibraryA(
  [in] LPCSTR lpLibFileName
);
C

这是函数定义,传递的是一个字符串指针,指向目标字符串的地址,我们可以把字符串放在栈上

首先把user32.dll这个字符串转换为字节码

"user32.dll"  \x75\x73\x65\x72\x33\x32\x2e\x64
              \x6c\x6c\x00\x00\x00\x00\x00\x00
C

注意小端存储

mov rdx, 0x6c6c
push rdx
mov rdx, 0x642e323372657375
push rdx
mov rcx, rsp      ; 获取这个字符串在栈上的地址
ASM

调用LoadLibraryA()函数

push rax
sub rsp, 32                      ; 注意预留32位的空间和栈对齐
mov rax, 0x00007FFD68602D80      ; 硬编码的LoadLibraryA函数的入口地址,注意patch
call rax
ASM


这是执行情况

这是函数调用情况,可以发现传递的参数是我们的目标字符串

执行之后rax返回user32.dll的地址,然后我们就可以通过偏移量计算出MessageBoxA()的地址

MessgaeBoxA

user32.dll中可以发现偏移量是0x8D230

那么我们只需要将user32.dll的基地址加上0x8D230就行了

mov rbx, rax
add rbx, 0x8D230
ASM

接着传递我们的参数就可以了,先放出MessageBoxA()的参数

int MessageBox(
  [in, optional] HWND    hWnd,
  [in, optional] LPCTSTR lpText,
  [in, optional] LPCTSTR lpCaption,
  [in]           UINT    uType
);
C

汇编代码如下

xor rcx, rcx            ; hWnd = NULL

mov rdx, 0x2020207472656c61
push rdx
mov r8, rsp            ; lpCaption = "alert"

xor rdx, rdx
push rdx

mov rdx, 0x2020202020207972
push rdx
mov rdx, 0x61473a3737313138
push rdx
mov rdx, 0x3132303333323032
push rdx
mov rdx, rsp            ; lpText = "2023302181177:Gary" 

push rax
mov rax, rbx
call rax
ASM


在这里call,观察寄存器

rax的地方正好就是我们的MessageBoxA()的地址,rcx是第一个参数,rdx是第二个参数

这是要打印的东西,执行之后就能成功打印了

但是打印完成之后程序会崩溃,因为后面没有再写的东西了,那我们有办法让它正常结束吗,当然是有的,要么跳转回原程序的下一条地址,要么让shellcode再执行一个结束函数,毕竟它是shellcode

exit

我们退出可以调用kernel32.dll中的ExitProcess(),它的地址也可以通过kernel32.dll的基地址加偏移计算出来,因此我们直接硬编码,简化过程

VOID ExitProcess(
  [in] UINT uExitCode
);
C

那就很简单了,直接放出代码

xor rcx, rcx
mov rax, 0x00007FFA982418A0
call rax
ASM


完整代码

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

unsigned char shellcode_bytes[] = { 0x48, 0xC7, 0xC2, 0x6C, 0x6C, 0x00, 0x00, 0x52, 0x48, 0xBA, 0x75, 0x73, 0x65, 0x72, 0x33, 0x32, 0x2E, 0x64, 0x52, 0x48, 0x89, 0xE1, 0x50, 0x48, 0x83, 0xEC, 0x20, 0x48, 0xB8, 0x80, 0x2D, 0x24, 0x98, 0xFA, 0x7F, 0x00, 0x00, 0xFF, 0xD0, 0x48, 0x89, 0xC3, 0x48, 0x81, 0xC3, 0x30, 0xD2, 0x08, 0x00, 0x48, 0x31, 0xC9, 0x48, 0xBA, 0x61, 0x6C, 0x65, 0x72, 0x74, 0x20, 0x20, 0x20, 0x52, 0x49, 0x89, 0xE0, 0x48, 0x31, 0xD2, 0x52, 0x48, 0xBA, 0x72, 0x79, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x52, 0x48, 0xBA, 0x38, 0x31, 0x31, 0x37, 0x37, 0x3A, 0x47, 0x61, 0x52, 0x48, 0xBA, 0x32, 0x30, 0x32, 0x33, 0x33, 0x30, 0x32, 0x31, 0x52, 0x48, 0x89, 0xE2, 0x50, 0x48, 0x89, 0xD8, 0xFF, 0xD0, 0x48, 0x31, 0xC9, 0x48, 0xB8, 0xA0, 0x18, 0x24, 0x98, 0xFA, 0x7F, 0x00, 0x00, 0xFF, 0xD0 };
int main() {
    SIZE_T sc_size = sizeof(shellcode_bytes);

    // 在运行时申请可执行内存并复制 shellcode
    void *exec_mem = VirtualAlloc(NULL, sc_size,
                                  MEM_COMMIT | MEM_RESERVE,
                                  PAGE_READWRITE);
    if (!exec_mem) {
        fprintf(stderr, "VirtualAlloc failed: %lu\n", GetLastError());
        return 1;
    }

    // 复制 bytes,然后设置为可执行(DEP 兼容)
    memcpy(exec_mem, shellcode_bytes, sc_size);

    DWORD oldProt;
    if (!VirtualProtect(exec_mem, sc_size, PAGE_EXECUTE_READ, &oldProt)) {
        fprintf(stderr, "VirtualProtect failed: %lu\n", GetLastError());
        VirtualFree(exec_mem, 0, MEM_RELEASE);
        return 1;
    }

    // 强制刷新指令缓存(通常不必要,但保险)
    FlushInstructionCache(GetCurrentProcess(), exec_mem, sc_size);

    // 调用 shellcode
    typedef void (*sc_t)(void);
    sc_t sc = (sc_t)exec_mem;
    sc();

    // 释放内存
    VirtualFree(exec_mem, 0, MEM_RELEASE);
    return 0;
}

/* call chain
LoadLibraryA("user32.dll")      // ensure the user32.dll is loaded
MessageBox(
    NULL,
    text,
    title,
    flags)
ExitProcess(0)
*/


/* print string
\x61\x6c\x65\x72\x74\x20\x20\x20

\x32\x30\x32\x33\x33\x30\x32\x31
\x38\x31\x31\x37\x37\x3a\x47\x61
\x72\x79\x20\x20\x20\x20\x20\x20

push 0x2020207472656c61

push 0x3132303333323032
push 0x61473a3737313138
push 0x2020202020207972
*/

// ===================================================

/* exit process
xor rcx, rcx
mov rax, 0x00007FFA982418A0
call rax
*/

// ===================================================

/* messagebox
mov rbx, rax
add rbx, 0x8D230

xor rcx, rcx            ; hWnd = NULL

mov rdx, 0x2020207472656c61
push rdx
mov r8, rsp            ; lpCaption = "alert"

xor rdx, rdx
push rdx

mov rdx, 0x2020202020207972
push rdx
mov rdx, 0x61473a3737313138
push rdx
mov rdx, 0x3132303333323032
push rdx
mov rdx, rsp            ; lpText = "2023302181177:Gary" 

push rax
mov rax, rbx
call rax
*/

// ====================================================

// "user32.dll"  \x75\x73\x65\x72\x33\x32\x2e\x64
//               \x6c\x6c\x20\x20\x20\x20\x20\x20

/* LoadLibrary
mov rdx, 0x6c6c
push rdx
mov rdx, 0x642e323372657375
push rdx
mov rcx, rsp      ; 获取这个字符串在栈上的地址

push rax
sub rsp, 32                      ; 注意预留32位的空间和栈对齐
mov rax, 0x00007FFD68602D80      ; 硬编码的LoadLibraryA函数的入口地址,注意patch
call rax
*/

// =====================================================

/* the complete shellcode call chain
; LoadLibraryA("user32.dll")      // ensure the user32.dll is loaded
mov rdx, 0x6c6c
push rdx
mov rdx, 0x642e323372657375
push rdx
mov rcx, rsp

push rax
sub rsp, 32
mov rax, 0x00007FFA98242D80
call rax

; MessageBox(null, text, title, flags)
mov rbx, rax
add rbx, 0x8D230

xor rcx, rcx           

mov rdx, 0x2020207472656c61
push rdx
mov r8, rsp           

xor rdx, rdx
push rdx

mov rdx, 0x2020202020207972
push rdx
mov rdx, 0x61473a3737313138
push rdx
mov rdx, 0x3132303333323032
push rdx
mov rdx, rsp           

push rax
mov rax, rbx
call rax

; ExitProcess(0)
xor rcx, rcx
mov rax, 0x00007FFA982418A0
call rax
*/
C

最后总共是 127 个字节的shellcode

总结

这次尝试写了Windows x64下的简单shellcode,已知条件只有kernel32.dll的基地址,总体来说还算可以,完成了目标任务


实验有一个附加题是选用gadget-chain实现,在我理解来说就是rop链了,感觉和shellcode没什么关系了,就不在此赘述,等到后面有rop专题的时候再来讲吧

🙂 lucky pwning

评论

  1. Daik321
    Windows Edge
    1 月前
    2025-10-26 23:14:31

    非常好的盖瑞

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇