X64 ShellCode 介绍(翻译)
原文链接
http://mcdermottcybersecurity.com/articles/windows-x64-shellcode
介绍
Shellcode是指通过缓冲区溢出类型的安全漏洞将其注入到进程的内存中之后执行的可执行机器代码(以及任何相关数据)。该术语来自早期的一种情况“针对unix 平台的早期攻击中,攻击者通常会执行‘启动shell 程序来监听TPC/IP 端口,以连接并拥有系统的最高访问权’的代码。当今的针对web浏览器以及应用程序的攻击更倾向于下载并执行另一个程序而不是生成shell 程序。但是这个术语保留了下来。
通常,shellcode 可以被认为是任何能够从内存中的任意位置执行的代码,而不依赖于操作系统加载程序提供的服务和传统的可执行文件。根据使用的方式不同,shellcode 可能需要包含较小的大小并避免特定字节模式的代码。在任何情况下,shellcode必须完成通常由操作系统加载器完成的两个任务:
1. 获得数据元素的地址(比如程序中引用的字节
2. 得到所调用函数的地址
这篇文章将实现一个shellcode代码。解决上面的两个问题的途径:1.使用RIP 相关寻址指令,即指令包含RIP 相关的寻址指令,如RIP 相对寻址等。2.解决的主要途径就是PE 文件等知识的运动,动态获得API地址。另外,shellcode 中代码和指令是一体的,并不存在“段”的概念。
- RIP-Relative Addressing
所读/写的存储器地址的引用可以被编码为当前RIP + 偏移。X86 体系下jmp 和 call 指令为rip 相关的。但是在X64 体系下,RIP 相关的读写内存指令也是允许的。
X86 中,标签指定的变量的地址将在编译和链接的时候替换为一个硬编码的内存地址,基于一个假设,这个程序将被加载到一个特定的基地址,在加载程序的时候,windows 加载体将会给定这个基地址,因此这个相对地址也会改变。但是shellcode 必须有能力从任何内存位置执行,因此,这个变量的地址必须可以通过代码动态得到,下面是一种技巧:
call skip
db 'Hello World',0
skip:
pop esi ;esi 是'Hello World'的地址
在X64 下,我们只需要使用RIP 相对寻址就可以实现上面的功能,因为当前指令的地址与数据的地址是可以确定的。
- API Lookup Overview
通常的编程中,我们可以静态加载DLL,借助函数声明通过函数名来调用函数,在编译和连接的时候,编译器在PE 中以导入表的形式将DLL+函数与我们的函数调用绑定在一起。在运行阶段,windows 加载器通过修正导入表帮助我们找到真正的函数地址并调用目标函数。另外,我们可以通过LoadLibrary + GetProcAddress 的方式动态加载DLL,得到函数地址并调用需要的函数。LoadLibrary 和 GetProcAddress 函数实现于kernel32.dll 中,我们的shellcode的执行没有windows 加载器的帮助,当我们需要实现功能即调用系统dll 中的特定函数,即需要使用这两个函数的时候,就需要手动获得两个函数的地址,然后再动态获取我们所需要的功能。- API Lookup Demo
接下来讲述如何在一个加载的DLL 中找到一个函数的地址。
整体思路:TEB[X86-fs寄存器,X64-gs寄存器,windbg-$teb 伪寄存器]->PEB->PEB_LDR_DATA->选择三个链表中的其中一个->找到模块->得到模块基地址->根据PE文件结构遍历导出表->找到目标函数的地址。
使用vs2015 创建纯asm、无导入表的应用程序
下面将通过vs2015 创建一个纯asm、无导入表的应用程序,该程序的代码段可以装载在任意基地址执行,可以作为shellcode 的一部分重复利用,但是需要注意的是,该函数并没有遵守X64 调用约定中关于易失性寄存器和非易失性寄存器的使用规定。
创建项目的过程
创建空项目
![]()
生成自定义,添加masm 编译器
![]()
添加main.asm 文件并设置编译器
![]()
![]()
设置程序入口点
![]()
编译链接运行程序
程序介绍
main 函数流程
main proc
sub rsp, 28h ;X64 中函数调用方负责为函数调用预留栈空间
and rsp, 0fffffffffffffff0h ;当执行call 指令的时候esp 必须是16 对齐的
lea rdx, loadlib_func
lea rcx, kernel32_dll
call lookup_api ;得到LoadLibraryA 函数的地址
mov r15, rax ;r15 寄存器存储的是LoadLibraryA 函数的地址,如果所查找的函数为转发函数,需要调用LoadLibraryA 加载目标DLL
lea rcx, user32_dll
call rax ;加载user32.dll,该模块导出了MessageBoxA/W 函数
lea rdx, msgbox_func
lea rcx, user32_dll
call lookup_api ;得到MessageBoxA 函数的地址
xor r9, r9 ;MB_OK
lea r8, title_str ;caption
lea rdx, hello_str ;Hello world
xor rcx, rcx ;hWnd (NULL)
call rax ;传入参数并调用MessageBoxA 函数
lea rdx, exitproc_func
lea rcx, kernel32_dll
call lookup_api ;得到ExitThread函数的地址,该函数为kernel32.dll 函数的转发函数
xor rcx, rcx ;exit code zero
call rax ;因为我们的程序没有用到vs 的运行库,因此需要自己手动调用ExitProcess函数,另外,由于当前程序没有运行库的支持,如果想调用常规的printf 和 scanf 等常用的运行库函数将会比较困难,这就是运行库的作用吧。
main endp
- lookup_api 函数流程
kernel32_dll db 'KERNEL32.DLL', 0
loadlib_func db 'LoadLibraryA', 0
user32_dll db 'USER32.DLL', 0
msgbox_func db 'MessageBoxA', 0
hello_str db 'Hello world', 0
title_str db 'Message', 0
exitproc_func db 'ExitProcess', 0
;从DLL 导出表中得到函数地址
;rcx=单字DLL名, rdx=单字函数名
;DLL名必须是大写
;r15=LoadLibraryA 函数的地址
;返回值放到rax 寄存器中
;找不到DLL 或者 函数的话返回0
lookup_api proc
sub rsp, 28h ;为LoadLibraryA 函数调用预留空间
start:
mov r8, gs:[60h] ;peb
mov r8, [r8+18h]
; ntdll!_PEB
; ...
; +0×018 Ldr : 0×00000000`779a3640 _PEB_LDR_DATA
lea r12, [r8+10h] ;InLoadOrderModuleList (list head) - save for later
; ntdll!_PEB_LDR_DATA
; +0×010 InLoadOrderModuleList: _LIST_ENTRY [ 0x00000000`00373040 - 0x39a3b0 ]
mov r8, [r12] ;follow _LIST_ENTRY->Flink to first item in list
; ntdll!_LIST_ENTRY
; +0×000 Flink : Ptr64 _LIST_ENTRY
; +0×008 Blink : Ptr64 _LIST_ENTRY
cld ;DF 标志为1
; ntdll!_LDR_DATA_TABLE_ENTRY
; +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000000`00333620 - 0x333130 ]
; +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000000`00333630 - 0x333140 ]
; +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000000`003344e0 - 0x333640 ]
; +0x030 DllBase : 0x00000000`77650000 Void
; +0x038 EntryPoint : 0x00000000`7766eff0 Void
; +0x040 SizeOfImage : 0x11f000
; +0x048 FullDllName : _UNICODE_STRING "C:\Windows\system32\kernel32.dll"
; +0x058 BaseDllName : _UNICODE_STRING "kernel32.dll"
for_each_dll: ;r8 指向当前 _ldr_data_table_entry
mov rdi, [r8+60h] ;rdi 指向当前模块名【双字】
;ntdll!_UNICODE_STRING
; +0×000 Length : Uint2B
; +0×002 MaximumLength : Uint2B
; +0×008 Buffer : Ptr64 Uint2B
; mov rsi, rcx ;pointer to dll we're looking for
compare_dll:
lodsb ;load character of our dll name string
test al, al ;查看是否为空字符串
jz found_dll ;到了结尾
mov ah, [rdi] ;得到当前dll 的名称
cmp ah, 61h ;lowercase 'a'
jl uppercase
sub ah, 20h ;转换为大写
uppercase:
cmp ah, al
jne wrong_dll ;一个不匹配就下一个
inc rdi ;skip to next unicode character
inc rdi
jmp compare_dll ;下一个
wrong_dll:
mov r8, [r8] ;下一个DLL
cmp r8, r12 ;r12 为链表的第一个,如果相等了就代表循环结束
jne for_each_dll
xor rax, rax ;DLL not found
jmp done
found_dll:
mov rbx, [r8+30h] ;得到模块基地址
mov r9d, [rbx+3ch] ;DOS header e_lfanew成员 指向 PE 头
add r9, rbx ;r9 指向 _image_nt_headers64
add r9, 88h ;r9 当前指向导出表
mov r13d, [r9] ;得到导出表的虚拟地址
test r13, r13 ;if zero, module does not have export table
jnz has_exports
xor rax, rax ;no exports - function will not be found in dll
jmp done
has_exports:
lea r8, [rbx+r13] ;
;r8 指向_image_export_directory 结构
mov r14d, [r9+4] ;r14d 为导出表大小
add r14, r13 ;得到导出表的尾地址,用于后面判断是否为转发函数
mov ecx, [r8+18h] ;ecx = 函数名个数
mov r10d, [r8+20h] ;AddressOfNames (array of RVAs)
add r10, rbx ;r10 为导出函数名地址的数组的地址
dec ecx ;从后往前查找的
for_each_func:
lea r9, [r10 + 4*rcx] ;get current index in names array
mov edi, [r9] ;get RVA of name
add rdi, rbx ;rdi 指向当前导出函数名
mov rsi, rdx ;rsi 指向我们查找的函数名
compare_func:
cmpsb
jne wrong_func ;function name doesn't match
mov al, [rsi] ;current character of our function
test al, al ;是否到了我们要查找的函数的末尾
jz found_func ;if at the end of our string and all matched so far, found it
jmp compare_func ;continue string comparison
wrong_func:
loop for_each_func ;查找下一个函数
xor rax, rax ;function not found in export table
jmp done
found_func: ;ecx is array index where function name found
;r8 points to _image_export_directory structure
mov r9d, [r8+24h] ;AddressOfNameOrdinals (rva)
add r9, rbx ;add dll base address
mov cx, [r9+2*rcx] ;get ordinal value from array of words
mov r9d, [r8+1ch] ;AddressOfFunctions (rva)
add r9, rbx ;add dll base address
mov eax, [r9+rcx*4] ;Get RVA of function using index
cmp rax, r13 ;查看找到的地址是否在导出表的地址范围内,在的话就是转发函数
jl not_forwarded
cmp rax, r14 ;if r13 <= func < r14 then forwarded
jae not_forwarded
;forwarded function address points to a string of the form <DLL name>.<function>
;note: dll name will be in uppercase
;extract the DLL name and add ".DLL"
lea rsi, [rax+rbx] ;add base address to rva to get forwarded function name
lea rdi, [rsp+30h] ;使用栈区的内存作为我们临时存储DLL名称的地方,
mov r12, rdi ;save pointer to beginning of string
copy_dll_name: ; 如果是转发函数的话,先加载该dll
; 以新的DLL名+函数名跳转到函数开始
movsb
cmp byte ptr [rsi], 2eh ;check for '.' (period) character
jne copy_dll_name
movsb ;also copy period
mov dword ptr [rdi], 004c4c44h ;补.DLL 和 一个结尾字符
mov rcx, r12 ;r12 "<DLL name>.DLL"
call r15 ;LoadLibraryA(r12)
mov rcx, r12 ;target dll name
mov rdx, rsi ;target function name
jmp start ;start over with new parameters
not_forwarded:
add rax, rbx ;add base addr to rva to get function address
done:
add rsp, 28h ;clean up stack
ret
lookup_api endp
总结
- 本文给出的代码十分的基础,有很多可以探讨的地方,比如转发表可以在编码之前就进行确定,但是处理转发函数让代码更加具有适应性,另外,本文采用的是masm,nasm(Netwide Assembler)具有多平台的特性。实际应用shellcode 的情形往往更加复杂,会面临许多的问题,如DEP,数据执行保护,必须修改页面执行属性才可执行我们的代码,ASLR,地址空间布局随机化,使函数地址的获得更加复杂等等。这些问题的解决需要更加深入的学习。