team-logo
Published on

elfisyou

Authors

Introduction

Description

Let me mention right away that this solution is also available in Polish on this page.

According to the description, ELF is a game where you move blocks to build a working program. At the beginning, you get an explanation of what needs to be done, so it seems like it shouldn't be too hard... But it is – very much so.

Do you remember the cult classic Baba Is You? Those were the days when you sat there, clueless about what to do, and the solution turned out to be completely unconventional – like moving an entire fence. Inspired by that game, a great reverse challenge appeared in the CTF tournament. No, this isn't Baba Is You – this is elfisyou. You can probably guess that it involves an ELF file.

You can download a Python program, a Docker file to run it locally, and a corrupted binary that you can test offline. However, the main goal is to connect online and arrange the bytes correctly. Fourteen teams solved the challenge, including mine – MindCrafters – as the last one.

Description

What's Going On?

By connecting to the address:

socat file:$(tty),raw,echo=0 tcp:chall.lac.tf:31189
We see an image: elf It displays a 13x13 grid, which is very important—as we will soon discover. In the top right corner, there is a green square that we can move around the board, shifting other bytes as well. It's also possible to move multiple bytes at once.

We move using the wsad keys. I shifted a few bytes manually, but when I realized I needed to make many moves, mistakes were easy to make. So, I used the PWN tools library in Python for automation. Surprisingly, it worked well, although the connection and movement took quite a while. However, after disabling p.clean() and pause, it ran smoothly—even online! I was shocked—very few CTFs have such great infrastructure.

Structure

Alright, it's time to finally figure out these bytes. What do we actually see? Fortunately, mostly zeros and some text. We know there must be a magic ELF header, and we also spot the letter f from flag.txt. That’s a small advantage.

Payload

We'll deal with the ELF structure later. First, we need to determine what kind of payload this is. Typically, the program is located at the end of the file, so it made sense to check there for any recognizable mnemonics. However, instead of doing that, I started by writing an assembly program that opens a file, reads it, and prints it to the screen.

It was necessary to determine the architecture and make sure it wasn’t ARM (that would have been overkill). Looking at the syscalls, it was clear that this was x86-64. The open, read, and write syscalls require three arguments, but here, there were only two pairs (0F 05). This suggested the use of a trick with sendfile (ax=0x28), meaning the file was simply opened and sent directly to the screen.

The author wasn’t too much of a sadist and didn’t try to obfuscate the code, so it was fairly standard. However, I did make one mistake with the instruction order—moving didn’t work correctly, so I had to adjust the sequence. Luckily, that wasn’t a major issue. The next challenge was NASM itself. The payload was supposed to look like this:

section .text
    global _start

_start:

push   0                          ; Push 0 onto the stack (null terminator for the filename string)
mov    rax,0x7478742e67616c66    ; Move the string 'flag.txt' (reversed due to little-endian format) into RAX
push   rax                        ; Push the filename onto the stack
mov    eax,0x2                    ; Set EAX to 2 (sys_open syscall number)
mov    rdi,rsp                    ; Set RDI to point to the filename on the stack
syscall                           ; Call sys_open (open file "flag.txt")

mov    rax,0x28                   ; Set RAX to 40 (sys_sendfile syscall number)
mov    edi,0x1                    ; Set EDI to 1 (stdout file descriptor)
mov    esi,0x3                    ; Set ESI to 3 (assumed file descriptor returned from sys_open)
mov    r10,0x100                  ; Set R10 to 256 (number of bytes to transfer)
syscall                           ; Call sys_sendfile to send file content to stdout

With the mnemonics:

;    1000:       6a 00                   push   0x0
;    1002:       48 b8 66 6c 61 67 2e    movabs rax,0x7478742e67616c66
;    1009:       74 78 74 
;    100c:       50                      push   rax
;    100d:       b8 02 00 00 00          mov    eax,0x2
;    1012:       48 89 e7                mov    rdi,rsp
;    1015:       0f 05                   syscall
;    1017:       48 c7 c0 28 00 00 00    mov    rax,0x28
;    101e:       bf 01 00 00 00          mov    edi,0x1
;    1023:       be 03 00 00 00          mov    esi,0x3
;    1028:       49 c7 c2 00 01 00 00    mov    r10,0x100
;    102f:       0f 05                   syscall

