Writeup by etan.1444 for La routine

intro reverse linux x86/x64

June 14, 2026

Basic static / dynamic analysis

Running file and checksec reveals that the binary is not stripped and PIE is disabled.

A review of strings yielded no interesting results, and running the binary with strace or ltrace did not provide any useful insights.

When executing the binary, it prompts for user input and then prints an output.

$ ls
La_routine
$ ./la_routine
FCSC{test}
Essayez encore

Static analysis

Using Binary Ninja, we can easily locate main.main since the binary is not stripped.

Because Go is a high-level language, the binary includes a significant amount of initialization and garbage collection overhead. Furthermore, all necessary libraries are statically linked within the compiled binary.

image

At the end of the main.main function, we find an input scan from stdin, followed by a conditional check and a print statement, confirming our observations from the initial dynamic analysis. image

Inside the main.check function, we analyze how and why the function returns. There are three distinct return paths:

  • A return triggered if an error occurs.
  • A return triggered if the flag length is not a multiple of 4.
  • A final return reached once the loop completes successfully. image

Let’s focus on the primary verification logic. The application calls internal/bytealg.Compare, passing main.expected and an unknown buffer as arguments.

image image

Now for the core algorithm: the function appears to transform our input before comparing it to the main.expected array we identified earlier.

image

Let’s solve this problem dynamically.

Dynamic analysis

For the dynamic analysis, we will use a GDB script.

We have identified a few functions that might be useful:

  1. a function to calculate pie address (though it is redundant here since PIE is disabled) .
def pie_calc_address(binja_address):
   # exe_base = 0x555555554000
   exe_base = 0x400000
   binja_base = 0x400000
   offset = binja_address - binja_base
   exe_address = exe_base + offset
   return hex(exe_address)
  1. functions to break and watch.
def pie_break(binja_address):
   exe_address = pie_calc_address(binja_address)
   gdb.execute(f"break *{exe_address}")
   gdb.execute("commands $bpnum\nsilent\nend")
   
def pie_watch(binja_address):
   exe_address = pie_calc_address(binja_address)
   gdb.execute(f"watch *{exe_address}")
  1. A function to print memory content at a specific address and parse code.
def pie_print(binja_address, message):
   exe_address = pie_calc_address(binja_address)
   tokens = re.findall(r'\{\*?([\$\w\+\-\*\d\s]+),\s*(%\w+)\}', message)
   fmt = re.sub(r'\{\*?[\$\w\+\-\*\d\s]+,\s*%\w+\}', lambda m: re.search(r'%\w+', m.group()).group(), message)
   gdb_args = ", ".join(f"*(char*)({expr})" if m.startswith('{*') else expr.replace('$', '$')
                        for m, (expr, _) in zip(re.findall(r'\{[^}]+\}', message), tokens))
   if gdb_args:
       gdb.execute(f'dprintf *{exe_address}, "{fmt}\\n", {gdb_args}')
   else:
       gdb.execute(f'dprintf *{exe_address}, "{fmt}\\n"')
      
def binary_ninja_dbg(code):
   lines = code.split("\n")
   for line in lines:
       if len(line.split()) <= 1: continue
       address, sep, code = line.partition(" ")
       address = int(address, 16)
       print(address, sep + code)
       pie_print(address, sep + code)

Let’s set a breakpoint at the compare function.

# gdb script
python pie_break(0x004a698d) # compare function break
  
python pie_print(0x004a698a, "0x004a698a rax = {$rax, %s}")  # print expected
python pie_print(0x004a698a, "0x004a698a rdi = {$rdi, %s}")  # print our transformed flag
python pie_print(0x004a698a, "0x004a698a call internal/bytealg.Compare")
run <<< FCSC{fake_flag.}

image image

