大数跨境
0
0

64位buffer overflow

64位buffer overflow 卓识网安
2022-07-14
2
导读:卓识网安

缓冲区溢出


缓冲区是一段连续的存储数据的内存,当我们向缓冲区中输入超过其长度的数据,就会重写与其相邻的内存的数据,导致程序崩溃。单纯的崩溃程序并没有什么意义,我们更关注的是能否通过溢出来执行一些系统命令。


寄存器


x86架构只有8个寄存器(eax,ebx,ecx,edx,ebp,esp,esi,edi)。x64架构多了8个寄存器(r8,r9,r10,r11,r12,r13,r14,r15)




栈是一个先进后出的数据结构,有两种栈操作:push和pop,push向栈中添加一个对象,pop从栈中弹出一个对象。当调用函数时会为其创建一个栈帧,用于存储参数,局部变量和返回地址。


上图展示了一个函数调用另一个函数时的栈布局结构。函数参数先入栈,然后是返回地址(rip中的值),其后是栈基址指针(rpb中的值,指向前一个栈帧的基址)。在64位的架构中前6个参数不会入栈,而是存在寄存器中,但32位架构则是全都入栈。

例如,如下代码

long myfunc(long a, long b, long c, long d,long e, long f, long g, long h)

{

long xx = a * b * c * d * e * f * g * h;

long yy = a + b + c + d + e + f + g + h;

long zz = utilfunc(xx, yy, xx % yy);

return zz + 20;

}

64位栈结构


如图所示,前6个参数a,b,c,d,e,f并不入栈,而是分别存在寄存器rdi,rsi,rdx,rcx,r8,r9中,剩下两个参数g,h入栈,压入的方向为从右到左,所以h比g先入栈,当函数返回时,返回地址会被弹出送入rip中,继续从给定的地址处执行命令。


实验配置


操作系统为kali64位,为了方便攻击,需要关掉一些保护机制。


Address Space Layout Randomization (ASLR)


地址空间随机分布,该机制会随机排布进程的地址空间

echo 0 > /proc/sys/kernel/randomize_va_space 关闭该保护



Data Execution Policy (DEP)/NX/XD


数据执行保护,将某些内存段的代码标记为不可执行。为了让栈段的代码可执行,需要在编译时传递 -z execstack标记,例如:gcc bof_demo.c -z execstack -o bof_demo

使用 readelf 命令可以查看栈段是否可执行

readelf -l bof_demo



Stack Canaries/Cookies


在缓冲区和控制数据之间插入一个值,用于检测是否被溢出,编译时传递 -fno-stack-protector禁用,例如:gcc bof_demo.c -o bof_demo -fno-stack-protector。


漏洞代码


#include <stdio.h>

#include <unistd.h>

int foo(){

char buffer[600];

int characters_read;

printf("Enter some string:\n");

characters_read = read(0, buffer, 1000);

printf("You entered: %s", buffer);

return 0;

}

void main(){

foo();

}

buffer只有600个字节的存储空间,但read函数读取了1000个字节进入buffer。


编译代码

gcc bof_demo.c -o bof_demo -z execstack -fno-stack-protector 


FUZZ


为了通过溢出,准确的修改栈中的返回地址,我们需要找到准确的偏移值,同时还要查清楚系统的字节序列和合法地址。

字节序列分为大端和小端。小端存储将低位的字节存在低地址,高位的字节存在高地址,反之则为大端。

lscpu指令可以查看



用C代码也能检测

#include <stdio.h>

union s{

int n;

char b;

}x;

void main(){

x.n = 0x4142;

if(x.b == 0x42){

printf("Little Endian\n");

}

else{

printf("Big Endian\n");

}

}

 
合法地址

在64位架构中,并不是整个2的64次方字节都用于地址空间,通常只用48位表示地址。合法地址的范围:

1. 0x0000000000000000 ~ 0x00007FFFFFFFFFFF 

2. 0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF 

任何此范围以外的地址均为非法

32位架构中,每当缓冲区被溢出,eip会从栈中加载被重写的返回地址,但64位架构的并不是,64位的rip只加载合法地址,非法地址不会加载。



 
如何确定偏移

从64位的栈结构可以看出,rbp就在返回地址的前面,可以通过溢出到rbp来确定偏移


查找buffer的起始地址



从中选择一个离buffer开头比较近的地址,作为后续rip的返回地址


生成msf pattern

msf-pattern_create -l 1000 > fuzz_rbp.in



查找偏移,offset值为608



rbp的起始偏移为608个字节,再往后8个字节就是返回地址的起始,所以返回地址的起始偏移应该是608+8=616,返回地址长度为8字节,所以要重写的部分为616到616+8=624,需要写入合法地址,机器为小端存储。



 
payload

msfvenom生成payload


脚本代码,用python2写比较好



用另一台kali监听,然后执行命令./bof_demo_exploit.py|./bof_demo,将print的结果传输给漏洞程序

收到反弹shell



与在gdb中调试不同,实战中rip的地址需要多做几次尝试才能成功,使用不同的路径执行bof_demo也会导致可用的返回地址不同。


参考


https://medium.com/@buff3r/basic-buffer-overflow-on-64-bit-architecture-3fb74bab3558


PS


python2和python3的输出不同,python2默认的字符串是ASCII,当输出超出ASCII范围时,也并不会转换该字符



但python3对不是ASCII字符会编码成双字节的utf-8



python3编码方式如下

110vvvvv 10vvvvvv,其中v是原字符的代码点

例如:

0x80

1000 0000 转换为8位的二进制

000 1000 0000 因为编码中有11个v用作代码点,所以要转换为转换位11位的二进制

00010 000000 拆分成5+6的格式

11000010 10000000 分别添加上头110和10

0xc2 0x80 转换成16进制

所以这就是为什么开头会多出c2

python3要实现像python2一样输出,使用以下代码

python3 -c 'import sys; sys.stdout.buffer.write(b"\x90")'




【声明】内容源于网络
0
0
卓识网安
北京卓识网安技术股份有限公司(原北京华电卓识信息安全测评技术中心有限公司)是一家致力于能源(电力)行业信息安全测评服务的独立第三方专业测评机构。
内容 69
粉丝 0
卓识网安 北京卓识网安技术股份有限公司(原北京华电卓识信息安全测评技术中心有限公司)是一家致力于能源(电力)行业信息安全测评服务的独立第三方专业测评机构。
总阅读31
粉丝0
内容69