After compilation:

nasm -f elf64 -o payload.o payload.asm
ld -shared -o payload.so payload.o

It looks like this:

objdump -d -M intel payload
  401000:       6a 00                   push   0x0
  401002:       48 b8 66 6c 61 67 2e    movabs rax,0x7478742e67616c66
  401009:       74 78 74 
  40100c:       50                      push   rax
  40100d:       b8 02 00 00 00          mov    eax,0x2
  401012:       48 89 e7                mov    rdi,rsp
  401015:       0f 05                   syscall
  401017:       b8 28 00 00 00          mov    eax,0x28
  40101c:       bf 01 00 00 00          mov    edi,0x1
  401021:       be 03 00 00 00          mov    esi,0x3
  401026:       41 ba 00 01 00 00       mov    r10d,0x100
  40102c:       0f 05                   syscall

Nasm replaced mov r10,0x100 with mov r10d,0x100. As a result, the mnemonics changed. Instead of 49 c7 c2 00 01 00 00, it became 41 ba 00 01 00 00. Since there were multiple instructions and mnemonics, I rewrote them as db.

section .data
    
section .text
    global _start

_start:
db 0x6a ,0x00               ; push 0
db 0x48 ,0xB8 ,0x66 ,0x6C ,0x61 ,0x67 ,0x2E ,0x74 ,0x78 ,0x74 ;mov rax, 0x7478742E67616C66 ; flag.txt
db 0x50                     ;push rax    
db 0xb8, 0x2,0x00,0x00,0x00 ; mov rax, 2              
db 0x48, 0x89 ,0xe7         ;mov rdi, rsp
db 0x0f, 0x05               ; syscall                   


db 0x48, 0xc7, 0xc0, 0x28,0x00,0x00,0x00  ; mov rax, 0x28
db 0xbf, 0x01, 0x00,0x00,0x00             ; mov rdi, 1 
db 0xbe, 0x03, 0x00, 0x00, 0x00           ; mov si, 03 
db 0x49, 0xc7, 0xc2,0x00,0x01,0x00,0x00   ; mov r10, 10
db 0x0f, 0x05                             ; syscall                    

Later, I split it into four bottom lines of 13 bytes each, and that way, I had the payload arranged.

00 00 00 6a 00 48 B8 66 6C 61 67 2E 74
78 74 50 b8 02 00 00 00 48 89 e7 0f 05
48 c7 c0 28 00 00 00 bf 01 00 00 00 be
03 00 00 00 49 c7 c2 00 01 00 00 0f 05

We have the payload, now it's time for ELF. To be honest, I don't know it in great detail, but that's what Wikipedia is for. That page provides a detailed breakdown of the format.

While reviewing and trying to match the bytes, I noticed that some numbers didn't line up. I wasn’t sure what 03 was for. It turned out to indicate a Shared object, which made sense. There were also offsets at 0x64 and a header size of 0x56.

I spent several hours working on this. Honestly, 010 Editor has a great feature for navigating through file headers of various formats. I won’t lie—I spent a long time tweaking things before I managed to piece together the entire file correctly (of course, offline).

I ran into many issues, especially with the program table element—I deleted too many bytes, set incorrect values, and so on. Eventually, the program ran, but only one hour remained before the CTF ended, and everything still needed to be fine-tuned. Fortunately, the payload was already done; all that remained was finalizing the ELF header—though not entirely.

010 editor

Solution

Here is the source code that allowed me to quickly extract the flag

from pwn import *             

HOST="chall.lac.tf:31189"
ADDRESS,PORT=HOST.split(":")

p = remote(ADDRESS,PORT)

wait=0.00
def down(count):    
    for i in range(count):
        #p.clean()
        p.sendline(b"s")
        sleep(wait)