We discovered that the first five characters of the transformed input match the prefix of the expected flag (FSCS{).

This indicates that the algorithm processes and transforms our input character by character, independently. Consequently, we can brute-force the flag one character at a time by comparing the output of each attempt against the main.expected array.

We can retrieve the flag using the following function:

def brute_force():
   n = 48
   res = ['.']*n
   ind = 0
   pie_break(0x004a698d)

   while (ind < n):
       for c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890{}_!":
           res[ind] = c
           gdb.execute(f"run <<< {"".join(res)}", to_string=True)

           inferior = gdb.selected_inferior()
           expected_address = int(gdb.parse_and_eval("$rax"))
           expected_len = int(gdb.parse_and_eval("$rbx"))
           expected = inferior.read_memory(expected_address, expected_len).tobytes().decode('utf-8', errors='ignore')
           flag_address = int(gdb.parse_and_eval("$rdi"))
           flag_len = int(gdb.parse_and_eval("$rsi"))
           flag = inferior.read_memory(flag_address, flag_len).tobytes().decode('utf-8', errors='ignore')

           if expected[ind] == flag[ind]:
               print("".join(res))
               ind += 1
               break

image

FCSC{GoLanG\_.......................\_P4TTerNs!!!}

Here is the full script.

set sysroot /
set disable-randomization on
set confirm off
set print thread-events off
set verbose off
set debuginfod enabled off
set print inferior-events off
set print thread-events off

file ./la-routine

# python function
python
import re

def pie_calc_address(binja_address):
    # exe_base = 0x555555554000
    exe_base = 0x400000
    binja_base = 0x400000
    offset = binja_address - binja_base
    exe_address = exe_base + offset
    return hex(exe_address)

def pie_break(binja_address):
    exe_address = pie_calc_address(binja_address)
    gdb.execute(f"break *{exe_address}")
    gdb.execute("commands $bpnum\nsilent\nend")

def pie_print(binja_address, message):
    exe_address = pie_calc_address(binja_address)
    tokens = re.findall(r'\{\*?([\$\w\+\-\*\d\s]+),\s*(%\w+)\}', message)
    fmt = re.sub(r'\{\*?[\$\w\+\-\*\d\s]+,\s*%\w+\}', lambda m: re.search(r'%\w+', m.group()).group(), message)
    gdb_args = ", ".join(f"*(char*)({expr})" if m.startswith('{*') else expr.replace('$', '$')
                         for m, (expr, _) in zip(re.findall(r'\{[^}]+\}', message), tokens))
    if gdb_args:
        gdb.execute(f'dprintf *{exe_address}, "{fmt}\\n", {gdb_args}')
    else:
        gdb.execute(f'dprintf *{exe_address}, "{fmt}\\n"')

def pie_watch(binja_address):
    exe_address = pie_calc_address(binja_address)
    gdb.execute(f"watch *{exe_address}")

def binary_ninja_dbg(code):
    lines = code.split("\n")
    for line in lines:
        if len(line.split()) <= 1: continue
        address, sep, code = line.partition(" ")
        address = int(address, 16)
        print(address, sep + code)
        pie_print(address, sep + code)

def brute_force():
    n = 48
    res = ['.']*n
    ind = 0
    pie_break(0x004a698d)

    while (ind < n):
        for c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890{}_!":
            res[ind] = c
            gdb.execute(f"run <<< {"".join(res)}", to_string=True)

            inferior = gdb.selected_inferior()
            expected_address = int(gdb.parse_and_eval("$rax"))
            expected_len = int(gdb.parse_and_eval("$rbx"))
            expected = inferior.read_memory(expected_address, expected_len).tobytes().decode('utf-8', errors='ignore')
            flag_address = int(gdb.parse_and_eval("$rdi"))
            flag_len = int(gdb.parse_and_eval("$rsi"))
            flag = inferior.read_memory(flag_address, flag_len).tobytes().decode('utf-8', errors='ignore')

            if expected[ind] == flag[ind]:
                print("".join(res))
                ind += 1
                break
end

# gdb script
# python pie_break(0x004a698d)

# python pie_print(0x004a698a, "0x004a698a rax = {$rax, %s}")
# python pie_print(0x004a698a, "0x004a698a rdi = {$rdi, %s}")
# python pie_print(0x004a698a, "0x004a698a call internal/bytealg.Compare")
# run <<< FCSC{fake_flag.}
python brute_force()

To use it:

chmod +x la-routine
gdb --q --nx -x solve.gdb