CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows x64 ShellCode入门教程
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows x64 ShellCode入门教程
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Windows x64 ShellCode入门教程
  • Windows x64 ShellCode入门教程 01
    • 第1部分 - x64 基础知识:寄存器
    • 第1部分 - x64基础知识:栈对齐
    • 第1部分 - x64 基础:影子空间(Shadow Space)
    • 第2部分 - x64首个程序:动态定位WinExec并执行calc.exe
    • 第3部分 - 转换为x64 shellcode:执行自定义shellcode
  • Windows x64 ShellCode入门教程 02
  • Windows x64 ShellCode入门教程 03
  • Windows x64 ShellCode入门教程 04
  • Windows x64 ShellCode入门教程 05
  • Windows x64 ShellCode入门教程程 06
  • Windows x64 ShellCode入门教程 07
目录

Windows x64 ShellCode入门教程 01

# Windows x64 ShellCode入门教程 01

必须承认,我在互联网上四处搜寻x64基础 shellcode 开发的示例,但收获不大。许多教程和课程似乎仍侧重于x86汇编,甚至许多现代的shellcode课程也仍以x86为主。别误会,x86很棒,学习曲线也没那么陡峭。但在你的渗透测试之旅中,大多数有效载荷将基于x64架构,这是有区别的!我希望提供一系列循序渐进的课程,帮助读者你获得必要的资源和知识,以便顺利学习x64汇编/shellcode开发,尽量少走弯路。那么,我们开始吧,好吗?

声明 - 说到x64汇编语言,我可不是专家。但我所掌握的知识,至少足以指导那些有兴趣的人学习基础知识,并生成可用于漏洞利用开发、逆向工程概念和渗透测试项目的可用shellcode。

最后,NASM(The Netwide Assembler)汇编语法将作为我们x64汇编编码需求的首选语法。让我们开始吧!🐱

# 第1部分 - x64 基础知识:寄存器

好的,让我们先把那些枯燥但至关重要的信息讲清楚。在x64汇编中,有两种类型的寄存器值:

  • 易失性(Volatile)寄存器:适用于寄存器RAX、RCX、RDX、R8、R9、R10、R11
  • 非易失性(Non-Volatile)寄存器:RBX、RBP、RDI、RSI、R12、R13、R14、R15、RSP

Volatile寄存器正如其名,会根据函数调用等情况改变值。

Non-Volatile寄存器在函数调用后不会改变值,并且可以可靠地用于存储你的代码中需要的数值。

寄存器 RCX、RDX、R8 和 R9 按此确切顺序用作参数。例如,当你执行 ExitProcess 并将第一个参数 0 传递给函数调用时,你会使用寄存器 RCX,如下所示:

; void ExitProcess(UINT uExitCode);
mov r15, rax ;address for ExitProcess previously acquired
mov rcx, 0   ;move '0' into the first and only expected parameter
call r15     ;Execute ExitProcess!!!
1
2
3
4

如果有多个参数怎么办呢?嗯,那会将RCX用作第一个参数,RDX用作第二个参数。如果你还有第三个和第四个参数值,那么将分别使用r8和r9。以下是WinExec的x64汇编代码,将应用程序字符串传入RCX,将值“1”传入RDX。如果应用程序有窗口/图形用户界面要显示,1就相当于“显示窗口”。

; UINT WinExec(LPCSTR lpCmdLine, UINT uCmdShow);
pop r15                         ;address for WinExec previously acquired
mov rax, 0x00                   ;NULL byte
push rax                        ;push to stack
mov rax, 0x6578652E636C6163     ;calc.exe 
push rax                        ;push to stack
mov rcx, rsp	                ; RCX, our first parameter, now points to the string of the application we wish to execute: "calc.exe"
mov rdx, 1                      ; move 1 into RDX as the 2nd parameter to display the application's GUI/window
sub rsp, 0x30                   ; I'll explain this in greater detail later.  It involves shadow space/16 byte stack alignment
call r15                        ; Execute WinExec!!!
1
2
3
4
5
6
7
8
9
10

四个参数都用上怎么样?我们可以用MessageBoxA来演示一下:

