基本ROP

本文详细介绍了返回导向编程(ROP)的概念、原理和分类,包括ret2text、ret2shellcode、ret2syscall以及ret2libc,并通过实例深入解析了每个分类下的漏洞利用方法,涉及到plt表、got表、动态链接库加载原理以及如何通过ROP技术劫持程序执行流。此外,文章还展示了如何利用gdb、cyclic工具和ida进行漏洞分析和利用payload的编写。
摘要由CSDN通过智能技术生成

基本ROP

1.原理

1.1 概念原理

ROP(Return Oriented Programming),中文名返回导向编程,即利用return语句跳转到指定位置,对程序的控制流进行劫持。它是建立在栈溢出的基础上,利用现有的小片段(gadgets)来改变寄存器或者变量的值,从而改变程序的执行流程。

gadget是以ret指令结尾的指令片段,如下所示即为某个gadget:

$ ROPgadget --binary ret2libc2 --only "pop|ret"
Gadgets information
============================================================
0x0804872f : pop ebp ; ret
0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804843d : pop ebx ; ret
0x0804872e : pop edi ; pop ebp ; ret
0x0804872d : pop esi ; pop edi ; pop ebp ; ret
0x08048426 : ret
0x0804857e : ret 0xeac1

Unique gadgets found: 7

使用ROP要满足两个条件:

  1. 程序存在栈溢出
  2. 程序里面能够找到可用的gadget

1.2 分类

1.2.1 ret2text

在代码段中直接发现需要使用的片段,如 sysystem("\bin\sh"),那么控制程序直接跳转到这个地方

1.2.2 ret2shellcode

上面的ret2text已经拥有了某些可以直接用来获取系统shell的函数,但是很多时候程序是不会有这样的语句的,因此需要我们自己去填充shellcode。当自己完成填充shellcode后,利用栈溢出使得程序跳转到shellcode的位置。

1.2.3 ret2syscall

这次劫持执行流,劫持的是系统调用,即syscall,通过给syscall传入不同的参数,可以实现对不同函数的劫持,比如劫持sysystem("\bin\sh"),劫持write()函数

系统调用参考博客:https://blog.csdn.net/qq_33948522/article/details/93880812

Linux的系统调用通过int 80h实现,用系统调用号来区分入口函数。

操作系统实现系统调用的基本过程是:

  1. 应用程序调用库函数(API)
  2. API将系统调用号存入EAX,然后通过中断调用使系统进入内核态
  3. 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用)
  4. 系统调用完成相应功能,将返回值存入EAX,返回到中断处理函数
  5. 中断处理函数返回到API中
  6. API将EAX返回给应用程序

应用程序调用系统调用的过程是:

  1. 把系统调用的编号存入EAX;
  2. 把函数参数存入其它通用寄存器
  3. 触发0×80号中断(int 0x80)
1.2.4 ret2libc
1.2.4.1 plt表与got表(延迟绑定机制)
1.2.4.1.1 原理介绍

plt表于got表的关系如下图所示:

对于某一个函数(比如gets()函数,这里假设它对应于plt[1]),两次寻址的方式是不一样的。

第一次寻址:

  1. 当程序调用gets()函数,那么gets()函数会去plt[1]寻址
  2. plt[1]内部存储的是3条汇编指令,先执行第一条汇编执行 jmp [got[3]]。
  3. got[3]在第一次被访问时,里面存储的是addr,即跳转到plt[1]中第二条汇编指令的地址,因此程序跳转到addr的位置
  4. 程序执行plt[1]第二条汇编指令:push reloc_arg,将_dl_runtime_resolve第二个参数压栈
  5. 程序执行plt[1]第三条汇编指令:jmp plt[0]
  6. plt[0]内部存储2条汇编指令,首先执行第一条:push [got[1]],将_dl_runtime_resolve第一个参数压栈
  7. plt[0]执行第二条指令:jmp [got[2]],即让程序执行_dl_runtime_resolve()函数,而先前它的两个参数都已经被压入栈了。
  8. 执行完_dl_runtime_resolve(link_map , reloc_arg)函数,获得了gets()函数的真实地址,存储在got[1]内。

第二次寻址:

  1. 程序调用gets()函数,gets()函数会去plt[1]寻址
  2. plt[1]存储了got[3]的地址,因此程序跳转到got[3]
  3. got[3]存储了gets()函数的真实地址,直接把这个真实地址返回,得到了gets()的真实地址

下面

1.2.4.1.2 实例调试

下面对于第一次寻址进行调试分析:

