Select Page

I really enjoyed this challenge during Plaid CTF even though we didn’t end up getting the flag because of a minor mistake as I will explain below. I spent a few precious hours on this challenge and did everything (well, mostly) right the first time and all indications that the exploit should have worked were there but I just couldn’t figure out why it failed so eventually gave up and went to do a different challenge. It was only later after reading the write-ups that I figured out my mistake, but I still felt pride for almost finishing.

This challenge was a 250 point Misc challenge in which we get a .sml file that contains the source code of a game and we also get an IP and port where the actual game is hosted.

everland.pwni.ng:7772

The source code is the following:

(* 
 * Setup 
 *
 * In which we define the realm of terror
 * Known as... Everland!
 *)

 
exception CatFlag of string
exception Malformed
exception GameOver of string

 
type health = int ref
type strength = int ref

 
type arena = health * strength * health * strength
type move = string * (arena -> ((unit -> unit) * (unit -> unit)))

 
type item = (string * (int * int * move list))
type bag = item list ref

 
datatype Enemy =
 Flag
| Nothing
| Opponent of (health * strength * move list * string * Enemy)
| Init of ((Enemy) -> Enemy)

 
datatype Player = Hero of (health * strength * move list ref * bag) 

 
datatype Choice = Fight | Forage | Eat

 
datatype Colors = RED | BLUE | GREEN | ORANGE | ARB of int

 
val player_max = 200

 
val enemy_names = [
 "Wild Balugaloo", 
 "Feral Pewpewpine", 
 "Spatula", 
 "Fearsome Skitterator",
 "Count Dorkula",
 "Splitter Splatter",
 "Elektrikal",
 "Puny Frogpole",
 "Skedankedanker",
 "Skaboose"
]

 
(* 
 * Helpers 
 *
 * In which we realize we can never truly
 * venture out on our own
 *)

 
fun max (a, b) = if a > b then a else b
fun min (a, b) = if a < b then a else b

 
val esc = (String.str (Char.chr 27))

 
(* Request Text from the user *)
fun prompt s =
let
 val _ = TextIO.output(TextIO.stdOut, s^" ")
 val _ = TextIO.flushOut(TextIO.stdOut)
 val result = TextIO.inputLine TextIO.stdIn
in
 case result
 of NONE => raise (GameOver "EOF received")
 | SOME x => String.substring(x, 0, (String.size x)-1)
end

 
(* Fun with ansi escape codes (tm) *)
fun get_color x = esc^"[38;5;"^(Int.toString x)^"m"

 
fun color s c =
 (case c
 of RED => get_color 160
 | BLUE => get_color 33
 | GREEN => get_color 42
 | ORANGE => get_color 202
 | ARB x => get_color x)^s^
 esc^"[0m"

 
fun smult s 0 = ""
 | smult s n = 
 if n < 0 then "" 
 else s^(smult s (n-1))

 