; int MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
mov r15, rax                   ; MessageBoxA address previously acquired
mov rcx, 0                     ; 1st Parameter - hWnd = NULL (no owner window)
mov rax, 0x006D                ; move the final letter, m, into RAX and null terminate with a '0'
push rax                       ; push 'm' and 0 to the stack, pointed to by RAX
mov rax, 0x3374737973743367    ; move the first 8 characters of the string 'g3tsyst3' into RAX.  
push rax                       ; push 'g3tsyst3' string to the stack, pointed to by RAX
mov rdx, rsp                   ; 2nd Parameter - lpText = pointer to message
    
mov r8, rsp                    ; 3rd Parameter - lpCaption = pointer to title
mov r9d, 0                     ; 4th Parameter - uType = MB_OK (OK button only)

sub rsp, 0x30                  ;I'll explain this in greater detail later.  It involves shadow space/16 byte stack alignment
call r15                       ; Call MessageBoxA
1
2
3
4
5
6
7
8
9
10
11
12
13
14

请注意我是如何使用寄存器 R15 来存储API的地址值的。我选择这个寄存器是因为和它的其他同类寄存器R14、R13和R12一样,它是非易失性的,这意味着在函数调用后它不会被改变。当你需要保留一个尚未压入堆栈的值时,这些非易失性寄存器至关重要。这里有一个函数调用前后寄存器值的示例。请注意,所有易失性寄存器的值都如预期般发生了变化,但R15保持不变。

调用前:

函数调用结束后:

好的!以上就是对x64寄存器的大致解析。接下来继续!

# 第1部分 - x64基础知识:栈对齐

我保证,枯燥的内容就快讲完了。有趣的部分马上就来。😺 好啦,继续往下讲。咱们来聊聊 16 byte stack alignment convention。如果你觉得这听起来像天书,别担心,虽然实现起来有点繁琐,但相当简单明了。我会尽可能简单地讲解。

在x64汇编中,栈以16字节边界运行。在进行函数调用之前,栈需要根据这一原则进行对齐。

简单来说,在进行函数调用之前,RSP必须能被16整除。

不必只关注16字节对齐时RSP的具体值,你可以将此要求理解为栈指针(RSP)需要位于任何能被16整除的地址上(即0x10、0x20、0x30等)。这意味着,任何使得RSP % 16 == 0的 RSP 值都被视为是对齐的。

整除示例:

PUSH和CALL是会使栈指针递减8字节的指令示例。POP会使栈指针递增8字节。这将改变栈对齐方式。例如:

执行POP指令之前,RSP的十位数值是0x88,即十进制的136。这个数不能被16整除(136/16 = 8.5)。 然而…

在执行POP指令后(该指令会使RSP增加8字节),栈又恢复到了16字节对齐的状态。

现在,RSP的十位保存着十六进制值0x90,十进制为144,且能被16整除(144/16 = 9)!仔细想想,这本质上非常数学化。不管你喜欢还是讨厌,这都是x64汇编的一部分,但它并不像看上去那么痛苦。在整个代码过程中,最好让堆栈保持对齐状态,不过在函数调用之前保持对齐尤为重要。如果堆栈没有正确对齐,你的代码很可能会跳转到内存中意想不到的位置并导致失败。

# 第1部分 - x64 基础:影子空间(Shadow Space)

好的,希望你还能坚持读到这里,而且目前为止一切都能理解。如果还有什么不太明白的地方,可以给我发私信。好了,我保证我们马上就要讲完这篇文章x64基础部分啦!🐶 现在我们来谈谈Shadow Space,也叫home space / 或叫spill space。

在Windows x64调用约定中,调用方需要为被调用方预留32字节(4个8字节的槽位)作为shadow space,即使函数并不需要它。这个空间是预留的,但不会自动调整,除非使用诸如sub rsp, 0x20或其他指令显式处理。

函数经常需要额外的栈空间来存放局部变量并进行进一步对齐。你可能会看到sub rsp, 0x30,甚至像sub rsp, 0x40这样更大的调整,以便在函数调用前分配影子空间和额外空间。我在自己的代码中经常这样做。再次强调,这有助于确保当函数需要将预期值以及可能的非预期值压入栈中时,有足够的空间,并且有助于确保RSP始终保持16字节对齐。这里有一个示意图,能帮助你更好地理解这一点。

首先,我将注释掉函数调用前的影子空间分配,看看会发生什么:

GetProcAddress(hKernel32, "LoadLibraryA");
1

然后编译它(我喜欢用ld.exe来编译我的x64汇编代码):

>nasm -fwin64 getproc.asm
>ld -m i386pep -o getproc.exe getproc.obj
1
2

RCX存放着kernel32的基地址,RDX存放着指向我们的"LoadLibraryA"字符串的指针,而 R15存放着GetProcAddress的地址:

如果我们在函数调用前完全忽略设置任何影子空间,似乎通常会放入RAX的返回值就无法正常工作。如果函数调用后RAX为0,这通常不是好事。我们放在栈上的参数和其他数据,在没有我们应该提供给函数的正常预留空间的情况下,很可能被破坏了。看看这个:

函数调用前:

函数调用后:

好吧,这证明了如果不设置适当的影子空间储备,事情会变得多么糟糕。我们现在就这么做,看看情况会不会对我们更有利:😸

现在,我们将添加影子空间,在同一位置重新编译和反汇编程序,看看会发生什么:

中啦! 就在这儿,如我们所愿找到了LoadLibraryA这个Windows API的地址。 就在那里,在RAX寄存器中热切地等待着我们。 你还会看到我们对影子空间堆栈的调整:

我可以不断地讲述减轻潜在影子空间问题的方法。但这会让你很好地了解预期情况,以及如何为使用x64 16字节堆栈对齐和影子空间要求的函数调用做准备。既然我们已经对x64汇编的寄存器和堆栈对齐要求有了一个很好的概述,那么让我们深入到这篇文章的下一部分。

# 第2部分 - x64首个程序:动态定位WinExec并执行calc.exe

在处理完所有必要的乏味事务后,我们终于要开始做令人兴奋的事了。

好的,我将基于这样一个假设,即你已经熟悉了一些常规的x64指令。如果还不熟悉,也不用担心!我会添加注释,以帮助解释你应该熟悉的最常见指令,并帮助你理解它们的工作原理。此外,我还假设你知道定位kernel32基地址以及遍历PE文件(可执行文件)的导出表以查找函数/API名称的序号的基本模板是什么。如果有机会,我建议你熟悉一下PE导出表,但目前你可以直接在我的模板基础上进行操作。

让我们从定位kernel32基地址开始。这实际上非常简单!

;nasm -fwin64 [x64findkernel32.asm]
;ld -m i386pep -o x64findkernel32.exe x64findkernel32.obj

BITS 64
SECTION .text
global main
main:

sub rsp, 0x28
and rsp, 0xFFFFFFFFFFFFFFF0
xor rcx, rcx             ;RCX = 0
mov rax, [gs:rcx + 0x60] ;RAX = PEB
mov rax, [rax + 0x18]    ;RAX = PEB / Ldr
mov rsi,[rax+0x10]       ;PEB_Ldr / InLoadOrderModuleList
mov rsi, [rsi]           ;could substitute lodsq here instead if you like
mov rsi,[rsi]            ;also could substitute lodsq here too
mov rbx, [rsi+0x30]      ;kernel32.dll base address
mov r8, rbx              ;mov kernel32.dll base addr into register of your choosing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

好的,kernel32 base address 现在存于 r8 中。r8 是一个 易失性 寄存器,所以如果你需要多次使用这个寄存器,一定要将该寄存器保存的值转移到另一个寄存器中,因为在你第一次调用函数后,该值几乎肯定会被覆盖。我们来测试一下,看看是否能获取到kernel32基地址。果然,它就在 RBX 中,并且也在我们复制到的 R8 中:

既然我们已经获取了kernel32的基地址,那我们接着获取函数总数以及相对虚拟地址(RVA)/虚拟内存地址(VMA)的信息:

;Code for parsing Export Address Table
mov ebx, [rbx+0x3C]           ; Get Kernel32 PE Signature (0x3C) into EBX
add rbx, r8                   ; signature offset
mov edx, [rbx+0x88]           ; PE32 Signature / Export Address Table
add rdx, r8                   ; kernel32.dll + RVA ExportTable = ExportTable Address
mov r10d, [rdx+0x14]          ; Total count for number of functions
xor r11, r11                  ; clear R11 
mov r11d, [rdx+0x20]          ; AddressOfNames = RVA
add r11, r8                   ; AddressOfNames = VMA
1
2
3
4
5
6
7
8
9

接下来,让我们插入我们要查找的函数名,并设置函数计数器:

mov rcx, r10                  ; Setup loop counter

