【Day 07】欢迎来到实力至上主义的 Shellcode (上) - Windows x86 Shellcode

环境

  • Windows 10 21H1
  • Visual Studio 2019
  • NASM 2.14.02

Shellcode 用途

Shellcode 是一段用於利用软件漏洞而执行的代码,藉由塞入一段可以让 CPU 执行的机械码,使电脑可以执行攻击者的任意指令。有接触过 CTF 的 Pwn 的人一定都很熟悉 Shellcode,当已经可以控制有漏洞的程序流程後,让程序执行 Shellcode 就可以拿到 Shell。

在这一篇我将会说明 32-bit Windows Shellcode,执行 WinExec("C:\Windows\System32\calc.exe", 10),以及介绍一些在 Windows 上写 Shellcode 时会使用到的工具。虽然这篇是介绍 32-bit Shellcode,但是其实与 64-bit 概念大多相同。

Linux Shellcode VS Windows Shellcode

应该有不少人是从 Linux 开始学习 Shellcode 的,所以这边来比较一下两者的差异。首先看看 x86 Linux 的 Shellcode,里面使用的 int 0x80 是一个 Interrupt,而 0x80 是由 Kernel 处理的,可以用来让程序呼叫 System Call。

; execve("/bin/sh", 0, 0)
0:  31 c0                   xor    eax,eax
2:  50                      push   eax
3:  68 2f 2f 73 68          push   0x68732f2f
8:  68 2f 62 69 6e          push   0x6e69622f
d:  89 e3                   mov    ebx,esp
f:  50                      push   eax
10: 50                      push   eax
11: 53                      push   ebx
12: 89 e1                   mov    ecx,esp
14: b0 0b                   mov    al,0xb
16: cd 80                   int    0x80

然而 Windows 的 Syscall Number 常常变动,所以使用 Windows API 会更稳定,然後 Windows API 再去呼叫 Native API。这些原因导致撰写 Windows Shellcode 的步骤会变得比 Linux Shellcode 复杂许多,因为我们必须深入了解 PE 结构,从目标 dll 中取出需要的 Windows API 使用。

概略流程

  1. 找到 kernel32.dll
    • 取得 PEB 位址
    • 取得 PEB_LDR_DATA 位址
    • 取得 InMemoryOrderModuleList 位址
    • 取得 kernel32.dll 的 Base Address
  2. 从 kernel32.dll 中找到 WinExec 函数
    • 取得 NT Header 位址
    • 取得 Export Directory 位址
    • 取得 Address Table、Name Pointer Table、Ordinal Table
    • 回圈寻找目标函数
  3. 执行 WinExec

详细流程

1. 找到 kernel32.dll

取得 PEB 位址

要得到 PEB 位址,首先要知道 TIB(Thread Information Block),它是用来存放有关目前的 Thread 的资讯,结构有点大可以自己点进连结看一下。

在 32-bit 和 64-bit,分别可以使用 FS、GS 这两个 Segment Register 找到,而在 32-bit 我们的目标 PEB 就位在 FS:[0x30],也就是 TIB 的 Offset 0x30。

取得 PEB_LDR_DATA 位址

首先来看一下 PEB 结构

typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;
} PEB, *PPEB;

在第五个参数,也就是 PEB 的 Offset 0xC,就是我们的目标 PEB_LDR_DATA。

取得 InMemoryOrderModuleList 位址

PEB_LDR_DATA 这个结构中可以看到三个 List,分别是 InLoadOrderModuleList、InMemoryOrderModuleList、InInitializationOrderModuleList,它们都是目前载入的 Image 的 Double Linked List,只是顺序不同。