调试代码main.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
void vuln() {
   
    char buf[100];
    setbuf(stdin, buf);
    read(0, buf, 256);
}
int main() {
   
    char buf[100] = "Welcome to XDCTF2015~!\n";
    setbuf(stdout, buf);
    write(1, buf, strlen(buf));
    vuln();
    return 0;
}

编译命令:

gcc -o main -m32 -fno-stack-protector main.c -no-pie

首先看一下几个关键位置:

.plt:

$ objdump -d main | grep plt
Disassembly of section .plt:
08049030 <.plt>:

调用write函数位置:

$ objdump -d main | grep write@plt
080490d0 <write@plt>:
 80492ce:       e8 fd fd ff ff          call   80490d0 <write@plt>

gdb调试如下:

首先打个断点:

pwndbg> b *0x80492ce
Breakpoint 1 at 0x80492ce
pwndbg> r
Starting program: /mnt/d/study/ctf/资料/ctf-challenges/pwn/stackoverflow/ret2dlresolve/main 

程序停在第一次调用write()的位置:

 ► 0x80492ce <main+142>    call   write@plt <write@plt>
        fd: 0x1
        buf: 0xffffd0bc ◂— 'Welcome to XDCTF2015~!\n'
        n: 0x17

进入write@plt看看:

pwndbg> si
0x080490d0 in write@plt ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────[ REGISTERS ]────────
 EAX  0xffffd0bc ◂— 'Welcome to XDCTF2015~!\n'
 EBX  0x804c000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804bf0c (_DYNAMIC) ◂— 0x1
 ECX  0x2f8c
 EDX  0x3
 EDI  0xffffd120 ◂— 0x1
 ESI  0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
 EBP  0xffffd138 ◂— 0x0
*ESP  0xffffd09c —▸ 0x80492d3 (main+147) ◂— 0xe810c483
*EIP  0x80490d0 (write@plt) ◂— 0xfb1e0ff3
───────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────────────────────────
 ► 0x80490d0  <write@plt>      endbr32 
   0x80490d4  <write@plt+4>    jmp    dword ptr [0x804c01c] <0x8049080>
    ↓
   0x8049080                   endbr32 

发现程序会在0x80490d4产生jmp指令,跳转到[0x804c01c]。

而从

EBX  0x804c000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804bf0c (_DYNAMIC) ◂— 0x1

我们可以知道 0x804c000是got表首地址,因此 0x804c01c 其实是 got + 0x1c (0x804c01c - 0x804c000 = 0x1c)。这个地方是当前plt[“write”]对应的got[“write”],里面的内容是0x8049080。

pwndbg> x/x 0x804c01c
0x804c01c <write@got.plt>:      0x08049080

因此要进行 jmp 0x8049080,也就是跳转到write[“plt”]中的下一条汇编指令。执行:0x8049080 endbr32

程序继续往下:

pwndbg> si
0x08049084 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────────────────────────────────────
 EAX  0xffffd0bc ◂— 'Welcome to XDCTF2015~!\n'
 EBX  0x804c000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804bf0c (_DYNAMIC) ◂— 0x1
 ECX  0x2f8c
 EDX  0x3
 EDI  0xffffd120 ◂— 0x1
 ESI  0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
 EBP  0xffffd138 ◂— 0x0
 ESP  0xffffd09c —▸ 0x80492d3 (main+147) ◂— 0xe810c483
*EIP  0x8049084 ◂— 0x2068 /* 'h ' */
───────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────────────────────────
   0x80490d0  <write@plt>      endbr32 
   0x80490d4  <write@plt+4>    jmp    dword ptr [0x804c01c] <0x8049080>
    ↓
   0x8049080                   endbr32 
 ► 0x8049084                   push   0x20
   0x8049089                   jmp    0x8049030 <0x8049030>

开始将0x20压入栈,这个_dl_runtime_resolve()的第二个参数reloc_arg。

然后jmp到0x8049030,也就是jmp 到 plt[0]。

plt[0]里面存放的是2条汇编指令,一个是把got[1]压栈,也就是把 link_map 的地址(_dl_runtime_resolve()函数的第一个参数)压栈;另外一条跳转到got[2],也就是0x804c008 (0x804c008 - 0x804c000 = 8,8 / 4 = 2),这个got[2]里面存放的就是动态链接器_dl_runtime_resolve的入口地址。

   0x8049080                   endbr32 
   0x8049084                   push   0x20
   0x8049089                   jmp    0x8049030 <0x8049030>
    ↓
 ► 0x8049030                   push   dword ptr [_GLOBAL_OFFSET_TABLE_+4] <0x804c004>
   0x8049036                   jmp    dword ptr [0x804c008] <0xf7fe7b10>
    ↓

