# 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 Haskellerror print). yikes, i've never done Haskell reversing before. however, there is one main resourceon this i found, a haskell decompiler with a writeup from the author on reversing for a previousCTF'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 thanwhat's in the writeup, but it is handled by hsdecomp. however, running hsdecomp (even with errorcode 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 ofcomposing xor and integer bits, so this operation is computing the XOR of the input. the error pointin map is just calling Show on each integer, so the integers getting generated by Main_v_info areconverted into strings for XOR. now the issue is the last error point which is generating theintegers. we can see it stops when Main_r_info on the first arg is equal to the arg. examiningMain_r_info shows that it reverses the digits of a given integer. thus, integer generation stopswhen there's a palindrome, and otherwise it generates a list of integers transformed by someerroring closure. the initial integer is 1997. i spent a lot of time trying to reverse the lasterror 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 thencomputing the XOR of the output with the input to recover the key. by default we see it starts with1997 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 iguessed 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 foundexactly one such offset -- 152.
then i created a script to patch the binary for 1997, the exact instruction that loads it can befound in `s3m8_info` at address `0x407c57`. i found this by simply searching the memory in ghidrafor 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{` atoffset 152.
```pythondef 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 thatwork, 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 shouldhave tried fuzzing _before_ i sunk like 4 hours total into kinda fruitless RE. if someone could fixup hsdecomp so it works on this i would be very happy.