def left(count):    
    for i in range(count):
        #p.clean()
        p.sendline(b"a")
        sleep(wait)

def right(count):    
    for i in range(count):
        #p.clean()
        p.sendline(b"d")
        sleep(wait)

def up(count):    
    for i in range(count):
        #p.clean()
        p.sendline(b"w")
        sleep(wait)

down(5)
left(1)
down(2)
right(1)
down(3)
left(1)
down(1)
left(1)
down(1)
left(1)
up(2)
right(2)
left(1)
up(5)
right(2)
down(2)
left(1)
down(2)
up(2)
right(1)
down(1)
left(4)
down(1)
left(1)
down(1)
right(2)
down(1)
right(1)
up(1)
left(4)
down(1)
left(2)
up(1)
right(6)
up(2)
left(1)
up(1)
left(4)
down(2)
left(2)
down(1)
right(5)
left(3)
up(3)
left(2)
up(2)
left(2)
down(4)
up(2)
right(1)
down(2)
up(2)
right(1)
down(2)
up(2)
right(5)
down(1)
right(1)
down(1)
left(3)
right(1)
up(2)
left(3)
down(1)
right(1)
down(1)
right(5)
up(1)
right(2)
down(1)
left(4)
up(1)
left(2)
down(2)
right(1)
up(1)
left(3)
up(1)
left(2)
down(1)
right(10)
up(4)
right(2)
up(2)
left(1)
down(4)
right(1)
down(1)
left(1)
up(1)
left(1)
down(1)
up(3)
left(1)
up(2)
left(1)
up(1)
left(1)
down(6)
left(1)
up(1)
left(1)
down(1)
up(1)
left(1)
down(1)
left(1)
#down(1)
right(3)
down(1)
right(2)
up(1)
right(4)
up(8)
left(4)
down(3)
right(1)
down(1)
left(1)
up(1)
left(1)
down(5)
right(5)
up(8)
left(1)
down(7)
right(1)
down(1)
left(3)
up(1)
left(1)
down(1)
left(5)
down(1)
right(1)
left(2)
up(5)
right(1)
down(4)
right(5)
up(2)
right(2)
down(1)
right(1)
up(6)
right(1)
up(1)
left(4)
down(3)
right(1)
up(2)
right(1)
up(1)
left(7)
right(6)
down(7)
left(5)
up(2)
right(1)
up(1)
left(1)
up(1)
left(1)
down(1)
right(1)
down(1)
left(3)
right(2)
down(2)
left(3)
up(6)
down(3)
right(2)
down(1)
right(2)
up(2)
left(1)
down(3)
left(1)
down(1)
right(8)
down(1)
right(1)
up(7)
right(1)
up(1)
left(8)
right(4)
down(2)
left(1)
up(1)
right(1)
up(1)
left(1)
down(5)
left(2)
up(5)
down(2)
left(1)
p.clean()
up(2)
down(1)
left(2)
up(1)
right(5)
up(1)
right(1)
down(1)
left(6)
down(1)
left(2)
up(1)
right(9)
down(2)
left(1)
up(1)
right(1)
up(1)
left(5)
right(5)
up(1)
right(2)
down(1)
left(5)
right(1)
down(4)
left(2)
up(1)
right(1)
down(1)
right(1)
up(3)
down(1)
right(3)
up(1)
left(1)
up(1)
right(1)
down(2)
left(7)
up(1)
left(1)
down(1)
right(7)
down(1)
right(2)
down(3)
left(6)
up(2)
right(1)
up(1)
left(1)
right(4)
down(1)
left(5)
right(2)
down(1)
left(4)
up(1)
left(1)
down(2)
p.sendline(b'x')
p.interactive()

Summary

I managed to capture the flag 8–10 minutes before the end of the CTF, although I had doubts about whether I would make it in time. This was a fantastic challenge.

I learned a lot about the ELF structure, arranged the payload, played around with the game, and ultimately captured the flag. Those 20–30 hours were definitely not wasted. ;)

final

Flag:
lactf{1m_r3ally_s0rry_1f_th1s_w4s_annoy1ng}