然后程序执行_dl_runtime_resolve(link_map, reloc_arg)。

调用write@plt结束:

   0xf7ebecda <write+58>    pop    esi
   0xf7ebecdb <write+59>    ret    
    ↓
   0x80492d3  <main+147>    add    esp, 0x10
 ► 0x80492d6  <main+150>    call   vuln <vuln>
        arg[0]: 0x0
        arg[1]: 0x1
        arg[2]: 0xf7ffd990 ◂— 0x0
        arg[3]: 0x636c6557 ('Welc')

看一下got[“write”]内写的是什么:

pwndbg> x/x 0x804c01c
0x804c01c <write@got.plt>:      0xf7ebeca0

这个时候里面的内容就和原来的 0x8049080 不一样了,存储的是write()函数的真实地址。

1.2.4.2 动态链接库加载原理

*.so文件在程序运行时会被加载到内存中,如果程序开启了aslr,那么每次加载时的基地址是不一样的。但是其中包含的函数,诸如system()函数、write()函数,还要很多的需要的字符串,诸如"\bin\sh",这些东西相对于及地址的偏移量都是固定的。因此,如果能够泄露出libc的基地址,那么计算出偏移量,就能够找到这些函数、字符串在内存中的真实地址。

其他知识点:

  1. libc.so 动态链接库中的函数之间相对偏移是固定的。
  2. 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。比如puts函数的真实地址0xf7d54360,它的低十二位是360,在任意一个libc内都是360。

那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

1.2.4.3 ret2libc基本原理

ret2libc即控制程序执行libc中的函数,一般是返回至某个plt处或者函数的具体的位置(比如got表项的内容)。一般,我们选择跳转到system("/bin/sh")位置,从而获取shell。

ret2libc下面有三个变形:

  1. ret2libc1:有system,有”\bin\sh“

  2. ret2libc2:有system,无”\bin\sh“

  3. ret2libc3:无system,无”\bin\sh“

2.例题

2.1 ret2text

2.1.1 习题信息

题目来自ctf-wiki中,路径:ctf-challenges\pwn\stackoverflow\ret2text\bamboofox-ret2text

2.1.2 程序分析

查看一下基本的保护机制:

$ checksec ret2text
[*] '/mnt/c/Users/Chance/Desktop/bamboofox-ret2text/ret2text'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

可用看出来是32位程序,然后开了nx

使用ida看一下main函数:

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

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("There is something amazing here, do you know anything?");
  gets((char *)&v4);
  printf("Maybe I will tell you next time !");
  return 0;
}

发现上面第8行存在危险函数gets(),可用用来进行栈溢出

2.1.3 漏洞定位

因为看到了gets函数,它的输入是v4,而gets函数没有限制输入的长度,这里存在栈溢出漏洞

2.1.4 漏洞利用

基于上面的分析,可用使用输入的字符串淹没ret,然后控制程序跳转到 secure中的system("/bin/sh")中去

通过ida分析出来,我们知道system("/bin/sh")的地址是:0804863A ,注意不是直接从_system开始,而是要从前一行开始,因为那里传参也得包含进去

.text:08048626                 mov     dword ptr [esp], offset unk_8048760
.text:0804862D                 call    ___isoc99_scanf
.text:08048632                 mov     eax, [ebp+input]
.text:08048635                 cmp     eax, [ebp+secretcode]
.text:08048638                 jnz     short locret_8048646
.text:0804863A                 mov     dword ptr [esp], offset command ; "/bin/sh"
.text:08048641                 call    _system

因此在ret的位置,我们只需要填上0804863A 即可劫持程序的执行流到system位置

注意:这里能够看到system("/bin/sh")的绝对地址是因为程序没有开启PIE(从前面checksec可以看出来),如果开启了PIE,那么IDA分析出来的是相对的偏移量,比如0x0000063A,那么就需要计算libc基地址,然后绝对地址=libc基地址+0x0000063A

那么现在如何确定要填充的长度,保证能够淹没返回地址ret?

2.1.4.1 方法一:gdb手工计算

在gets()函数打断点

pwndbg> b gets
Breakpoint 1 at 0x8048460
pwndbg> r
Starting program: /mnt/c/Users/Chance/Desktop/bamboofox-ret2text/ret2text
There is something amazing here, do you know anything?

在gets位置停下,步过若干次,后输入若干个’A’

pwndbg> n
AAAAAAAA

跳出gets()函数

pwndbg> fin
Run till exit from #0  _IO_gets (buf=0xffffd21c "") at iogets.c:39
main () at ret2text.c:25
25         
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值