Skip to content

Latest commit

 

History

History

[Medium] QuickScan

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

QuickScan

4th 03 24 / Document No. D24.102.17

Prepared By: clubby789

Challenge Author: clubby789

Difficulty: Medium

Classification: Official

Synopsis

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.

Skills Required

- Reading assembly

Skills Learned

- Scripting with Pwntools

Solution

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.

rep movsb

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.

Solving

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.