X64 ShellCode 介绍(翻译)

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 编译器
生成自定义,添加masm 编译器
添加main.asm 文件并设置编译器
添加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,地址空间布局随机化,使函数地址的获得更加复杂等等。这些问题的解决需要更加深入的学习。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值