mov rax, 0x00636578456E6957   ;"WinExec" string NULL terminated with a '0' 
push rax                      ;push to the stack
mov rax, rsp	              ;move stack pointer to our WinExec string into RAX
add rsp, 8                    ;keep with 16 byte stack alignment
1
2
3
4
5
6

现在,让我们找出 WinExec 函数:

; Loop over Export Address Table to find WinApi names
kernel32findfunction: 
    jecxz FunctionNameNotFound    ; If ecx is zero (function not found), set breakpoint
    xor ebx,ebx                   ; Zero EBX
    mov ebx, [r11+rcx*4]          ; EBX = RVA for first AddressOfName
    add rbx, r8                   ; RBX = Function name VMA / add kernel32 base address to RVA to get WinApi name
    dec rcx                       ; Decrement our loop by one, this goes from Z to A
   
    mov r9, qword [rax]                ; R9 = "WinExec"
    cmp [rbx], r9                      ; Compare all bytes
    jz FunctionNameFound               ; jump if zero flag is set (found function name!)
	jnz kernel32findfunction             ; didn't find the name, so keep loopin til we do!

FunctionNameFound:
push rcx                               ; found it, so save it for later
jmp OrdinalLookupSetup

FunctionNameNotFound:
int3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

现在是代码的最后一部分:

OrdinalLookupSetup:  ;We found our target WinApi position in the functions lookup
   pop r15           ;getprocaddress position
   js OrdinalLookup
   
OrdinalLookup:   
   mov rcx, r15                  ; move our function's place into RCX
   xor r11, r11                  ; clear R11 for use
   mov r11d, [rdx+0x24]          ; AddressOfNameOrdinals = RVA
   add r11, r8                   ; AddressOfNameOrdinals = VMA
   ; Get the function ordinal from AddressOfNameOrdinals
   inc rcx
   mov r13w, [r11+rcx*2]         ; AddressOfNameOrdinals + Counter. RCX = counter
   ;With the function ordinal value, we can finally lookup the WinExec address from AddressOfFunctions.

   xor r11, r11
   mov r11d, [rdx+0x1c]          ; AddressOfFunctions = RVA
   add r11, r8                   ; AddressOfFunctions VMA in R11. Kernel32+RVA for function addresses
   mov eax, [r11+r13*4]          ; function RVA.
   add rax, r8                   ; Found the WinExec Api address!!!
   push rax                      ; Store function addresses by pushing it temporarily
   js executeit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

看看我们的WinExec API地址现在是否在RAX中:

果然,它在那里!

现在,让我们使用新找到的WinExec地址来执行calc.exe!

executeit:
; --- prepare to call WinExec ---
pop r15                         ;address for WinExec
mov rax, 0x00                   ;push null string terminator '0'
push rax                        ;push it onto the stack
mov rax, 0x6578652E636C6163     ; move string 'calc.exe' into RAX 
push rax                        ; push string + null terminator to stack
mov rcx, rsp	                ; RDX points to stack pointer "WinExec" (1st parameter))
mov rdx, 1                      ; move 1 (show window parameter) into RDX (2nd parameter)
sub rsp, 0x30                   ; align stack 16 bytes and allow for proper setup for shadow space demands
call r15                        ; Call WinExec!!
1
2
3
4
5
6
7
8
9
10
11

我这里就不给计算器程序拍照啦。相信我,它加载出来了😸 然而!!! 这个编译后的程序不能正常退出,因为我们没有加载 ExitProcess。这可以作为你的作业。试着利用这篇文章中获取的信息,找到定位ExitProcess(它也在kernel32.dll中)的方法,并干净利落地退出这个程序。好了,进入我们的最后一部分……

# 第3部分 - 转换为x64 shellcode:执行自定义shellcode

首先,继续编译它:

nasm.exe -f win64 winexec.asm -o winexec.o
1

这将生成一个.obj文件。现在,只需执行以下操作:

objdump -d winexec.o
1

你应该获取你的shellcode输出以及汇编指令。这是我的输出示例。

Disassembly of section .text:

