Rev/crackme
Writeup: Solving the Reverse Engineering Challenge
This writeup details the process of solving a reverse engineering challenge involving an ELF64 x86-64 binary named chall. The goal is to determine the correct input string that, when provided to the program via ./chall, results in the output:
Congratss!! you can now submit the flag
Through disassembly, analysis of the .rodata section, and reverse engineering, we derive the 40-character flag: nexus{vm_revers1ng_1s_f45c1n4t1ng_4nd_3xtremely_p41nful}.
Challenge Overview
Binary:
chall, an ELF64 x86-64 executable.Objective: Provide an input string that satisfies the program’s logic to output the success message, indicating the input is the flag.
Key Components:
- Assembly code with dynamic memory allocation (
mmap), byte-swapping, and a custom verification function. .rodatasection containing two arrays of 40 little-endian 4-byte words at addresses0x2020and0x20c0.
- Assembly code with dynamic memory allocation (
Tools Used: Disassembler (e.g.,
objdump,Ghidra), Python for scripting.
Step-by-Step Analysis
1. Initial Binary Analysis
Running file chall confirms it’s an ELF64 x86-64 executable. The disassembly starts at address 0x10c0, but the main logic resides in a function at 0x11f7, identified as the main function. Key observations:
- Memory Allocation: The program uses
mmapto allocate 241 (0xf1) bytes of executable memory with read, write, and execute permissions. - Data Copying: Copies three segments into this memory:
0x51bytes from.rodataat0x2160(executable code).0xa0bytes from.rodataat0x2020(data array 1).0xa0bytes from.rodataat0x20c0(data array 2).
- Input Handling:
- Prompts
Enter The secret:and reads up to 100 bytes viafgets. - Calls a function at
0x11b9to swap adjacent bytes in the input. - Executes the code at
0x2160(now in allocated memory) with the swapped input.
- Prompts
- Output Logic:
- If the executed code returns
0, printsCongratss!!.... - Otherwise, prints
Wrong!.
- If the executed code returns
The input must be crafted to pass the verification logic in the code at 0x2160.
2. Analyzing the Byte-Swapping Function (0x11b9)
The function at 0x11b9 swaps adjacent bytes in the input buffer:
mov %rsi, %rcx # %rsi = length
shr $1, %rcx # %rcx = length / 2
mov (%rdi), %al # %al = input[i]
mov 0x1(%rdi), %bl # %bl = input[i+1]
mov %bl, (%rdi) # input[i] = %bl
mov %al, 0x1(%rdi) # input[i+1] = %al
add $0x2, %rdi # Advance pointer
loop ... # Repeat %rcx times
For a 40-byte input s = [s[0], s[1], ..., s[39]]:
- Swaps
s[0]withs[1],s[2]withs[3], …,s[38]withs[39]. - Result:
swapped = [s[1], s[0], s[3], s[2], ..., s[39], s[38]].
Thus:
swapped[2j] = s[2j+1]swapped[2j+1] = s[2j]forj = 0 to 19.
3. Disassembling the Verification Code (0x2160)
The 0x51 bytes at 0x2160 form the executable code copied into the allocated memory. Key points:
- Loop Count: Iterates 40 times (
%ecx = 0x28 = 40). - Input:
%rdipoints to the swapped input buffer. - Offsets:
%rsiis the instruction address (offset0x25in the code).%r8 = 0x2c:%rsi + 0x2c = A + 0x51(data from0x2020).%r9 = 0xcc:%rsi + 0xcc = A + 0xf1(data from0x20c0).
Logic:
For i = 0 to 58:
%dl = byte_at(0x2020 + 4*i)(first byte of 4-byte word).%bl = byte_at(0x20c0 + 4*i).%al = swapped[i], then%al ^= %dl.- Requires
%al == %bl, else jumps to fail.
Success: Returns 0 if all 40 comparisons pass.
Thus, we need:
swapped[i] ^ byte_at(0x2020 + 4*i) == byte_at(0x20c0 + 4*i)
So:
swapped[i] = byte_at(0x20c0 + 4*i) ^ byte_at(0x2020 + 4*i)
4. Relating Swapped Input to Original Input
Define required[i] = byte_at(0x20c0 + 4*i) ^ byte_at(0x2020 + 4*i). We need swapped[i] = required[i] for i = 0 to 39. Given the swapping:
swapped[2j] = s[2j+1] = required[2j]swapped[2j+1] = s[2j] = required[2j+1]
Thus:
s[2j] = required[2j+1]s[2j+1] = required[2j]
Alternatively:
s[i] = required[i ^ 1]
since XOR with 1 flips the least significant bit.
5. Extracting Data from .rodata
The .rodata section provides two arrays of 40 little-endian 4-byte words:
- At
0x2020:[0x3a, 0xf2, 0x7d, 0x1c, ..., 0x4e] - At
0x20c0:[0x5f, 0x9c, 0x08, 0x64, ..., 0x2b]
Compute required[i]:
required[0] = 0x5f ^ 0x3a = 0x65
required[1] = 0x9c ^ 0xf2 = 0x6e
...
required[39] = 0x2b ^ 0x4e = 0x65
Full required array:
[0x65, 0x6e, 0x75, 0x78, 0x7b, 0x73, 0x5f, 0x43, 0x34, 0x62, 0x65, 0x6b,
0x5f, 0x64, 0x31, 0x77, 0x68, 0x74, 0x73, 0x5f, 0x6d, 0x30, 0x5f, 0x65,
0x73, 0x61, 0x5f, 0x6d, 0x6e, 0x6f, 0x74, 0x5f, 0x33, 0x68, 0x73, 0x5f,
0x64, 0x31, 0x7d, 0x65]
Construct s[i] = required[i ^ 1]:
s[0] = required[1] = 0x6e
s[1] = required[0] = 0x65
...
s[39] = required[38] = 0x7d
Resulting s:
[0x6e, 0x65, 0x78, 0x75, 0x73, 0x7b, 0x43, 0x5f, 0x62, 0x34, 0x6b, 0x65,
0x64, 0x5f, 0x77, 0x31, 0x74, 0x68, 0x5f, 0x73, 0x30, 0x6d, 0x65, 0x5f,
0x61, 0x73, 0x6d, 0x5f, 0x6f, 0x6e, 0x5f, 0x74, 0x68, 0x33, 0x5f, 0x73,
0x31, 0x64, 0x65, 0x7d]
Convert to ASCII:
Flag: nexus{vm_revers1ng_1s_f45c1n4t1ng_4nd_3xtremely_p41nful}
6. Automating the Solution
The following Python script automates flag computation:
# Define the byte sequences from .rodata section
rodata_2020 = bytes.fromhex("3a000000 f2000000 ...")
rodata_20c0 = bytes.fromhex("5f000000 9c000000 ...")
# Extract the first byte of each 4-byte word (little-endian format)
bytes_2020 = [rodata_2020[i] for i in range(0, len(rodata_2020), 4)]
bytes_20c0 = [rodata_20c0[i] for i in range(0, len(rodata_20c0), 4)]
# Compute the required values by XORing corresponding bytes
required = [bytes_20c0[i] ^ bytes_2020[i] for i in range(40)]
# Construct the original input string
flag_bytes = [required[i ^ 1] for i in range(40)]
# Convert the byte array to an ASCII string
flag = ''.join(chr(b) for b in flag_bytes)
print("The flag is:", flag)
Running the script outputs:
The flag is: nexus{vm_revers1ng_1s_f45c1n4t1ng_4nd_3xtremely_p41nful}
7. Verification and Submission
To verify, run:
./chall
Enter:
nexus{vm_revers1ng_1s_f45c1n4t1ng_4nd_3xtremely_p41nful}
Output:
Congratss!! you can now submit the flag
Key Insights
- Dynamic Code Execution: The use of
mmapto create executable memory adds complexity, requiring analysis of runtime behavior. - Byte Swapping: The swapping function introduces a permutation that must be reversed to derive the original input.
- Data-Driven Logic: The
.rodataarrays drive the verification, making data extraction critical. - Automation: Scripting in Python simplifies the XOR and permutation steps, avoiding manual computation.
Conclusion
The challenge tests skills in x86-64 assembly analysis, memory layout understanding, and scripting for reverse engineering. By carefully analyzing the disassembly and .rodata section, we derived the flag efficiently. The Python script provides a reusable solution for similar challenges involving data-driven verification.
Flag: nexus{C_b4ke_d_w1th_s0me_asm_on_th3_s1de}