Skip to content
Suzu
4 min read

【Reverse】AIS3 Pre-Exam 2026 Writeup: tetris

A reverse writeup for tetris, analyzing the static ELF and hidden flag path.

Series
30 posts
View the complete series →
On this page

【AIS3 Pre-Exam 2026 Writeup】Reverse - tetris

No source file was given. The binary became the source.

The directory only had one file:

ls -la
tetris

Basic triage:

file ./tetris
checksec --file=./tetris

The binary is a 64-bit Linux ELF. It is statically linked and stripped.

RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
SHSTK:      Enabled
IBT:        Enabled

No PIE was useful. All code and data addresses stay fixed, so gdb calls are easy later. Static and stripped was annoying, because a lot of libc code was mixed into the binary and there were no function names.

Running it showed a real terminal Tetris game:

./tetris

The screen printed controls and a board. It did not ask for a password or flag input. I first thought maybe I had to clear many lines or reach a hidden score. That was too slow to test by hand, and there was no obvious network or file trigger.

Strings:

strings -a -n 5 ./tetris | rg "AIS3|flag|FLAG|TETRIS|Score|Lines|Game Over|Press ENTER"

Only UI strings appeared:

TETRIS - Score: %d | Lines: %d
Game Over! Final Score: %d
Lines Cleared: %d
Press ENTER to start...

No AIS3{ in plain text. The static libc strings also made the output noisy.

I spent a short time on the printf/glibc side because the binary is static and there were many libc strings. The Tetris code only used normal print calls for UI text. There was no controlled format string, no suspicious printf table setup, and no post-write hook like a pwn task. This was not the useful path. No trigger, no path.

The entry point was 0x404550.

objdump -d -M intel --start-address=0x404550 --stop-address=0x404580 ./tetris

The important part:

404568: mov rdi,0x15c4017
40456f: call 0x1668660

So main is 0x15c4017. This was better than searching from the top of .text, because the static binary is huge.

Disassembly around main looked strange. There were many small jumps like this:

eb ff
c0 ...

At first I was not sure if objdump was decoding from the wrong place. After checking more blocks, it looked like anti-disassembly junk. Linear disassembly falls into fake bytes, but real execution jumps over them. I stopped trying to make the whole function pretty.

The useful way was to follow data references. The Tetris UI string address helped locate the game code. Then another nearby block looked more interesting than the normal drawing loop.

Two functions stood out:

0x15c30b5
0x15c317f

The first one copies 0x1c bytes from .data:

objdump -d -M intel --start-address=0x15c30b5 --stop-address=0x15c326d ./tetris

Important lines:

15c3154: mov edx,0x1c
15c3159: lea rax,[rip+0x4e2fd0] # 0x1aa6130
15c3163: lea rax,[rip+0x4e58f6] # 0x1aa8a60
15c316d: call 0x401030

0x1aa6130 was not text. It looked like encrypted data.

objdump -s --start-address=0x1aa6130 --stop-address=0x1aa6150 ./tetris
2e a5 56 46 0d 7c 8e dc 83 6f 30 83 ff f8 a5 5c
d0 76 d8 cd 99 dc 3f 39 9d 65 70 64

The second function uses 0x1aa8a60, calls another transform function, then prints it with %s.

15c31fe: mov esi,0x1c
15c3203: lea rax,[rip+0x4e5856] # 0x1aa8a60
15c320d: call 0x15c1c61
15c322e: lea rax,[rip+0x4e582b] # 0x1aa8a60
15c3238: lea rax,[rip+0x174119] # "%s" style print path
15c3247: call 0x168bfe0

This looked like decrypt-and-print. I was not sure if the functions needed game state, so I tested it in gdb instead of fully reversing the transform.

Because the binary is no PIE, the addresses can be called directly:

gdb -q ./tetris -batch \
  -ex 'set pagination off' \
  -ex 'set confirm off' \
  -ex 'b *0x15c4017' \
  -ex 'run' \
  -ex 'call ((void(*)())0x15c30b5)()' \
  -ex 'call ((void(*)())0x15c317f)()' \
  -ex 'x/s 0x1aa8a60' \
  -ex 'quit'

The buffer became:

0x1aa8a60: "AIS3{T3tr1s_P4tt3rn_M4st3r!}"

So playing Tetris was not needed. The hidden path only prepares and decodes the flag buffer.

Full solve script:

#!/usr/bin/env python3
import re
import subprocess


GDB_CMDS = [
    "set pagination off",
    "set confirm off",
    "b *0x15c4017",
    "run",
    "call ((void(*)())0x15c30b5)()",
    "call ((void(*)())0x15c317f)()",
    "x/s 0x1aa8a60",
    "quit",
]


def main():
    cmd = ["gdb", "-q", "./tetris", "-batch"]
    for gdb_cmd in GDB_CMDS:
        cmd += ["-ex", gdb_cmd]

    out = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode(
        errors="replace"
    )

    m = re.search(r'"(AIS3\{[^"]+\})"', out)
    if not m:
        print(out)
        raise SystemExit("flag not found")

    print(m.group(1))


if __name__ == "__main__":
    main()

Running it:

python3 solve.py

Flag:

AIS3{T3tr1s_P4tt3rn_M4st3r!}

Share this page