0000000000000000 <main>:
   0:   48 83 ec 28             sub    $0x28,%rsp
   4:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
   8:   48 31 c9                xor    %rcx,%rcx
   b:   65 48 8b 41 60          mov    %gs:0x60(%rcx),%rax
  10:   48 8b 40 18             mov    0x18(%rax),%rax
  14:   48 8b 70 10             mov    0x10(%rax),%rsi
  18:   48 8b 36                mov    (%rsi),%rsi
  1b:   48 8b 36                mov    (%rsi),%rsi
  1e:   48 8b 5e 30             mov    0x30(%rsi),%rbx
  22:   49 89 d8                mov    %rbx,%r8
  25:   8b 5b 3c                mov    0x3c(%rbx),%ebx
  28:   4c 01 c3                add    %r8,%rbx
  2b:   8b 93 88 00 00 00       mov    0x88(%rbx),%edx
  31:   4c 01 c2                add    %r8,%rdx
  34:   44 8b 52 14             mov    0x14(%rdx),%r10d
  38:   4d 31 db                xor    %r11,%r11
  3b:   44 8b 5a 20             mov    0x20(%rdx),%r11d
  3f:   4d 01 c3                add    %r8,%r11
  42:   4c 89 d1                mov    %r10,%rcx
  45:   48 b8 57 69 6e 45 78    movabs $0x636578456e6957,%rax
  4c:   65 63 00
  4f:   50                      push   %rax
  50:   48 89 e0                mov    %rsp,%rax
  53:   48 83 c4 08             add    $0x8,%rsp
  57:   eb 00                   jmp    59 <kernel32findfunction>

0000000000000059 <kernel32findfunction>:
  59:   67 e3 19                jecxz  75 <FunctionNameNotFound>
  5c:   31 db                   xor    %ebx,%ebx
  5e:   41 8b 1c 8b             mov    (%r11,%rcx,4),%ebx
  62:   4c 01 c3                add    %r8,%rbx
  65:   48 ff c9                dec    %rcx
  68:   4c 8b 08                mov    (%rax),%r9
  6b:   4c 39 0b                cmp    %r9,(%rbx)
  6e:   74 02                   je     72 <FunctionNameFound>
  70:   75 e7                   jne    59 <kernel32findfunction>

0000000000000072 <FunctionNameFound>:
  72:   51                      push   %rcx
  73:   eb 01                   jmp    76 <OrdinalLookupSetup>

0000000000000075 <FunctionNameNotFound>:
  75:   cc                      int3

0000000000000076 <OrdinalLookupSetup>:
  76:   41 5f                   pop    %r15
  78:   78 00                   js     7a <OrdinalLookup>

000000000000007a <OrdinalLookup>:
  7a:   4c 89 f9                mov    %r15,%rcx
  7d:   4d 31 db                xor    %r11,%r11
  80:   44 8b 5a 24             mov    0x24(%rdx),%r11d
  84:   4d 01 c3                add    %r8,%r11
  87:   48 ff c1                inc    %rcx
  8a:   66 45 8b 2c 4b          mov    (%r11,%rcx,2),%r13w
  8f:   4d 31 db                xor    %r11,%r11
  92:   44 8b 5a 1c             mov    0x1c(%rdx),%r11d
  96:   4d 01 c3                add    %r8,%r11
  99:   43 8b 04 ab             mov    (%r11,%r13,4),%eax
  9d:   4c 01 c0                add    %r8,%rax
  a0:   50                      push   %rax
  a1:   78 00                   js     a3 <executeit>

00000000000000a3 <executeit>:
  a3:   41 5f                   pop    %r15
  a5:   b8 00 00 00 00          mov    $0x0,%eax
  aa:   50                      push   %rax
  ab:   48 b8 63 61 6c 63 2e    movabs $0x6578652e636c6163,%rax
  b2:   65 78 65
  b5:   50                      push   %rax
  b6:   48 89 e1                mov    %rsp,%rcx
  b9:   ba 01 00 00 00          mov    $0x1,%edx
  be:   48 83 ec 30             sub    $0x30,%rsp
  c2:   41 ff d7                call   *%r15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

现在让我们提取 shellcode:

for i in $(objdump -D winexec.o | grep “^ “ | cut -f2); do echo -n “\x$i” ; done
1

只提取机器码后的样子如下:

