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的地址。
PEB全称是Process Environment Block 进程环境块。
通过上面的介绍已经说了每个线程都有自己的TEB,其实每个进程也有自己的PEB信息。
PEB也存放在用户态的地址空间。
在以前的操作系统中,PEB的默认地址在0x7FFDF000处。
PEB的地址计算有两种:
1 通过EPROCESS偏移0x1b0得到,但是EPROCESS是系统地址空间,访问EPROCESS需要R0的权限
2 通过TEB结构偏移0x30获取PEB
之前已经讲过,通过FS:[0] 可以获取到当前TEB 的起始地址
所以通过fs:[0x30] 就可以获取到PEB的地址
我们都知道,CreateProcess函数可以用来创建一个新进程。官方文档如下:
通过该函数,我们可以看得出来,CreateProcess函数有个关键的参数是程序的路径。
CreateProcess函数在调用之后
1 首先会参数中路径的文件,以FILE_EXECUTE的方式打开
2 把改文件映像装载进RAM
3 创建进程内核对象,也就是EPROCESS、KPROCESS和PEB结构
4 为新创建的进程分配地址空间
5 创建主线程的线程执行对象,也就是EHTREAD、KTHREAD和TEB结构
6 为主线程分配堆栈,建立该进程主线程的执行上下文
7 由Kernel32.dll通知系统进程创建成功
8 执行线程
9 在进程和线程的Context中完成地址空间的初始化,正式执行程序
上面已经说过,PEB保存了进程中的关键信息。PEB不仅包含了二进制映像信息,还包含了3个链表。这三个链表与已在进程空间中映射的已加载模块相关,所以可以通过链表找到kernel32.dll的地址。
比如在PEB中偏移0xC的位置指向了一个名为PEB_LDR_DATA结构的地址,PEB_LDR_DATA结构包含了三个LDR_DATA_TABLE双链。所以,要通过PEB找到kernel32.dll步骤如下:
xor ecx, ecx mul ecx mov eax, [fs:ecx + 0x030] ; 获取PEB,存入eax mov eax, [eax + 0x00c] ; 获取LDR,存入eax mov eax, [eax + 0x014] ; 获取InMemoryOrderModuleList,存入eax mov eax, [eax] ; 模块1 eax的地址 mov eax, [eax] ; 模块2 ntdll的地址 mov eax, [eax + 0x10] ; 模块3 kernel32.dll的地址 xor eax, eax
通过这段代码,就可以获取kernel32.dll的地址并且存放到eax中
保存为exe,od加载,这里可以看到成功获取。
找到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
%include "io.inc" section .text global 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
2020安全开发者峰会(2020 SDC)议题征集 中国.北京 7月!
最后于 2020-1-21 15:13 被顾何编辑 ,原因: