Hello! I’m a bit late to solving this challenge, but I’d like to share my thought process and walk through how I approached it.
Description
TwilightRev
Author: kanon
at twilight...
https://alpacahack.com/daily/challenges/twilightInitial Analysis
First, we download the challenge file and extract it locally. After inspecting the binary, we can confirm that the challenge file is a 64-bit ELF executable, and we are also provided with an out.txt file containing what appears to be an encrypted flag.
┌──(chjwoo㉿hackbox)-[~/…/alpacahack/rev/twilight/twilight]└─$ file challchall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=cad54f8480fb7120a6bf06f1647f1801e946a02c, for GNU/Linux 3.2.0, not stripped
┌──(chjwoo㉿hackbox)-[~/…/alpacahack/rev/twilight/twilight]└─$ cat out.txt0x41, 0x6D, 0x72, 0x62, 0x67, 0x64, 0x81, 0x46, 0x74, 0x79, 0x6B, 0x68, 0x6D, 0x45, 0x6F, 0x6C, 0x7B, 0x4E, 0x7B, 0x7D, 0x73, 0x42, 0x85, 0x79, 0x7C, 0x7C, 0x8C, 0x77, 0x7D, 0x73, 0x82, 0x62,The contents of out.txt reveal a list of hexadecimal values, which likely represent the encrypted flag. At this point, we know:
- The binary is a 64-bit ELF file
out.txtmost likely contains the encrypted output of the flag
Before diving deeper, we attempt a quick string extraction to see if anything useful stands out.
┌──(chjwoo㉿hackbox)-[~/…/alpacahack/rev/twilight/twilight]└─$ strings chall/lib64/ld-linux-x86-64.so.2__cxa_finalize__libc_start_mainstrlen__isoc99_scanfputchar__stack_chk_failprintflibc.so.6GLIBC_2.7GLIBC_2.4GLIBC_2.34GLIBC_2.2.5_ITM_deregisterTMCloneTable__gmon_start___ITM_registerTMCloneTablePTE1u+UHEnter the flag:%33s0x%X,:*3$"GCC: (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0Scrt1.o__abi_tagcrtstuff.cderegister_tm_clones__do_global_dtors_auxcompleted.0__do_global_dtors_aux_fini_array_entryframe_dummy__frame_dummy_init_array_entrychall.c__FRAME_END___DYNAMIC__GNU_EH_FRAME_HDR_GLOBAL_OFFSET_TABLE_putchar@GLIBC_2.2.5__libc_start_main@GLIBC_2.34_ITM_deregisterTMCloneTable_edata_finistrlen@GLIBC_2.2.5__stack_chk_fail@GLIBC_2.4printf@GLIBC_2.2.5__data_start__gmon_start____dso_handle_IO_stdin_used_end__bss_startmain__isoc99_scanf@GLIBC_2.7__TMC_END___ITM_registerTMCloneTable__cxa_finalize@GLIBC_2.2.5_init.symtab.strtab.shstrtab.interp.note.gnu.property.note.gnu.build-id.note.ABI-tag.gnu.hash.dynsym.dynstr.gnu.version.gnu.version_r.rela.dyn.rela.plt.init.plt.got.plt.sec.text.fini.rodata.eh_frame_hdr.eh_frame.init_array.fini_array.dynamic.data.bss.commentUnfortunately, nothing particularly interesting appears in the output. With no obvious clues from strings, we proceed to static analysis.
Static Analysis
We load the binary into Ghidra and begin by analyzing the main function.
undefined8 main(void)
{ uint out_flag; size_t len_flag; long in_FS_OFFSET; int counter; code *local_58 [2]; char input_flag [40]; long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28); printf("Enter the flag: "); __isoc99_scanf(&DAT_00102015,input_flag); local_58[0] = a; local_58[1] = b; counter = 0; while( true ) { len_flag = strlen(input_flag); if (len_flag <= (ulong)(long)counter) break; out_flag = (*local_58[counter % 2])(input_flag[counter],counter); printf("0x%X, ",(ulong)out_flag); counter = counter + 1; } putchar(10); if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0;}To make the analysis clearer, several variables were renamed. From this function, we can identify the following key points:
- The user input is stored in a 40-byte buffer
- Two functions are used:
aandb - These functions are stored in an array and called alternately
- The loop iterates over each character of the input
- If the index is even, function
ais called - If the index is odd, function
bis called - Each processed character is printed as a hexadecimal value
Next, we inspect both helper functions.
int a(int param_1,int param_2)
{ return param_2 + param_1;}
uint b(uint param_1,uint param_2)
{ return param_1 ^ param_2;}Function Behavior
- Function
aperforms simple addition between the character and its index - Function
bperforms an XOR operation between the character and its index
This alternating logic explains how the encrypted values in out.txt were generated.
Solution
To recover the original flag, we reverse each operation:
- For function
a, we subtract the index instead of adding it - For function
b, we apply XOR again, which naturally reverses the operation
Based on this logic, we write a solver script.
enc_flag = [0x41, 0x6D, 0x72, 0x62, 0x67, 0x64, 0x81, 0x46, 0x74, 0x79, 0x6B, 0x68, 0x6D, 0x45, 0x6F, 0x6C, 0x7B, 0x4E, 0x7B, 0x7D, 0x73, 0x42, 0x85, 0x79, 0x7C, 0x7C, 0x8C, 0x77, 0x7D, 0x73, 0x82, 0x62]
def fun_a(flag_index, counter): return flag_index - counter
def fun_b(flag_index, counter): return counter ^ flag_index
def decode_flag(enc_flag): flag = []
for i in range(len(enc_flag)): if i % 2 == 0: flag.append(fun_a(enc_flag[i], i)) else: flag.append(fun_b(enc_flag[i], i))
return bytes(flag)
if __name__ == "__main__": flag = decode_flag(enc_flag) print(flag.decode())Running the solver produces the original flag:
┌──(chjwoo㉿hackbox)-[~/…/alpacahack/rev/twilight/twilight]└─$ python solver.pyAlpaca{AlpacaHack_in_Wonderland}Flag
Alpaca{AlpacaHack_in_Wonderland}