(* There's probably a built-in for div with ceil, but eh *)
fun cdiv (x, y) =
let
 val d = x div y
in
 if (d * y) <> x then d 1 else d
end

 
infix 5 cdiv 

 
(* MLton and SML/NJ use different Random libraries so we have this instead *)
val rng = ref (Int.fromLarge ((Time.toMilliseconds (Time.now ()))
 mod (Int.toLarge (List.length enemy_names))))

 
fun getIdx () =
 (rng := ((!rng) * 1001017) mod (List.length enemy_names);
 !rng)

 
(* Global Definitions *)
val should_capture = ref false

 
val has_captured = ref false

 
val captured = ref Nothing

 
val posessing = ref false

 
val name = prompt ("Welcome, mighty hero, to the abyss of "
 ^(color "Everland" BLUE)^". Pray tell, "
 ^"what is your name?")

 
val _ = TextIO.print "
"

 
(* No Peaking! *)

 
fun get_flag () =
 "pctf{Fake flag: please run on server}"

 
(* 
 * Attacks 
 *
 * In which we hone our skills and
 * prepare to do battle
 *)

 
fun kill_fn (my_h, my_s, their_h, their_s) =
let
 val h = !their_h
in
 (* It kills you. You're dead *)
 ((fn () => their_h := 0), (fn () => their_h := h))
end 

 
fun lunge_fn (my_h, my_s, their_h, their_s) =
let
 val th = !their_h
 val mh = !my_h
in
 (fn () => (their_h := 2*(!their_h) div 3; my_h := max(!my_h-10, 0)),
 fn () => (their_h := th; my_h := mh))
end

 
fun strike_fn (my_h, my_s, their_h, their_s) =
let
 val th = !their_h
in
 (fn () => (their_h := (max((!their_h)-15, 0))), fn () => (their_h := th))
end

 
fun empower_fn (my_h, my_s, their_h, their_s) =
let
 val mh = !my_h
 val ms = !my_s
in
 (fn () => (my_s := (!my_s   20); my_h := max(!my_h - 20, 0)),
 fn () => (my_s := ms; my_h := mh))
end

 
fun recoup_fn (my_h, my_s, their_h, their_s) =
let
 val mh = !my_h
 val ms = !my_s
in
 (fn () => (my_s := max(!my_s - 20, 0); my_h := (!my_h   10)),
 fn () => (my_s := ms; my_h := mh))
end

 
fun capture_fn (my_h, my_s, their_h, their_s) =
 ((fn () => should_capture := true), 
 (fn () => should_capture := false))

 
fun stink_fn (my_h, my_s, their_h, their_s) =
let
 val ts = !their_s
in
 (fn () => (their_s := max(!their_s - 10, 0)),
 fn () => (their_s := ts))
end

 
fun wrap_fn (my_h, my_s, their_h, their_s) =
let
 val th = !their_h
 val ts = !their_s
in
 (fn () => (their_s := max(!their_s - 5, 0); their_h := max(!their_h - 5, 0)),
 fn () => (their_s := ts; their_h := th))
end

 
fun pass_fn _ =
 (fn () => (), fn () => ())

 
val strike = ("Sword Strike", strike_fn)
val kill = ("Death Wave", kill_fn)
val lunge = ("Spear Lunge", lunge_fn)
val empower = ("Full Empower", empower_fn)
val pass = ("Skip Turn", pass_fn)
val recoup = ("Recouperate", recoup_fn)
val capture = ("Capture", capture_fn)
val stink = ("Stink Out", stink_fn)
val wrap = ("Vine Wrap", wrap_fn)

 
(* Ok this is a hack. But at least I admit that *)
val nop = (0.0, ("Pass", fn () => ()))

 
(* 
 * Control Flow Logic 
 * 
 * In which we weave a thrilling tale
 * Of pain sorrow, and pwnage
 *)

 
fun get_opt () =
let
 val opt = prompt( 
 "Are you ready for your next fight? You can:
"
 ^" - "^(color "fight
" GREEN)
 ^" - "^(color "forage
" GREEN)
 ^" - "^(color "use
" GREEN)
 ^" >")
in
 if opt = "fight" then Fight else
 if opt = "forage" then Forage else
 if opt = "use" then Eat else
 (TextIO.print "Sorry, what was that?
"; get_opt())
end

 
fun state_heuristic (arena as (my_h, my_s, their_h, their_s)) (n, move_fn) =
let
 val (activate, deactivate) = move_fn arena
 val _ = activate ()
 val mh = real(!my_h)
 val ms = real(!my_s)
 val th = real(!their_h)
 val ts = real(!their_s)
 val _ = deactivate ()
in
 ((ms/ts)   3.0*(mh/th), (n, activate))
end 

 
(* Looks like a garbage collector spilled their load *)
fun print_stats (Hero(p_h, p_s, p_ms, bag)) (Opponent(e_h, e_s, e_ms, e_name, _)) =
 TextIO.print (
 "
"^name^" ("^(color ((Int.toString (!p_s))^"st") GREEN)^") ["^
 (smult (color "|" GREEN) (((!p_h) * 10 div player_max ))) ^
 (smult " " (((player_max - (!p_h)) * 10 cdiv player_max))) ^ "] -- "^
 e_name^" ("^(color ((Int.toString (!e_s))^"st") RED)^"): "^
 (color ((Int.toString (!e_h))^"hp") RED)^"

"
 )
 | print_stats _ _ = ()

 
fun get_choice opts col =
 let 
 val move_names = List.map (fn (n, _) => color n col) opts
 val (_, move_str) = List.foldl (fn (name, (n, msg)) =>
 (n 1, msg^" - ("^(Int.toString n)^") "^name^"
")) (0, "") move_names
 val _ = TextIO.print (move_str^"
")
 val choice = Int.fromString (prompt ">") 
 in
 case choice
 of NONE => get_choice opts col
 | SOME x => get_elmt x opts opts col
 end
and get_elmt n [] opts col = get_choice opts col
 | get_elmt 0 (move::ms) _ _ = move
 | get_elmt n (_::ms) opts col = get_elmt (n-1) ms opts col
(* Fight! Fight! Fight! Fight! *)
fun fight (hero as Hero(p_h, p_s, p_ms, bag)) 
 (enemy as (Opponent (e_h, e_s, e_ms, e_name, next))) =
let
 val _ =
 if (!posessing) then
 (posessing := false;
 TextIO.print ("An "^(color "eerie" RED)^" wind rushes past, and"
 ^" a fallen foe rises again...

"))
 else
 let
 val arena = (e_h, e_s, p_h, p_s)
 val options = List.map (state_heuristic arena) e_ms
 val (_, (name, activate)) = List.foldl (fn (ad as (a, _), bd as (b, _)) =>
 if a > b then ad else bd) nop options
 val _ = TextIO.print (e_name^" used "^name^"!
")
 in
 activate () 
 end
in
 if (!p_h) <= 0 then
 raise GameOver "You Died!"
 else
 let
 val _ = print_stats hero enemy
 val _ = TextIO.print "Available Moves:
"
 val (name, init) = get_choice (!p_ms) RED
 val _ = TextIO.print ("Using: "^(color name RED)^"
")
 val (act, _) = init (p_h, p_s, e_h, e_s)
 val _ = act()
 val _ = if (!p_h) <= 0 then
 raise GameOver "You Killed Yourself!"
 else ()
 in
 if (!should_capture) andalso (not (!has_captured)) 
 then
 if (!e_h > 50) then
 (TextIO.print ("It was too strong, you failed to capture "^
 (color e_name ORANGE));
 enemy)
 else
 let
 val _ = should_capture := false
 val _ = has_captured := true
 val _ = captured := enemy
 (* Kill them so that you can heal yourself *)
 fun sacrifice_fn (my_h, my_s, their_h, their_s) =
 (fn () => (
 my_h := min((!my_h) min(!e_h, !my_s*10), player_max);
 e_h := (!e_h-(!my_h)*10);
 p_ms := List.filter (fn (n, _) => n <> "Sacrifice") (!p_ms)),
 fn () => ()) (* Only used by the AI, not us *)
 val _ = p_ms := (List.filter (fn (n, _) => n <> "Capture") (!p_ms))
 @[("Sacrifice", sacrifice_fn)]
 in
 next
 end
 else if (!e_h) <= 0 then (TextIO.print ("You Killed "^(color e_name
 ORANGE)^"!
"); next) else enemy
 end
end
 | fight _ _ = raise Malformed
(* Forage! Forage! Forage... Forage? *) 
fun forage (Hero (_, _, _, bag)) wilderness =
 case (!wilderness)
 of [] => TextIO.print "You look around, but find nothing!
"
 | ((item as (name, (_, _, _)))::rest) =>
 let
 val _ = TextIO.print ("Collected "^(color name BLUE)^"!
")
 val _ = bag := (item :: (!bag))
 in
 wilderness := rest
 end

 
(* Use! Use! Ok, you get the point *)
fun use (hero as Hero(p_h, p_s, p_ms, bag)) =
 case (!bag)
 of [] => TextIO.print "No Available Items!
"
 | bg =>
 let
 val _ = TextIO.print "Available Items:
"
 val (name, (hp, st, mvs)) = get_choice bg BLUE
 val _ = TextIO.print ("Using: "^(color name BLUE)^"
")
 val _ = p_h := max(!p_h, hp)
 val _ = p_s := max(!p_s, st)
 val _ = bag := List.filter (fn (n, _) => n <> name) (bg)
 val _ = p_ms := (!p_ms)@mvs
 in
 ()
 end

 
fun play_game hero Flag world _ =
 (TextIO.print ("Wow, looks like you beat all the enemies!
"
 ^"Guess you deserve a flag: "); raise (CatFlag (get_flag())))
 | play_game hero (Init f) world original =
 play_game hero (f original) world original
 | play_game hero enemy world original =
 case (print_stats hero enemy; get_opt ())
 of Fight => play_game hero (fight hero enemy) world original
 | Forage => (forage hero world; play_game hero enemy world original)
 | Eat => (use hero; play_game hero enemy world original)
 (* Needs more: play_game hero enemy world original *)
 
fun find_best (this as (Opponent (_, my_s, _, _, next))) best entity =
 if (!my_s) > best then find_best next (!my_s) this
 else find_best next best entity
 | find_best _ _ entity = entity
fun gen_enemies 0 = Init (fn e =>
 case (find_best e 0 Nothing)
 of (Opponent (health, strength, _, name, _)) =>
 (health := max((!strength)*5, 250);
 strength := max((!strength)*5, 250);
 posessing := true;
 Opponent (health, strength, [kill], "Posessed "^name, Flag))
 | _ => raise (GameOver "Boooo, the phantom never appeared...."))
 | gen_enemies n = Opponent(ref 80, ref 50, [
 lunge, 
 strike, 
 empower, 
 recoup
 ], List.nth (enemy_names, getIdx ()), gen_enemies (n-1))

 
fun start () =
let
 val wilderness = ref [
 ("Gross Weed", (20, 20, [stink])),
 ("Risky Dust", (0, 35, [])),
 ("Random Vines", (50, 0, [wrap])),
 ("Sacrificial Net", (0, 0, [capture])),
 ("Lucky Elixir", (100, 100, []))
 ]
 (* Hey, count yourself lucky. I could have made it 100 *)
 val num_enemies = 10

 
 val hero = Hero(ref 200, ref 100, ref [strike, empower, recoup, pass], 
 ref [("Max Health Potion", (200, 0, [])), ("Sharpened Dagger", (0, 100, []))])

 
 val _ = TextIO.print ("Well, "^(color name GREEN)^". It is valiant of you to venture here, but "
 ^(color "Everland" BLUE)^" is a place of darkness and despair.
Few adventurers "
 ^"have dared descend these halls, and fewer have returned alive.
"
 ^"Good luck, and godspeed.

")

 
 val enemies = gen_enemies num_enemies
 val _ = (play_game hero enemies wilderness enemies)
 handle (GameOver msg) => (TextIO.print (color ("GameOver: "^msg) RED))

 
 val _ = TextIO.print "
And thus, another passes to dust...
"
in
 ()
end

 
(* 
 * Start
 * 
 * In which our tale thusly begins
 * Join us, dear friend
 *) 

 
val _ = start()
 handle (CatFlag f) => TextIO.print (color (f^"

") (ARB 200))

 

Reading the code, we manage to figure out that we have to beat 10 enemies to get the flag. At first glance this seems rather hard until we notice the “lunge” function:

 

Now, “their_h” and “my_h” are relative to the one calling this function. So “my_h” would be my health If I would lunge at the enemy and “my_h” would be the enemies health if they would lunge at me. We as a player do not have this function to call so only the enemy can lunge at us. So that means that to translate the code, “my_h” is the enemies health and “their_h” is our health.

Good, so now if we look closer at the code, we notice the following:

their_h := 2*(!their_h) div 3;

This means that it multiplies the current player health and divides it by 3. Meaning that if the enemy lunges at us we would never be able to die because we would always get 0 [point] something health left.

The code also goes on to decrease the enemy health by 10:

my_h := max(!my_h-10, 0)

Perfect. So we found our first snag in the code. But the problem is that the enemy doesn’t have only the “lunge” function. He also does some other things such as “strike” which decreases the player health by 15. Fortunately for us, if we go to where the code dictates which action the enemy takes we see the following:

 

This means that the “state_heuristic” function gets called. If we scroll to that function we see something interesting:

 

ms” meaning my strength,   “ts” meaning their strength, “mh” meaning my health and “th” meaning their health. Now knowing that “ms” and “ts” are relative and the function caller is the enemy this means that “ts” is actually the player strength. Ok, so what would happen if the player strength is 0? Well, a division by 0 which in our case will return infinity and cause the action choice to default to the first action, which is “lunge”.

So if we can keep our strength 0 at all time we can force the enemy to only attack us using “lunge” which never kills us but damages the enemy for 10 health each time.

Now, looking at the code once more, we see a player action called “recoup”:

 

This subtracts 20 strength from the player and adds 10 health. The “max” function when subtracting strength defaults to the larger number so this means that when the current strength of the player is lower than 20, the subtracted strength is 0, meaning that we will never go under 0 strength. Meaning that we will always have a division by 0 in the heuristic function and we found a way to never die and kill every enemy.

By spamming the Recuperate action we are essentially unlocking the god mode. Well… until the last enemy which kills us in one hit.

Ok, so what happened?

Well, if we look at the source code we see the following:

 

While playing the game, we saw that the last enemy had his name start with “Possessed”. Well, here we see that this enemy is spawned when there are no more enemies left and has his health and strength buffed up and also has a single attack which is this “kill” action. And if we scroll up to the function we see this:

 

Oh… ok. So no going around that one. But in the actual gameplay, we notice that the possessed enemy hits us after our first move. So we get one shot to finish it off.

This is where “capture” and “sacrifice” come in:

 

We can capture an enemy if it’s health is less than 50 and then we can sacrifice which makes its health (!e_h-(!my_h)*10); meaning it subtracts 10 times the player health from the enemy health.

At this point in the CTF I was actually excited that I finally managed to crack the thing but for some reason capturing the last enemy before the possessed one and sacrificing it when battling the possessed enemy did not work and it wasn’t all that clear because my understanding of the code was that sacrificing the enemy would turn the health of the captured enemy to the previous formula and not the enemy that you are currently battling. Only after I read the write-up it finally clicked. The first enemy you encounter is in essence also the last one (only with a fancy possessed title in front). So my whole chain of thought landed me in the right direction but I was capturing the wrong enemy and sacrificing it.

So all in all, the final formula was this:

1. fight the first enemy until it has below 50 health

2. capture it

3. spam recuperate in order to reach the final possessed enemy

4. drink the health potion in order to regain max health

5. sacrifice the captured enemy and get the flag

 

Final thoughts:

I really enjoyed this challenge even though I was so close to finishing it but in the end gave up. I learned a lot and next time I will push myself harder on these types of challenges. The idea of a game challenge was awesome and I would like to congratulate the developer of this challenge for all his hard work and creativity. Would love to see more of this in the future.

  • InfoSec enthusiast
  • Penetration tester
  • CTF player