DEV Community

Robert Babaev
Robert Babaev

Posted on

NorthSec 2022: Rego Prototype Review

Rego Prototype Review was a challenge at NorthSec 2022 which was unlike the rest, involving just figuring out a new programming language. This programming language resembles a cocktail of Go and Prolog. It's a bit unique in that it uses logical programming, as well as Go-like assignments and Javascript object syntax. But this isn't Robert's Programming Language review, we have Fireship for that.

Challenge 1

This is a warm up challenge, designed to get you used to the language. You're told to input a JSON object akin to:

    "flag": "<FLAG ATTEMPT>"
Enter fullscreen mode Exit fullscreen mode

Then, Rego will run the code to eval the flag and tell you whether or not it's correct. Kinda like reverse engineering without the BS.

We have a couple things in the source of the first challenge.

package challenge1

default correct_flag = false

mycorize(s) := replace(replace(s, "o", "0"), "i", "y")

correct_flag {
    words := ["champ", "mycoverse", "exo", "meta", "cyber", "block", "chain", "life"]
    parts := split(input.flag, "-")
    parts[1] == mycorize(words[x])
    indexof(input.flag,  "4") == 15
    parts[3] == mycorize(words[y])
    [parts[0], x, y] == ["FLAG", 1, 7]
Enter fullscreen mode Exit fullscreen mode

Off the bat, you might be drawn to the massive cluster at the bottom. It's labeled "correct_flag" and has a couple of conditions. Rego is a logical programming language, meaning that it tries to find a way to make each condition true to the best of its ability. Therefore, some assignments can look a little strange. Either way, that entire cluster has to evaluate to true.

Within this cluster, we have an array of words called words, and we know the flag is split into parts separated by hyphens (the split() function should look familiar to anyone who's used Python for a while). We also know that parts[1] has to be the result of the mycorize() function applied to words[x]. Wait, x hasn't been specified, what?

This is where the magic of logical programming comes in. Remember how the language tries to make each condition true to the best of its ability? We see x again in that last condition:

[parts[0], x, y] == ["FLAG", 1, 7]
Enter fullscreen mode Exit fullscreen mode

So parts[0], x, and y have to be "FLAG", 1, and 7 respectively. And because of the way Rego works, that's effectively an assignment, since x and y have yet to be defined. Cool!

Once you know that, the rest follows basic programming (though there is a first class function with myconize, so definitely read up on it if that looks foreign), and you get the flag.


Challenge 2

package challenge2

default correct_flag = false

parts = split(input.flag, "-")
flag = split(parts[1], "")

correct_flag {
  parts[0] == "FLAG"
  regex.match("[0-9a-f]{18}", parts[1])


check_daeb6676fb {
  checks = ["{2,5,f,e}", "{f,e,0}", "{7,a}", "{0,3}", "{8,3,4}", "{9,3}", "{b,6}", "{3,1}", "{7,0}", "{5,6,7,f}", "{4,d,3,b}", "{0,6}", "{5,7,d,0}", "{b,0,f}", "{0,2}", "{e,6}", "{5,4,a,3}", "{9,a,8}"]
  glob.match(checks[i], [], flag[i])

check_6fab77bfa0 {
  checks = ["{1,0}", "{0,3,5,1}", "{a,8,1,d}", "{f,b,a}", "{8,4}", "{f,2}", "{e,0,9,8}", "{8,d}", "{b,9}", "{e,6}", "{4,8}", "{c,e,a}", "{2,b,0}", "{4,3}", "{2,c,9,b}", "{e,0,6,1}", "{8,7,b,e}", "{a,2}"]
  glob.match(checks[i], [], flag[i])

# A bunch of check functions later...

check_flag {
  not check_daeb6676fb
  not check_6fab77bfa0
  not check_1303f08f3b
  not check_1e7696120e
  not check_7dfd706380
  not check_428af9a07f
  not check_31c7b0af16
  not check_549bdbab83
  not check_544db8ad08
  not check_8d627e437c
  not check_a1ab717ea8
  not check_2a472fcb62
  not check_32ad655751
  not check_dd21d3c001
  not check_5ef3d7283b
  not check_122e25ab7a
  not check_a592a1319d
  not check_c657345566
  not check_7c7db63c8b
  not check_13feb5fbad
  not check_818e5b66b1
  not check_a40912a851
  not check_06e9b90392
  not check_6a9b5a6923
  not check_88634b49bd
  not check_0d9a709c04
  not check_c52d9dc000
  not check_4ef6d355b8
  not check_564539041c
  not check_736ca3e0fa
  not check_a896cae94d
  not check_f50c810345
Enter fullscreen mode Exit fullscreen mode

Gah, that's long. However, looking at it, it's basically a bunch of checks that look like regex matches. So you have an array that checks for each character in the flag. Catch is, all the checks are negated. So what this is really telling you is which characters aren't in the flag. So a flag all in hex that's 18 characters without the "FLAG-" bit, checking to make sure there are no illegal characters.

Now, you could do this by manually going through and figuring out which characters are remaining, but I like my Python too much and had too little time to do that. So I used good old sets and loops!

checks0 = ["{2,5,f,e}", "{f,e,0}", "{7,a}", "{0,3}", "{8,3,4}", "{9,3}", "{b,6}", "{3,1}", "{7,0}", "{5,6,7,f}", "{4,d,3,b}", "{0,6}", "{5,7,d,0}", "{b,0,f}", "{0,2}", "{e,6}", "{5,4,a,3}", "{9,a,8}"]
checks1 = ["{1,0}", "{0,3,5,1}", "{a,8,1,d}", "{f,b,a}", "{8,4}", "{f,2}", "{e,0,9,8}", "{8,d}", "{b,9}", "{e,6}", "{4,8}", "{c,e,a}", "{2,b,0}", "{4,3}", "{2,c,9,b}", "{e,0,6,1}", "{8,7,b,e}", "{a,2}"]
checks2 = ["{8,c,5}", "{b,1,7,f}", "{9,7,c}", "{0,e}", "{3,1}", "{0,e,7,6}", "{6,5,e}", "{9,c,4,3}", "{5,f,d}", "{8,f,7}", "{1,a,f}", "{4,0}", "{8,3,9}", "{1,4}", "{a,2}", "{6,3}", "{4,f,6}", "{4,e,f}"]
checks3 = ["{0,5}", "{2,a,3}", "{8,7,e,f}", "{3,8,7,9}", "{2,6,9,7}", "{1,7}", "{d,b,a,5}", "{a,c,b}", "{a,7,1}", "{3,9,2,5}", "{8,2,7}", "{d,a,1,5}", "{8,3}", "{8,3,d}", "{0,b,9}", "{0,1}", "{b,c,1}", "{4,2}"]

checks = [

possible_chars = set([s for s in "0123456789abcdef"])
flag = "FLAG-"
for i in range(18):
    illegal_chars = set()
    for check_set in checks:
        cleaned_check_set = check_set[i].replace("{", "").replace("}","").split(",")
        for illegal_char in cleaned_check_set:
    flag += possible_chars.difference(illegal_chars).pop()
Enter fullscreen mode Exit fullscreen mode


Challenge 3

package challenge3

default correct_flag = false

parts = split(input.flag, "FLAG-")
flag = split(parts[1], "")

f(s) = sp {
  c := flag[s.i]
  g[s.s][0] == c
  j := count({ x | array.slice(flag, 0, s.i)[x] == c })
  sp := { "i": s.i + 1, "s": g[s.s][2][j] }

g = [["y",1,[11]],["n",1,[13]],["-",3,[12,10,9]],["h",1,[2]],["u",1,[9]],["l",1,[8]],["b",1,[13]],["t",1,[3]],["i",2,[7,4]],["m",3,[13,0,null]],["w",1,[8]],["c",2,[12,13]],["o",2,[9,1]],["e",4,[11,2,2,5]]]

hash := crypto.sha256(parts[1])

correct_flag {
  g[s][0] == flag[0]
  f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f({ "i": 0, "s": s })))))))))))))))))))))))).i == count(flag)
Enter fullscreen mode Exit fullscreen mode

This one was tricky. I could have probably solved it with a Python script, but that would have been a bit too error prone for my liking.

What that mild clusterfuck of a script boils down to is that g is like a convoluted recursive linked list, with pointers to the next item in the list based on how many of each entry's letters are in the flag. If that sounded confusing, it was.

What led me to this realization was that exactly one element in the "pointer list" of an entry of g was null. What else has a null final pointer? A linked list. (Yes, there are circular linked list, but technicalities are for the other challenges.)

So here's the structure of this "linked" list:

  • The list is comprised of entries. In each entry:
    • A letter, which we can call the "data."
    • A number, which represents the number of times the data appears in the flag
    • A list, which is the list of pointers to other entries. If the data has appeared 0 times so far, use pointer at index 0. 1 time, index 1, and so on.

So we can work backwards from the final letter and figure out what would point to it, by taking the most recent pointer that points to its index, and marking down the letter associated with that pointer. It's messy, but it works.



Rego Prototype Review was a slightly unrealistic but fun challenge using a new programming language. I think if I hadn't learned logical programming in school before this CTF I would have been stuck for way longer, but it is an excellent introduction without the weirdness of Prolog.

Top comments (0)