Tags: haskell reversing
Rating:
# THE LYCH KING
writeup by [haskal](https://awoo.systems) for [BLÅHAJ](https://blahaj.awoo.systems)
**Pwn/Rev**
**498 points**
**7 solves**
>and the bad seeds
>
>The binary was changed *ever so slightly* after the cipher text was generated
provided file: <LYCH_KING.zip>
## writeup
we immediately find out this is a Haskell binary (strings, or run it with no input to get a Haskell
error print). yikes, i've never done Haskell reversing before. however, there is one main resource
on this i found, a haskell decompiler with a writeup from the author on reversing for a previous
CTF's Haskell challenge
<https://github.com/gereeter/hsdecomp>
<https://sctf.ehsandev.com/reversing/lambda1.html>
note that the writeup is for 32-bit. this is a 64-bit binary so the ABI is slightly different than
what's in the writeup, but it is handled by hsdecomp. however, running hsdecomp (even with error
code patched out) results in a partial decompilation with several error points
```
Main_main_closure = >>= $fMonadIO
getArgs
(\s3mc_info_arg_0 ->
$
putStrLn
((\Main_a_info_arg_0 ->
case == r3jo_info Main_a_info_arg_0 [] of
False -> zipWith (on !!ERROR!! ord)
Main_a_info_arg_0
((\Main_g_info_arg_0 Main_g_info_arg_1 ->
case == r3jo_info Main_g_info_arg_0 [] of
False -> take
(length $fFoldable[] Main_g_info_arg_0)
(intercalate
[]
(map
!!ERROR!!
((\Main_v_info_arg_0 Main_v_info_arg_1 Main_v_info_arg_2 ->
case == $fEqInteger Main_v_info_arg_0 (Main_r_info Main_v_info_arg_0) of
False -> case >= $fOrdInteger Main_v_info_arg_2 (toInteger $fIntegralInt Main_v_info_arg_1) of
False -> : Main_v_info_arg_0 !!ERROR!!,
False -> [],
False -> []
)
Main_g_info_arg_1
(length $fFoldable[] Main_g_info_arg_0)
(S# 0)
)
)
),
False -> []
)
Main_a_info_arg_0
(S# 1997)
),
False -> []
)
(head s3mc_info_arg_0)
)
)
r3jo_info = $fEq[] $fEqChar
Main_r_info = \Main_r_info_arg_0 ->
case == $fEqInteger Main_r_info_arg_0 (S# 0) of
False -> + $fNumInteger (* $fNumInteger (mod $fIntegralInteger Main_r_info_arg_0 (S# 10)) (^ $fNumInteger $fIntegralInteger (S# 10) (- $fNumInteger (Main_mag_info Main_r_info_arg_0) (S# 1)))) (Main_r_info (div $fIntegralInteger Main_r_info_arg_0 (S# 10))),
False -> S# 0
Main_mag_info = \Main_mag_info_arg_0 ->
case == $fEqInteger Main_mag_info_arg_0 (S# 0) of
False -> case > $fOrdInteger Main_mag_info_arg_0 (S# 0) of
False -> case < $fOrdInteger Main_mag_info_arg_0 (S# 0) of
False -> patError 4871050,
False -> negate $fNumInteger (S# 1),
False -> + $fNumInteger (S# 1) (Main_mag_info (div $fIntegralInteger Main_mag_info_arg_0 (S# 10))),
False -> S# 0
```
by manually reversing i found the error point in zipwith is just something along the lines of
composing xor and integer bits, so this operation is computing the XOR of the input. the error point
in map is just calling Show on each integer, so the integers getting generated by Main_v_info are
converted into strings for XOR. now the issue is the last error point which is generating the
integers. we can see it stops when Main_r_info on the first arg is equal to the arg. examining
Main_r_info shows that it reverses the digits of a given integer. thus, integer generation stops
when there's a palindrome, and otherwise it generates a list of integers transformed by some
erroring closure. the initial integer is 1997. i spent a lot of time trying to reverse the last
error point and i didn't end up figuring out what it's doing besides something with Main_mag_info
(which computes log10 of its argument).
you can dump the integer stream it's using by calling the binary with a known string and then
computing the XOR of the output with the input to recover the key. by default we see it starts with
1997 and then a stream of numbers i was ultimately unable to reverse engineer.
so i gave up and switched to fuzzing. the flavor text says the binary was slightly changed, so i
guessed the initial argument of 1997 was changed to something else. by cribdragging the ciphertext
(looking for any points in the stream where XOR with any digits 0-9 can produce `rgbctf{`) i found
exactly one such offset -- 152.
then i created a script to patch the binary for 1997, the exact instruction that loads it can be
found in `s3m8_info` at address `0x407c57`. i found this by simply searching the memory in ghidra
for 1997. this corresponds to a file offset of `0x7c5b:0x7c5f`.
![ghidra view showing the instruction that must be patched](https://git.lain.faith/BLAHAJ/writeups/media/branch/writeups/2020/rgbctf/lych-king/ghidra.png)
then i tried numbers in order until the pad contained the right numbers to produce `rgbctf{` at
offset 152.
```python
def run_patch(i):
with open("lich", "rb") as f:
d = bytearray(f.read())
d[0x7c5b:0x7c5f] = struct.pack("<I", i)
with open("/tmp/lich", "wb") as f2:
f2.write(d)
return try_inp("a"*len(cipher))
for seed in range(100000):
res = run_patch(seed)
if len(res) >= len(cipher) and res[152:152+7] == b"9289134":
print(seed)
print(xor(res, cipher))
```
this actually completes in seconds, and found the correct decryption. there are multiple seeds that
work, but the "correct" one is intended to be `1585` as far as i can tell. the result is:
```
Kil'jaeden created the Lich King ages ago from the spirit of the orc shaman Ner'zhul to raise an undead army to conquer Azeroth for the Burning Legion. rgbctf{the flag is just rgb lol} Initially trapped within the Frozen Throne with Frostmourne, the Lich King eventually betrayed Kil'jaeden and merged with the human Arthas Menethil. When Frostmourne was destroyed and Arthas perished, Bolvar Fordragon became the new Lich King, imprisoning the master of the Scourge within the Frozen Throne once more in order to protect the world from future threats.:
```
see original writeup for full source etc
## addendum
i'm sad i never got to completely figure out how haskell reversing works, and in retrospect should
have tried fuzzing _before_ i sunk like 4 hours total into kinda fruitless RE. if someone could fix
up hsdecomp so it works on this i would be very happy.