typedef struct _PEB_LDR_DATA {
  ULONG                   Length;
  BOOLEAN                 Initialized;
  PVOID                   SsHandle;
  LIST_ENTRY              InLoadOrderModuleList;
  LIST_ENTRY              InMemoryOrderModuleList;
  LIST_ENTRY              InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

这三个 List 都是 LIST_ENTRY 结构,同时也是 Double Linked List 结构,Flink 指向下一个 Image,Blink 指向上一个 Image。

typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

取得 kernel32.dll 的 Base Address

那我们要这些 List 做什麽呢?如同上述,这三个 List 都是目前载入的 Image 的 Double Linked List,也就是说目标 kernel32.dll 也在其中。在这里我们为了实作方便使用 InMemoryOrderModuleList,不然其实也可以回圈任意一个 List 并透过对应 DLL 名称来找到目标 DLL。

InMemoryOrderModuleList 是根据记忆体的载入顺序排序,它的第三个 DLL 就是 kernel32.dll。其中在 List 的每个 Item 都指向一个 _LDR_DATA_TABLE_ENTRY 结构。

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID      DllBase;
    PVOID      EntryPoint;
    ULONG32    SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    UINT32   Unknow[17];
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

所以我们只要访问三次 InMemoryOrderModuleList 的 Flink 就会到达 kernel32.dll 的 _LDR_DATA_TABLE_ENTRY 结构。注意这边指向的位址是从 _LDR_DATA_TABLE_ENTRY+0x8 开始算,因为现在是用 InMemoryOrderModuleList 找,也就是说 DLLBase 在 Flink+0x10 位址。

最後取得的 DllBase 的位址就是 kernel32.dll 的 Base Address。

POC

主要是参考 Basics of Windows shellcode writing,有改一点东西以符合 NASM 还有加一些注解。在我的 GitHub zeze-zeze/2021iThome 可以找到完整版的 POC。

; 1. 找到 Kernel32.dll
; 取得 PEB 位址
mov ebx, [fs:30h]

; 取得 PEB_LDR_DATA 位址
mov ebx, [ebx + 0x0C]

; 取得并访问 InMemoryOrderModuleList 位址
mov ebx, [ebx + 0x14]

; 取得 kernel32.dll 的 Base Address
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; InMemoryOrderModuleList 的第三个 DLL 就是 kernel32.dll

NASM

能把组语编译成机械码的工具有很多,这边使用 NASM
它支援各种平台包含 Windows、Linux,用法也很简单。以我们现在要编的目标为例,因为是 32-bit Shellcode,所以 nasm -f win32 shellcode.asm -o shellcode.obj 就会产生 obj 副档名的档案。

看一下档案类型 Intel 80386 COFF object file, not stripped, 1 section, symbol offset=0xfe, 10 symbols 是个 COFF object file,然而我们要的是纯粹的 Shellcode,不需要其他东西。

这里我示范用 IDA 取出目标 Shellcode,载入档案後大概如下图。

虽然整个档案大小为 192 Bytes,但是我们实际上需要的 Shellcode 就只有 0x14 Bytes,所以使用 【Day 05】你逆 - 逆向工程工具介绍中推荐的 LazyIDA Plugin 按右键 => Convert => Convert to string 就可以转成 C String 了。

参考资料


<<:  Day8 HTML 其他常用标签

>>:  【Day11】- 递回Recursion

[第30天]理财达人Mx. Ada-货柜运价指数FBX

前言 本文说明使用scrapy爬虫函式库抓取海运FBX指数。 波罗的海货柜运价指数[FBX] 波罗的...

使用 Template Message 替 Line Bot 加上同意条款的功能(1)

昨天我们使用了 Quick Reply 让使用者可直接跟我们回应的讯息互动,今天要使用 Templa...

[D20] 物件侦测(1)

物件侦测(Object Detection)是影像辨识中重要的一环~ 物件侦测就是在照片或影片等图像...

Day 8 Compose UI Constraint Layout

今年的疫情蛮严重的,希望大家都过得安好,希望疫情快点过去, 能回到一些线下技术聚会的时光~今天要了解...

CSS For Kindle Made By Thefiresupport.Com

The first thing you should be clear about is what ...