Troll branch
I gonna make this real quick because this is a fake flag
So basically there are multiple ELF binary files embedded in the main executable. Each ELF binary validates every 7 consecutive bytes of the flag by encrypting it with XTEA and comparing with the constant hard-coded in the binary
The 7 consecutive bytes are changed into 8-byte sequence by this modification: a[i] = variable + variable / 255 + 1 and variable /= 255
We could recover the variable by using these steps
| |
The memfd_create -> fork -> fexecve is pretty insane and new to me, read the Lesson Learnt for more details
In the solve script I used z3 because I was lazy to infer the reverse formula
Solve script
| |
Challenge Trick
These are things you need to understand to figure out the author’s trick. I’m pretty sure you guys already know these things before but haven’t done any serious research on them, and yeah neither have I. So we will explore them together
Relocation
The structure of ELF64_Rela is
| |
For example:
R_X86_64_64: Write at offset r_offset the value of symbol’s address + Addend
Formula: P = S + A
R_X86_64_COPY: Copy the symbol’s actualy data into P
Formula: memcpy(P, S, sizeof(S))
R_X86_64_RELATIVE64: Write Base + Addend to offset r_offset
Formula: P = B + A
R_X86_64_GLOB_DAT: Write its symbol’s address at runtime to offset r_offset
Formula: P = S
R_X86_64_PC32/64: Write to offset r_offset a 32-bit/64-bit displacement from r_offset to the symbol’s address + r_addend
Formula: P = S + A - P
Symbol
The structure of ELF64_Sym is
| |
The st_info is important here. It is an one-byte variable storing two packed fields, I will mention only the important ones
The high 4-bit is Binding
- STB_LOCAL (0) visible only inside object file
- STB_GLOBAL (1) visible to all object files being linked
- STB_WEAK (2) Like STB_GLOBAL, but has weaker precedence
The low 4-bit is Type
- STT_NOTYPE (0) Undefined type
- STT_OBJECT (1) A data variable (Like array or struct)
- STT_FUNC (2) Executable code / function
- STT_GNU_IFUNC (10) GNU IFUNC
The significant pivot we need to take a look is STB_WEAK and STT_GNU_IFUNC. They could be used to change the program execution flow deliberately
When a program wants to resolve an address of a specific symbol, it does
Firstly if the symbol is defined in current binary, which usually means st_shndx != SHN_UNDEF, the program will get the value of st_value which is already resolved at the static linking phase by ld.so. Additionally, if STT_GNH_IFUNC flag is enabled, the shellcode stored at st_value will be triggered to resolve the address
Secondly, if the symbol is not defined, the program will hunt for the symbol’s address globally base on the string stored at index st_name in the dynamic string table
LinkMap
As you know in ELF there is a thing called Lazy Binding. Because of this feature, the address of a function in shared library will be dynamically resolved by ld.so directly or whenever the function is being called.
And the address is resolved and stored in GOT (Global Offset Table). Whenever a program wants to use a function, it calls an indirect stub called PLT (procedure linkage table) which is used to lazily resolved the GOT address or the PLT
In GOT, the first 3 elements are special.
| Index | Meaning |
|---|---|
| GOT[0] | Store the address to the dynamic section which supplies the information for ld.so |
| GOT[1] | Store a pointer to link_map structure used by ld.so to track loaded object |
| GOT[2] | Hold an address of dynamic runtime resolver _dl_runtime_resolved |
Alright these seem important but remembering it is unnecessary in some case, however in this challenge, the author abuses the GOT[1] to access the binary data before a binary’s entry point is actually executed
There is a technique in terms of exploiting called ret2dlresolve by hijacking the reloc_arg passed through the _dl_runtime_resolved. We could manually control the function that _dt_runtime_resolved resolves
Well talking about that would take a huge amount of time of me and you, so we only mention about how the challenge abuses it. If there is any other techniques using it, we could do research later
So basically its layout is
| |
l_info is using to cache the address of other dynamic sections. For example l_info[DT_SYMTAB] -> .dynsym
l_addr often stores loading bias, usually means the difference between the actual loaded base address with the preferred base address. For example, It could be used to leak libc base address because libc declares its static base in ELF header is 0x0. So loading bias = libc base
The real one
The general concept of this binary is leveraging the ability of the relocation process that linker performs to hide main the validation,
Suspicious Pivot
The first thing that we could notice is a weird rela.tivity section. By inspecting it, we could see that it is used to modifty the DT_RELASZ of each embedded binary. This part is not really important in terms of analyzing but drive a huge effect to the result of the challenge because the default embedded DT_RELASZ is not large enough to cover the whole hidden logic. So this part is used to expand. This is just a small obfuscation that could confuse us a little bit.
| |
Idk why I put this output here but yeah make it less “full text blog”
The truth is that in each relocation section of child program, it is redesigned to act like a simplified Virtual Machine that changes the value of the exit status variable
At first wrong = 1/correct = 0 so the exit() will explicitly show the correct result if the XTEA encrypted value match the hard-coded value. However, after passing the hidden validation in the relocation phase, if the input is not satisify, the correct will be changed to 1 -> The program always returns false.
So next we need to validate each relocation in each binary child. However it must be definitely a pattern, so we just need to analyze once. I will do with the first binary bin_0
Analyze the relocation VM
Well at this point, we need to do a little research, The following part abuses the link_map to access a hidden value in ld.so that stores information about the arguments.
The following logs are my translated instruction from the relocating progress, involving 4 relocation types
| |
Let’s analyze this shit carefully. Sorry, my disassembler is such a mess but I don’t know how to demonstrate the logs better.
Firstly, there is a repeated pattern you need to figure out
| |
This means to retrieve a 8-byte sequence at the address stored in symbol.st_value, then It will be copied into a specified address
Because 0x403ff0 is pointing to GOT[1] in our binary, it is iterating throw link_map 3 times
*(uint64_t *)link_map->l_next->l_next->l_next
It is equivalently getting the base address of ld.so, and then adding with offset 0x392d0. I don’t know what this is either, but after debugging the program, it shows an incrediable result
The value at 0x804180 is pointing to argv[1] which is our input
After doing a few research, I have known that it is trying to obtain the pointer pointing into the initial process stack through loader/linker internal data, by calculating the base address of ld.so and after that adding it with offset 0x392d0, we could leak the stack! Wow this is amazing the magic number 0x392d0 is insane, but should I remember this? Nah I just need to know there is such a magic like this, whenever I need it, I will find again
Now we know one observation what 0x804180 is. This is an enormous data because instead of blindly translate the assembly code we could concentrate on hunting for the input modification branch
There is a really weird part that took me a lot of time to figure out (of course using AI, although my assumption about this was at first correct but I ignored it because it just a flash idea flew throw my mind lol). That is, when I debug a program, I don’t understand why a function with 0xA flag enabled is not executed. The reason is the challenge set the st_shndx to zero (I mentioned the reason above)
To be honest, I don’t know, knowing these things are actually helpful or not because I feel like it is way too specific toward this challenge and reflex a tiny applicatable benefits
Well basically, when dealing with such a challenge highly obfuscated this like, I’m guessing that this virtual machine is also scrambled and modifed to be taxing to comprehend (after suffering a lots). So I think reading purely each line will break our mind (tried T_T). So we need to lift it to IR for easier analysis.
So we need to reconstruct our interpreter to make it cleaner. Then we extract each consecutive instruction, observe its repetition and merge it into a block with specific meaning.
For example
| |
This part could be completely lifted into
| |
Or this pattern
| |
Let’s analyze this
- First it pushes the address of the next function to
0x205b308which is theRWEsection to store the shellcode - Then it pulls shellcode, sets the value of
st_shndxto0xc(or any value different with zeero) and executes it - Then it will use
0x404040as a deliverer to execute theSTT_GNU_IFUNC(and it always uses0x404040)
We can lift these to
| |
There is also another JUMP pattern (the unexecuted one). The clear signal we figured out the changes of 0x8040de to zero. We could lift this pattern and simultaneously eliminate useless logs
By combining all lifted logs, we can understand how the program implements the validation
I came up with this script in order to automate my lifting process
| |
pure human suffering btw
In fact, trying hypotheses, writing scripts or reversing the logs took me over 20 hours. I was completely exhausted and drained. Whereas when I tried to solve it using GPT-5.5, it solved the challenge within 60 minutes… What a brutal joke
You can download the raw log here, and the lifted/deobfuscated log here
So basically the validation is
- It first generates a target array stored in 0x804198 -> 0x80419f
- It generates a constant array called
addend[i]at 0x804190 -> 0x804197- It stores its transformed input into 0x804188 -> 0x80418f
- The transformation formula is
res[i] = 7 * input[i] + addend[i]- Then it compares with the target array
So far we only need these observation, hunting for how the program changes the exit_status or other stuffs is unnecessary anymore
And from that we could reproduce the execution of each child and write a general solver to reverse the transformation, recover the input
But the inconvenient thing is that the addend[i] is not only derive from the [0x804190, 0x804197]. There is a few instructions for each input byte which hard-coded in the binary used to add additional value. There is not enough proof to prove it is stored at any specific address so we have to manually reproduce the process to figure out this value by performing a really complicated pattern matching (or it could be more simple but I haven’t tried so I don’t know)
There is an easier approach. First, we still create an interpreter but then we just need to feed it a decoy input value. Then we could calculate the total addend by performing some mathematical calculations (because we already know the input, and the transformation applied to every input does not change). The simplest decoy value is zero. At the end we can use the transformed data directly, because transformed[i] = 7 * 0 + addend[i]
Sorry I could not provide the exact solver script because of dreamhack’s rule but these analysis are my raw working process hope you guys like it. If you need any hints or how to write a proper solve script you could contact me. I’m very pleasant to share my works :D
Lesson Learnt
Hmm what did we have here? Well from the fake flag we could see that, anything that is executed before main entry point could be the choke point for reverse engineering. Without encountering such an equivalent challenge, it would be more difficult to figure out the correct path.
The
memfd_createfunction creates an anonymous file descriptor, it acts exactly the same as a normal file descriptor. For example, we canread,write, andmmap, but it does not exist in the filesystem (which means the file is not stored on disk).Forkcreates a child process inherited everything from its parent from process including its memory. ThePPIDof the child will be the PID of the process created it. And finallyfexecveis used to execute a program from an existed file descriptor (unlikeexecvewhich executes program using pathname)Unlike other reverse challenges, this reversing part of this challenge’s fake branch is rather simple and easy to do. There is no hard trick like obfuscation, packer or cryptography (well there is XTEA but not hard). The program is not effortless to reverse, we still need to devote considerable time to catch on the logic
There could be other techniques related to
ld.soused to hide deliberated branchIf there is a suspicious section with an abnormal permission. For example
WRITEfor unncessary section such as.relaorRWXfor an abitrary section could be a strong signal to triageTo get better at analyzing the virtual machine, we could write a disassembler/interpreter and run it with our input, the interpreter is luck-based because we’re hardly able to manage all the internal logic but the only thing it need to be accurate is the
meaningand theexecution branch.Lifting raw disassembled/interpreted script to readable IR version is a good approach to deobfuscate and make the pattern clearer. My trial is the proof for this, I was working with the raw version and took almost 15 hours without gaining any useful insight, whereas with the IR one, I solved the challenge in just 3-4 hours
If you notice any misleading information or incorrect parts in my writeup, don’t hesitate to DM me, I would be very pleased :D