格式化字符串漏洞利用时计算的偏移到底是什么?
我们平时在自己做题或者是看大佬们的wp时都会看见这种说法
说法一:
说法二:
相信有不少半路出家的小白都和我一样都只是知其然不知其所以然,那这里所说的“偏移”到底是什么意思呢?
我们结合这道题来详细讲一讲这些个偏移量所具体代表的意思
先来看伪代码
//main函数
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int v3; // [rsp+24h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
sub_4009FF();
puts("Welcome to the battle ! ");
puts("[Great Fairy] level pwned ");
puts("Select your weapon ");
while ( 1 )
{
while ( 1 )
{
Menu();
__isoc99_scanf("%d", &v3);
if ( v3 != 2 )
break;
sub_4008EB();
}
if ( v3 == 3 )
{
puts("Bye ");
exit(0);
}
if ( v3 == 1 )
sub_400960();
else
puts("Wrong!");
}
}
//显示开始菜单
int Menu()
{
puts("1. Stack Bufferoverflow Bug ");
puts("2. Format String Bug ");
return puts("3. Exit the battle ");
}
//分支2
unsigned __int64 sub_4008EB()
{
char buf[136]; // [rsp+0h] [rbp-90h] BYREF
unsigned __int64 v2; // [rsp+88h] [rbp-8h]
v2 = __readfsqword(0x28u);
memset(buf, 0, 0x80uLL);
read(0, buf, 0x7FuLL);
printf(buf);
return __readfsqword(0x28u) ^ v2;
}
从伪代码中可知这道题需要先使用格式化字符串漏洞泄露出canary的地址,然后再使用栈溢出漏洞覆盖返回地址,因为重点是分析偏移的意义,这里我们着重分析前半部分。
我们看到分支2,这里有一个明显的格式化字符串漏洞,就是printf函数的输出是由输入者自定义的,并且没有给出格式化字符串,所以我们可以通过给输入buf加上自定义的格式化字符串以泄露出canary的值
观察汇编代码,我们注意到了main函数中存在着canary值的判断
.text:000000000040094A mov rax, [rbp+var_8]
.text:000000000040094E xor rax, fs:28h
.text:0000000000400957 jz short locret_40095E
.text:0000000000400959 call ___stack_chk_fail
先是把rbp+var_8移到了rax寄存器中,然后再对rax的值进行判断,从这里可以看出canary的值被存放在rbp+var_8的位置,下面查看var_8的值
栈结构
-0000000000000008 var_8 dq ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables
所以,canary被存放在rbp-0x8的位置
那么canary到我们的格式化字符串的输入存放位置有多远呢?
unsigned __int64 sub_4008EB()
{
char buf[136]; // [rsp+0h] [rbp-90h] BYREF
unsigned __int64 v2; // [rsp+88h] [rbp-8h]
v2 = __readfsqword(0x28u);
memset(buf, 0, 0x80uLL);
read(0, buf, 0x7FuLL);
printf(buf);
return __readfsqword(0x28u) ^ v2;
}
我们可以看到输入buf的位置在rbp-0x90的位置,所以第一个偏移的位置就找出来了,就是buf距离canary的位置:0x90-0x8 = 0x88(十六进制别看错了)
第二个偏移需要涉及到函数参数的获取方式:
由于这个程序为64位程序,所以输入输出等函数的参数获取方式是栈与寄存器结合,而字符串类的参数自然是存放在栈中的(我觉得是因为因为这种参数大小不定,寄存器可能放不下)
其他类型函数获取参数具体方式可以看我的另一篇文章
首先来看分支2中的read函数,read函数先在输入缓冲区中获取我们的输入,然后把输入保存在buf位置,然后read函数执行结束,执行printf函数,print函数首先查看格式化输出的字符串,然后严格地按照格式化字符串所指示的输出方式输出栈中的元素。
但是,printf函数千算万算没想到啊,它所信任依赖的格式化字符串是用户(某个黑心的pwn手)输入的,很快啊,一下子就输出了canary的值,所以这个偏移实际上就是printf函数的栈顶到我们本应输出的字符串(buf)的位置。为了更清晰地说明这个问题,使用图表的方式解释这个问题。
栈帧结构:
高地址 | … |
---|---|
| | | 较早的栈帧 |
|堆| | … |
| | | 调用者rbp ——父函数(调用者)栈帧—— |
|栈| | 参数n |
| | | … |
|生| | 参数1 |
| | | ——调用者返回地址—— |
|长| | 调用者rbp ——子函数(被调用者)栈帧—— |
| | | 保存的寄存器 |
|方| | 局部变量 |
| | | … |
\ 向 / | … |
\/ | 局部变量 |
低地址 | 栈顶指针rsp |
在子函数调用时,执行的操作为:
- 函数将调用参数从后向前压栈
- 将返回地址压栈保存
- 跳转到子函数起始地址执行
- 子函数将父函数栈帧起始地址(rbp) 压栈
- 将 %rbp 的值设置为当前 %rsp 的值,即将 %rbp 指向子函数栈帧的起始地址
所以,第二种情况所说的偏移量实际上就是printf函数(子函数)栈顶到main函数(父函数)其中某一个参数的偏移量,也就是到buf的偏移量。
这篇文章从汇编层面说明了printf函数执行时可能会出现的漏洞,同时顺带着粗略地介绍了函数调用时的栈帧结构。如果对于格式化字符串漏洞的利用方式感兴趣的话可以戳下面的链接看看我的另一篇文章。才疏学浅,文中如有错误欢迎各位大佬在评论区批评指正~~~