Reverse Engineering
    CTF

    A reverse engineering challenge from a CTF competition

    February 26, 2025
    4 min read
    A reverse engineering challenge from a CTF competition

    📌 Note

    Due to a real challenge scenario, the flag will be masked.


    Introduction

    In this writeup, we will analyze a reverse engineering CTF, which requires reverse engineering a 64-bit ELF binary to extract the correct password and retrieve the flag.

    The approach follows a two-phase methodology:

    1. Static Analysis – Extracting preliminary information without executing the binary.
    2. Dynamic Analysis – Debugging the binary to observe its runtime behavior.

    Tools Used

    • strings – Extract embedded strings from the binary.
    • objdump – Disassemble the binary and analyze its assembly.
    • GDB – Debug the binary dynamically.
    • Ghidra – Reverse-engineer and analyze the decompiled C code.

    By the end, we will have a full understanding of the binary’s logic and successfully retrieve the flag.


    Step 1: Initial Analysis of the Binary

    A reverse engineering challenge from a CTF competition
    rainpwn@0xdeadc0de:~$ ./challenge_binary
    ./challenge <password>
    
    rainpwn@0xdeadc0de:~$ ./challenge_binary p4ssw0rd
    rainpwn@0xdeadc0de:~$

    1.1 Identifying the File Type

    First, let’s identify the format of the executable:

    A reverse engineering challenge from a CTF competition
    rainpwn@0xdeadc0de:~$ file challenge_binary
    challenge_binary: ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped
    rainpwn@0xdeadc0de:~$

    Observations:

    • The file is a 64-bit ELF binary compiled for the x86-64 architecture.
    • It is not stripped, meaning function names and symbols are present, making the analysis easier.

    1.2 Extracting Embedded Strings

    Let’s check for hardcoded strings in the binary:

    A reverse engineering challenge from a CTF competition
    rainpwn@0xdeadc0de:~$ strings challenge_binary

    Interesting output:

    A reverse engineering challenge from a CTF competition
    <...>
    ./challenge <password>
    > FLAG{%s}
    strncmp
    puts
    printf
    main
    <...>

    Key insights:

    • The program expects a password as an argument (./challenge <password>).
    • The format string FLAG{%s} suggests that the flag is dynamically constructed.
    • The presence of strncmp implies a direct string comparison, meaning our input is checked against a stored value.

    Step 2: Disassembling the Binary with objdump

    To understand the binary’s logic, we disassemble it using objdump:

    A reverse engineering challenge from a CTF competition
    rainpwn@0xdeadc0de:~$ objdump -D challenge_binary | grep -A20 "<main>"

    Relevant portion of the main function:

    A reverse engineering challenge from a CTF competition
        1161:	48 8d 3d f9 00 00 00 	lea    0xf9(%rip),%rdi        # 1261 <main>
        1168:	ff 15 72 2e 00 00    	call   *0x2e72(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
        116e:	f4                   	hlt
        116f:	90                   	nop
    
    0000000000001170 <deregister_tm_clones>:
        1170:	48 8d 3d 99 2e 00 00 	lea    0x2e99(%rip),%rdi        # 4010 <__TMC_END__>
        1177:	48 8d 05 92 2e 00 00 	lea    0x2e92(%rip),%rax        # 4010 <__TMC_END__>
        117e:	48 39 f8             	cmp    %rdi,%rax
        1181:	74 15                	je     1198 <deregister_tm_clones+0x28>
        1183:	48 8b 05 4e 2e 00 00 	mov    0x2e4e(%rip),%rax        # 3fd8 <_ITM_deregisterTMCloneTable>
        118a:	48 85 c0             	test   %rax,%rax
        118d:	74 09                	je     1198 <deregister_tm_clones+0x28>
        118f:	ff e0                	jmp    *%rax
        1191:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)
        1198:	c3                   	ret
        1199:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)
    
    00000000000011a0 <register_tm_clones>:
        11a0:	48 8d 3d 69 2e 00 00 	lea    0x2e69(%rip),%rdi        # 4010 <__TMC_END__>
        11a7:	48 8d 35 62 2e 00 00 	lea    0x2e62(%rip),%rsi        # 4010 <__TMC_END__>
    --
    0000000000001261 <main>:
        1261:	f3 0f 1e fa          	endbr64
        1265:	55                   	push   %rbp
        1266:	48 89 e5             	mov    %rsp,%rbp
        1269:	48 81 ec b0 00 00 00 	sub    $0xb0,%rsp
        1270:	89 bd 5c ff ff ff    	mov    %edi,-0xa4(%rbp)
        1276:	48 89 b5 50 ff ff ff 	mov    %rsi,-0xb0(%rbp)
        127d:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
        1284:	00 00 
        1286:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
        128a:	31 c0                	xor    %eax,%eax
        128c:	48 8d 85 60 ff ff ff 	lea    -0xa0(%rbp),%rax
        1293:	ba 98 00 00 00       	mov    $0x98,%edx
        1298:	be 00 00 00 00       	mov    $0x0,%esi
        129d:	48 89 c7             	mov    %rax,%rdi
        12a0:	e8 7b fe ff ff       	call   1120 <memset@plt>
        12a5:	48 8d 85 60 ff ff ff 	lea    -0xa0(%rbp),%rax
        12ac:	48 83 c0 08          	add    $0x8,%rax
        12b0:	48 89 c7             	mov    %rax,%rdi
        12b3:	e8 78 fe ff ff       	call   1130 <sigemptyset@plt>
        12b8:	48 8d 05 6a ff ff ff 	lea    -0x96(%rip),%rax        # 1229 <segill_sigaction>

    Key observations:

    • Control Flow Protection: The presence of endbr64 suggests CET (Control-flow Enforcement Technology).
    • Stack Setup: The program sets up a local buffer and manipulates memory.
    • Signal Handling: sigemptyset@plt is used, indicating possible anti-debugging mechanisms.

    Step 3: Reverse Engineering with Ghidra

    3.1 Examining the main Function

    Here is the decompiled output of main:

    A reverse engineering challenge from a CTF competition
    void main(void) {
      sigaction local_a8;
      memset(&local_a8,0,0x98);
      sigemptyset(&local_a8.sa_mask);
      local_a8.__sigaction_handler.sa_handler = segill_sigaction;
      local_a8.sa_flags = 4;
      sigaction(4,&local_a8,(sigaction *)0x0);
    
      do {
        invalidInstructionException();
      } while (true);
    }

    🔴 Anti-Debugging Mechanism:

    • The binary registers a signal handler (sigaction) for SIGILL (Illegal Instruction Exception).
    • It intentionally triggers an invalid instruction, causing a crash when executed under certain conditions.
    • This is a classic anti-debugging trick to detect analysis tools like GDB.

    Step 4: Extracting the Flag

    Instead of bypassing the anti-debugging mechanisms manually, we search for the hardcoded flag using objdump in .rodata.

    Yes, i'm lazy. :)

    What is the .rodata section?

    The .rodata section (Read-Only Data) in an ELF binary contains constant data that should remain immutable during the program's execution, such as constant strings, lookup tables, and other static data. Typically, this section holds:

    • Constant text strings used by functions like printf, puts, strncmp, etc.
    • Constant numeric values, such as predefined tables or hardcoded values.
    • Static initialization data, like default lookup tables or arrays.

    Analyzing the binary challenge_binary, i found several lea (Load Effective Address) instructions pointing to addresses relative to the RIP register. These addresses often correspond to data in the .rodata section.


    How .rodata and LEA are related

    In the assembly, i found these instructions:

    A reverse engineering challenge from a CTF competition
    1342:	48 8d 35 d2 0c 00 00 	lea    0xcd2(%rip),%rsi        # 201b <_IO_stdin_used+0x1b>
    1372:	48 8d 35 a6 0c 00 00 	lea    0xca6(%rip),%rsi        # 201f <_IO_stdin_used+0x1f>
    13a2:	48 8d 35 7a 0c 00 00 	lea    0xc7a(%rip),%rsi        # 2023 <_IO_stdin_used+0x23>
    13ce:	48 8d 35 52 0c 00 00 	lea    0xc52(%rip),%rsi        # 2027 <_IO_stdin_used+0x27>

    The LEA (Load Effective Address) instruction is used to calculate a memory address without performing an actual memory access. Here, lea X(%rip), %rsi is loading the address RIP + X into the RSI register.

    For example:

    A reverse engineering challenge from a CTF competition
    lea    0xcd2(%rip),%rsi    # 201b <_IO_stdin_used+0x1b>

    Here, 0x201b is a memory address, and from gdb session, it contains:

    A reverse engineering challenge from a CTF competition
    (gdb) x/s 0x201b
    0x201b:	"Str"

    So, the value in RSI will be a pointer to the string "Str".

    If we repeat this for the other addresses:

    A reverse engineering challenge from a CTF competition
    (gdb) x/s 0x201f
    0x201f:	"_9x"
    (gdb) x/s 0x2023
    0x2023:	"Qb_"
    (gdb) x/s 0x2027
    0x2027:	"OQ1"

    These data are part of the .rodata section and likely make up a string or flag that the program checks using strncmp.


    Verifying .rodata with objdump

    To directly view the contents of the .rodata section in the binary:

    A reverse engineering challenge from a CTF competition
    rainpwn@0xdeadc0de:~$ objdump -s -j .rodata challenge_binary

    This will show the hexadecimal content of the .rodata section, from which we can reconstruct the entire flag FLAG{Str_9xQb_OQ1}.

    Output:

    A reverse engineering challenge from a CTF competition
    challenge_binary:     formato del file elf64-x86-64
    
    Contenuto della sezione .rodata:
     2000 01000200 2e2f6368 616c6c65 6e676520  ...../challenge 
     2010 3c706173 73776f72 643e0049 747a005f  <password>.Str._
     2020 306e004c 795f0055 4432003e 20485442  9x.Qb_.OQ1.> FLAG
     2030 7b25737d 0a00                        {%s}..          

    An alternative approach was to search for all instances of "lea" using objdump and then locate them using gdb.

    A reverse engineering challenge from a CTF competition
    rainpwn@0xdeadc0de:~$ objdump -D challenge_binary | grep -A20 "lea"
    A reverse engineering challenge from a CTF competition
        133a:	48 8b 00             	mov    (%rax),%rax
        133d:	ba 03 00 00 00       	mov    $0x3,%edx
        1342:	48 8d 35 d2 0c 00 00 	lea    0xcd2(%rip),%rsi        # 201b <_IO_stdin_used+0x1b>
        1349:	48 89 c7             	mov    %rax,%rdi
        134c:	e8 6f fd ff ff       	call   10c0 <strncmp@plt>
        1351:	85 c0                	test   %eax,%eax
        1353:	0f 85 d0 00 00 00    	jne    1429 <main+0x1c8>
        1359:	0f 0b                	ud2
        135b:	48 8b 85 50 ff ff ff 	mov    -0xb0(%rbp),%rax
        1362:	48 83 c0 08          	add    $0x8,%rax
        1366:	48 8b 00             	mov    (%rax),%rax
        1369:	48 83 c0 03          	add    $0x3,%rax
        136d:	ba 03 00 00 00       	mov    $0x3,%edx
        1372:	48 8d 35 a6 0c 00 00 	lea    0xca6(%rip),%rsi        # 201f <_IO_stdin_used+0x1f>
        1379:	48 89 c7             	mov    %rax,%rdi
        137c:	e8 3f fd ff ff       	call   10c0 <strncmp@plt>
        1381:	85 c0                	test   %eax,%eax
        1383:	0f 85 97 00 00 00    	jne    1420 <main+0x1bf>
        1389:	0f 0b                	ud2
        138b:	48 8b 85 50 ff ff ff 	mov    -0xb0(%rbp),%rax
        1392:	48 83 c0 08          	add    $0x8,%rax
        1396:	48 8b 00             	mov    (%rax),%rax
        1399:	48 83 c0 06          	add    $0x6,%rax
        139d:	ba 03 00 00 00       	mov    $0x3,%edx
        13a2:	48 8d 35 7a 0c 00 00 	lea    0xc7a(%rip),%rsi        # 2023 <_IO_stdin_used+0x23>
        13a9:	48 89 c7             	mov    %rax,%rdi
        13ac:	e8 0f fd ff ff       	call   10c0 <strncmp@plt>
        13b1:	85 c0                	test   %eax,%eax
        13b3:	75 62                	jne    1417 <main+0x1b6>
        13b5:	0f 0b                	ud2
        13b7:	48 8b 85 50 ff ff ff 	mov    -0xb0(%rbp),%rax
        13be:	48 83 c0 08          	add    $0x8,%rax
        13c2:	48 8b 00             	mov    (%rax),%rax
        13c5:	48 83 c0 09          	add    $0x9,%rax
        13c9:	ba 03 00 00 00       	mov    $0x3,%edx
        13ce:	48 8d 35 52 0c 00 00 	lea    0xc52(%rip),%rsi        # 2027 <_IO_stdin_used+0x27>
        13d5:	48 89 c7             	mov    %rax,%rdi
        13d8:	e8 e3 fc ff ff       	call   10c0 <strncmp@plt>
        13dd:	85 c0                	test   %eax,%eax

    Here, we identified these occurrences of "lea":

    A reverse engineering challenge from a CTF competition
        1342:	48 8d 35 d2 0c 00 00 	lea    0xcd2(%rip),%rsi        # 201b <_IO_stdin_used+0x1b>
        1372:	48 8d 35 a6 0c 00 00 	lea    0xca6(%rip),%rsi        # 201f <_IO_stdin_used+0x1f>
        13a2:	48 8d 35 7a 0c 00 00 	lea    0xc7a(%rip),%rsi        # 2023 <_IO_stdin_used+0x23>
        13ce:	48 8d 35 52 0c 00 00 	lea    0xc52(%rip),%rsi        # 2027 <_IO_stdin_used+0x27>

    Using gdb, we retrieved the flag:

    A reverse engineering challenge from a CTF competition
    rainpwn@0xdeadc0de:~$ gdb challenge_binary 
    A reverse engineering challenge from a CTF competition
    GNU gdb (Debian 16.2-1) 16.2
    <...>
    For help, type "help".
    Type "apropos word" to search for commands related to "word"...
    Reading symbols from challenge_binary...
    (No debugging symbols found in challenge_binary)
    (gdb) x/s 0x201b
    0x201b:	"Str"
    (gdb) x/s 0x201f
    0x201f:	"_9x"
    (gdb) x/s 0x2023
    0x2023:	"Qb_"
    (gdb) x/s 0x2027
    0x2027:	"OQ1"
    (gdb) 
    A reverse engineering challenge from a CTF competition
    rainpwn@0xdeadc0de:~$ ./challenge_binary Str_9xQb_OQ1 
    > FLAG{Str_9xQb_OQ1}

    Conclusion

    • The binary used anti-debugging techniques such as SIGILL trapping to hinder analysis.
    • Instead of patching the binary, we extracted the flag from .rodata using objdump.
    • The final flag: FLAG{Str_9xQb_OQ1}