“\x48\x83\xec\x28\x48\x83\xe4\xf0\x48\x31\xc9\x65\x48\x8b\x41\x60\x48\x8b” “\x40\x18\x48\x8b\x70\x10\x48\x8b\x36\x48\x8b\x36\x48\x8b\x5e\x30\x49\x89” “\xd8\x8b\x5b\x3c\x4c\x01\xc3\x8b\x93\x88\x00\x00\x00\x4c\x01\xc2\x44\x8b” “\x52\x14\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4c\x89\xd1\x48\xb8\x57” “\x69\x6e\x45\x78\x65\x63\x00\x50\x48\x89\xe0\x48\x83\xc4\x08\xeb\x00\x67” “\xe3\x19\x31\xdb\x41\x8b\x1c\x8b\x4c\x01\xc3\x48\xff\xc9\x4c\x8b\x08\x4c” “\x39\x0b\x74\x02\x75\xe7\x51\xeb\x01\xcc\x41\x5f\x78\x00\x4c\x89\xf9\x4d” “\x31\xdb\x44\x8b\x5a\x24\x4d\x01\xc3\x48\xff\xc1\x66\x45\x8b\x2c\x4b\x4d” “\x31\xdb\x44\x8b\x5a\x1c\x4d\x01\xc3\x43\x8b\x04\xab\x4c\x01\xc0\x50\x78” “\x00\x41\x5f\xb8\x00\x00\x00\x00\x50\x48\xb8\x63\x61\x6c\x63\x2e\x65\x78” “\x65\x50\x48\x89\xe1\xba\x01\x00\x00\x00\x48\x83\xec\x30\x41\xff\xd7”;
1

现在,这一切的最后一步。让我们将x64 shellcode添加到一个自定义的C++程序中并执行它!

#include <windows.h>
#include <iostream>

unsigned char shellcode[] =
"\x48\x83\xec\x28\x48\x83\xe4\xf0\x48\x31\xc9\x65\x48\x8b\x41\x60"
"\x48\x8b\x40\x18\x48\x8b\x70\x10\x48\x8b\x36\x48\x8b\x36\x48\x8b"
"\x5e\x30\x49\x89\xd8\x8b\x5b\x3c\x4c\x01\xc3\x8b\x93\x88\x00\x00"
"\x00\x4c\x01\xc2\x44\x8b\x52\x14\x4d\x31\xdb\x44\x8b\x5a\x20\x4d"
"\x01\xc3\x4c\x89\xd1\x48\xb8\x57\x69\x6e\x45\x78\x65\x63\x00\x50"
"\x48\x89\xe0\x48\x83\xc4\x08\xeb\x00\x67\xe3\x19\x31\xdb\x41\x8b"
"\x1c\x8b\x4c\x01\xc3\x48\xff\xc9\x4c\x8b\x08\x4c\x39\x0b\x74\x02"
"\x75\xe7\x51\xeb\x01\xcc\x41\x5f\x78\x00\x4c\x89\xf9\x4d\x31\xdb"
"\x44\x8b\x5a\x24\x4d\x01\xc3\x48\xff\xc1\x66\x45\x8b\x2c\x4b\x4d"
"\x31\xdb\x44\x8b\x5a\x1c\x4d\x01\xc3\x43\x8b\x04\xab\x4c\x01\xc0"
"\x50\x78\x00\x41\x5f\xb8\x00\x00\x00\x00\x50\x48\xb8\x63\x61\x6c"
"\x63\x2e\x65\x78\x65\x50\x48\x89\xe1\xba\x01\x00\x00\x00\x48\x83"
"\xec\x30\x41\xff\xd7";

int main() {
    // 注意标志位PAGE_EXECUTE_READWRITE,给这段空间的数据设置为具有读写和可执行权限
    void* exec_mem = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    if (exec_mem == nullptr) {
        std::cerr << "Memory allocation failed\n";
        return -1;
    }
    memcpy(exec_mem, shellcode, sizeof(shellcode));
    auto shellcode_func = reinterpret_cast<void(*)()>(exec_mem);
    shellcode_func();
    VirtualFree(exec_mem, 0, MEM_RELEASE);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

信不信由你,我们才刚刚热身!我希望你和我一样兴奋,因为下一部分将介绍如何去除空字节(NULL bytes),这样我们就能在缓冲区溢出漏洞利用中使用这段 shellcode 啦!😸 我也希望这部分内容能让你有所收获,并且还算容易理解。我花了不少时间才把所有信息整合起来感谢大家!下一篇文章我们将专注于去除空字节 “00”,并学习如何使用 “GetProcAddress” 动态定位函数,然后弹出一个消息框。到时候见!

Windows x64 ShellCode入门教程
Windows x64 ShellCode入门教程 02

← Windows x64 ShellCode入门教程 Windows x64 ShellCode入门教程 02→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第二章 关键字static及其不同用法
03-27
03
第一章 auto与类型推导
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式