4th 03 24 / Document No. D24.102.17
Prepared By: clubby789
Challenge Author: clubby789
Difficulty: Medium
Classification: Official
QuickScan is a Medium reversing challenge. Players will be sent a series of small, randomly generated ELF files and must rapidly and automatically anlalyse them in order to extract required data.
- Reading assembly
- Scripting with Pwntools
If we connect to the server, we're given an explanation of the challenge:
I am about to send you 128 base64-encoded ELF files, which load a value onto the stack. You must send back the loaded value as a hex string
You must analyze them all in under 20 seconds
Let's start with a warmup
This is followed by sending us a base64-encoded ELF file, and some 'expected bytes':
ELF: f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAl4MECAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAgAQIAAAAAACABAgAAAAAswMAAAAAAACzAwAAAAAAAAAQAAAAAAAAS2XaK6c97108OmEJYu7q0KkERcXrRhuiJBCRfPBp2ijGxdWcVOW2cUGTTaI0O6ya/95TzgqVOj6IsdL2yxjc79xPMlN0zF3mKcfSoPhMRfTny55yoUspZqVrlvrfaDUL7HIHSMgK1E8nPW8DnPD2WT2KZm/ow0Jl3/qnAwGe9belBVoMvM2FDs0yHrtOD6McstHRrsk0l4m8ocnICYnODmSJ5a9MMM+1SDLJvHwPj+o5VwMme5snx87P5HtjXn1EEdnqVcgLDBdTcAbKYSy45eqsBZTklInD6uA+/CwiSvErzOTdRDwyIwMwUsCLBJebcyQJm+stSRwdJc74Pb+lYluev1VUuqGJpFP5bVj1wrxjYBnGRWLdr6UyOkkvuW7xbWed/t8hE9l2Kh+Hj3BE/Pmw7/5XxO+/6g7S9ZBW0rkcs7SqIBcy8g+pbj6x4aEHdxGoTz3mf9CS1cya/2W7GZ21Em2OV8eiBk5ouZoOiiD4ldWOp4WDiNFDTROtfM3bWQyvYXZ8THc10AUXBG3PAYPF1x6wpenN36Cp+FKxyxkzwwKNYZZPoDVhSs15xUcL5R2ziuKEHw3z6aP9zSasJ1n2EeGSlY4rR3GW4ZicfbNF1trKbl4TH23gbrErugWeu6nzGET067tgkzEV1Fejmug3QoXc9WzwSYWea9fsakCwUVhfsubZxpQZWGlKFTJbpT59jxcAyOFI2POODA+NP+JhBta7DzCVqX3kurRet0O5AJ3u8u+g9KCzCgEEqmHOBfYPjGfXyuHmWxNA7X42tirMSP7mh4DvLF8CTt4fCV1sfGzIXJ5c1qgOtXTd6Jrl95zyZ5IALTDNZ/l5wqw3JBMW0EMTEBHjPQB+J1/is9gC7U6ID+SpxL0/+XGW91ShX9cydYoNOn2qDbA2r2CHnxBsSO0x+29Zo7jog7qedUd0OumnwxDxEwIXhB78vyu3rg2eCeJzGZbToXXRuMbx+cilrXxL9DuVtSaNe48q8ajqp9IDP3D+h7aWZfbhkSko1W36L9zaMzeaM4+92hdCTbGLY1DrLZkyzQ2EoEgka0iD7BhIjTVT/f//SInnuRgAAADzpLg8AAAADwU=
Expected bytes: 9ef5b7a5055a0cbccd850ecd321ebb4e0fa31cb2d1d1aec9
This is a relatively small ELF, so we will open it up in a disassembler. A lot of functions that appear to be junk are loaded, but if we navigate to the entrypoint (_start
), we'll see some more readable code.
void _start() __noreturn
sub rsp, 0x18
lea rsi, [rel data_80480f5]
mov rdi, rsp {var_18}
mov ecx, 0x18
rep movsb byte [rdi], [rsi] {var_30} {var_18} {0x0}
mov eax, 0x3c
syscall
This simply allocates 0x18 of stack space, loads a pointer into the junk data into RSI then loads the stack into RDI. It then sets ecx
to 0x18 and uses the rep movsb
instruction. Finally, it uses the exit
(0x3c) syscall.
This instruction is a common, simply implementation of memcpy
. Given a vlaue in ECX
, it will copy bytes from the pointer in RSI
to the pointer in RDI
, decrementing ECX
until it is 0. If we follow the pointer here:
uint8_t data_80480f5[0x18] =
{
[0x00] = 0x9e
[0x01] = 0xf5
[0x02] = 0xb7
[0x03] = 0xa5
[0x04] = 0x05
[0x05] = 0x5a
[ ... SNIP ... ]
[0x14] = 0xd1
[0x15] = 0xd1
[0x16] = 0xae
[0x17] = 0xc9
}
We arrive at the 'expected bytes'. If we gather a few more binaries, we can see they all follow the same pattern - lots of random chunks of data, then an entrypoint which loads the address using lea
and copies it to the stack. We'll begin scripting a solution.
First, we'll connect to the server:
from pwn import *
r = remote(args.HOST, args.PORT)
We'll then write a function to solve each round.
import tempfile
def do_round():
r.recvuntil(b"ELF: ")
elf = b64d(r.recvline().decode())
with tempfile.NamedTemporaryFile("wb") as f, context.local(log_level='critical'):
f.write(elf)
f.flush()
elf = ELF(f.name)
pwntools
has many utilities for parsing ELF files, but they require an ELF on the disk. We'll write our received ELF into a temporary file, then load it with pwntools.
We now want to extract the data. We will therefore want to locate the lea
instruction, find out where it points to and grab the data there.
The bytes of this lea
instruction are 488d3553fdffff
. With some experimenting using online assemblers, we can determine that 0x48 0x8d 0x35
encode lea rsi, [rip +
. The next 4 bytes are a signed 32-bit offset - adding this value to the address of the instruction after the lea
gives us the target.
def get_loaded_value(e: ELF):
lea_addr = e.entrypoint + 4
lea_off = u32(e.read(lea_addr + 3, 4), sign="signed")
target = lea_addr + 7 + lea_off
return e.read(target, 0x18)
The lea
instruction begins 4 bytes after the entrypoint, and the offset begins 3 bytes after that. We can take this offset, add it to the lea address, and add the length of the lea itself (7), before reading the bytes from that location.
Let's use this in our do_round
function:
value = get_loaded_value(elf)
r.sendlineafter(b"Bytes? ", value.hex().encode())
We can now put it all together to solve the challenge:
do_round() # Solve the demo round
with log.progress("Solving binaries") as p:
for i in range(128):
do_round()
p.status(f"Solved {i}")
r.interactive()
Once we drop into interactive mode, the flag will be sent over.