- Published on
elfisyou
- Authors
- Name
- kerszi
Introduction

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.

What's Going On?
By connecting to the address:
socat file:$(tty),raw,echo=0 tcp:chall.lac.tf:31189

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.

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. ;)

Flag:lactf{1m_r3ally_s0rry_1f_th1s_w4s_annoy1ng}