# 1337UP LIVE CTF 2023
## Smiley Maze
> Our friend trapped us inside this maze! can you help us get out of this hell of a place safely? ?> > Author: Et3rnos, 0xM4hm0ud> > Put the flag inside `INTIGRITI{}` and the format of the flag is hex.> > [`smileymaze.zip`](https://raw.githubusercontent.com/D13David/ctf-writeups/main/1337uplive/game/smiley_maze/smileymaze.zip)
Tags: _game_
## SolutionFor this challenge we get a zip archive containing a `dockersetup` for a simple maze game. I didn't run the game at all and just statically reversed it. With the deployment we find the executable. After short inspection with `Ghidra` it became clear that the executable in fact is a `pyinstaller` binary. There are some [`pyinstaller extractor scripts`](https://github.com/extremecoders-re/pyinstxtractor) which can be used to get the python resources.
After extraction one file especially seems interesting `main.pyc`. Since it is compiled python bytecode we can try to decompile it with `pycdc`, but sadly the decompiler stumbled across various unsupported opcodes. So the is to just disassemble the file and try to reverse it from there.
The first two functions are very short
```python# end0       RESUME                        02       LOAD_GLOBAL                   1: NULL + print14      LOAD_CONST                    1: 'Nice Job, You reached the end!'16      PRECALL                       120      CALL                          130      POP_TOP                       32      LOAD_CONST                    0: None34      RETURN_VALUE                  
# checkfinish0       RESUME                        02       LOAD_GLOBAL                   0: j14      LOAD_CONST                    1: 12516      COMPARE_OP                    2 (==)22      POP_JUMP_FORWARD_IF_FALSE     13 (to 50)24      LOAD_GLOBAL                   2: i36      LOAD_CONST                    2: 138      COMPARE_OP                    2 (==)44      POP_JUMP_FORWARD_IF_FALSE     4 (to 54)46      LOAD_CONST                    3: True48      RETURN_VALUE                  50      LOAD_CONST                    0: None52      RETURN_VALUE                  54      LOAD_CONST                    0: None56      RETURN_VALUE                  ```
The functions can be easily back translated to python and look something like this. The first function just prints a message, the second function returns true if variables `i` and `j` have a specific value. Both variables are global and, as it later turns out, are used to represent the players position.
```pythondef end():    print("Nice Job, You reached the end!")
def checkfinish():    if j == 125 and i == 1:        return True```
Next comes the functions `print_board` and `on_press` which we skip for now, since we first want to make sense of the datastructure which is used to represent the maze. The loading code is in global scope and looks like this:
```python0       RESUME                        02       LOAD_CONST                    0: 04       LOAD_CONST                    1: ('keyboard',)6       IMPORT_NAME                   0: pynput8       IMPORT_FROM                   1: keyboard10      STORE_NAME                    1: keyboard12      POP_TOP                       14      LOAD_CONST                    0: 016      LOAD_CONST                    2: None18      IMPORT_NAME                   2: os20      STORE_NAME                    2: os22      LOAD_CONST                    3: '...'24      STORE_NAME                    3: m_string           m_string = '...'26      LOAD_CONST                    4: '...'28      STORE_NAME                    4: chars              chars = '...'30      LOAD_CONST                    5: 20132      STORE_NAME                    5: size               size = 20134      BUILD_LIST                    036      STORE_NAME                    6: m                  m = []38      PUSH_NULL                     40      LOAD_NAME                     7: range42      LOAD_CONST                    0: 044      LOAD_NAME                     5: size46      LOAD_CONST                    6: 248      BINARY_OP                     8 (**)52      PRECALL                       256      CALL                          266      GET_ITER                      68      FOR_ITER                      108 (to 286)          for i in range(0, size**2)70      STORE_GLOBAL                  8: i72      LOAD_GLOBAL                   16: i84      LOAD_NAME                     5: size86      BINARY_OP                     6 (%)90      LOAD_CONST                    0: 092      COMPARE_OP                    2 (==)98      POP_JUMP_FORWARD_IF_FALSE     21 (to 142)               if i % size == 0
#  new empty row100     LOAD_NAME                     6: m102     LOAD_METHOD                   9: append124     BUILD_LIST                    0126     PRECALL                       1130     CALL                          1                             m.append([])140     POP_TOP                       
# get last row and fill142     LOAD_NAME                     6: m144     LOAD_CONST                    7: -1146     BINARY_SUBSCR                                           row = m[-1]156     LOAD_METHOD                   9: append178     PUSH_NULL                     180     LOAD_NAME                     10: ord182     LOAD_NAME                     3: m_string184     LOAD_GLOBAL                   16: i196     LOAD_CONST                    8: 8198     BINARY_OP                     2 (//)202     BINARY_SUBSCR                                           m_string[i // 8]212     PRECALL                       1216     CALL                          1                         ord(m_string[i // 8])226     LOAD_CONST                    9: 1228     LOAD_CONST                    10: 7230     LOAD_GLOBAL                   16: i242     LOAD_CONST                    8: 8244     BINARY_OP                     6 (%)                     i % 8248     BINARY_OP                     10 (-)                    (i % 8) - 7252     BINARY_OP                     3 (<<)                    ((i % 8) - 7) << 1256     BINARY_OP                     1 (&)                     cell = ord(m_string[i // 8]) & ((i % 8) - 7) << 1260     POP_JUMP_FORWARD_IF_FALSE     2 (to 266)                if cell == 1
#  cell was 1262     LOAD_CONST                    9: 1                          cell_value = 1264     JUMP_FORWARD                  1 (to 268)
# cell was 0266     LOAD_CONST                    0: 0                          cell_value = 0 268     PRECALL                       1272     CALL                          1                         row.append(cell_value)282     POP_TOP                       284     JUMP_BACKWARD                 109```
Putthing the annotations together we get the following. The variables `m_string` and `chars` are initialized with some huge strings, also `size` is set to 201. Then an array `m` is created and filled with zero or one depending on a value computation with values from `m_string`. The array `m` describes then the full maze: rows and columns build a 2d grid and at each cell either 1 or 0 specifies if there is a wall or not.
```pythonm_string = "..."chars = "..."size = 201
m = []
for i in range(0, size**2)    if i % size == 0:        m.append([])    row = m[-1]    cell = ord(m_string[i//8]) & (1 << (((i % 8) - 7)))    if cell == 0:        row.append(0)    else:        row.append(1)```
After this the player position is set to `59, 199` and then reset to `127, 1`. The board is drawn and the keyboard listener is set to function `on_press`. Inbetween some functions are loaded and created. But since this doesn't really matters we skip this here.
```pythonposition = (59, 199)j = position[0]i = position[1]
j = 127i = 1 
print_board()keyboard.Listener(on_press)listener.join()```
Now we know how the data is loaded we can check out what happens when the board is drawn. First the screen is cleared. Then a nested loop is done over all the region surrounding the current player position, so that the player position is centered to this region. The player sprite is drawn at `0, 0` and a flag symbol is drawn if the visible region contains cell `1, 125`. This basically draws only a small visible region around where the player is located.
```python# clear screen0       RESUME                        02       LOAD_GLOBAL                   1: NULL + os14      LOAD_ATTR                     1: system24      LOAD_GLOBAL                   0: os36      LOAD_ATTR                     2: name46      LOAD_CONST                    1: 'nt'48      COMPARE_OP                    2 (==)54      POP_JUMP_FORWARD_IF_FALSE     2 (to 60)56      LOAD_CONST                    2: 'cls'58      JUMP_FORWARD                  1 (to 62)60      LOAD_CONST                    3: 'clear'62      PRECALL                       166      CALL                          176      POP_TOP                       
# start drawing map elements78      LOAD_GLOBAL                   7: NULL + range90      LOAD_CONST                    4: -292      LOAD_CONST                    5: 394      PRECALL                       298      CALL                          2108     GET_ITER                      110     FOR_ITER                      307 (to 728)          for y in range(-2, 3)114     STORE_FAST                    0: y116     LOAD_GLOBAL                   7: NULL + range128     LOAD_CONST                    4: -2130     LOAD_CONST                    5: 3132     PRECALL                       2136     CALL                          2146     GET_ITER                      148     FOR_ITER                      271 (to 694)          for x in range(-2, 3)152     STORE_FAST                    1: x154     LOAD_FAST                     1: x156     LOAD_CONST                    6: 0158     COMPARE_OP                    2 (==)
# draw player164     POP_JUMP_FORWARD_IF_FALSE     24 (to 214)               if x == 0:166     LOAD_FAST                     0: y168     LOAD_CONST                    6: 0170     COMPARE_OP                    2 (==)176     POP_JUMP_FORWARD_IF_FALSE     18 (to 214)               if y == 0:178     LOAD_GLOBAL                   9: NULL + print190     LOAD_CONST                    7: '?'192     LOAD_CONST                    8: ''194     KW_NAMES                      9196     PRECALL                       2200     CALL                          2210     POP_TOP                       212     JUMP_BACKWARD                 33
# draw flag214     LOAD_GLOBAL                   10: j226     LOAD_FAST                     0: y228     BINARY_OP                     0 (+)232     LOAD_CONST                    10: 125234     COMPARE_OP                    2 (==)240     POP_JUMP_FORWARD_IF_FALSE     32 (to 306)               if y+j == 125242     LOAD_GLOBAL                   12: i254     LOAD_FAST                     1: x256     BINARY_OP                     0 (+)260     LOAD_CONST                    11: 1262     COMPARE_OP                    2 (==)268     POP_JUMP_FORWARD_IF_FALSE     18 (to 306)               if x+i == 1270     LOAD_GLOBAL                   9: NULL + print282     LOAD_CONST                    12: '?'284     LOAD_CONST                    8: ''286     KW_NAMES                      9288     PRECALL                       2292     CALL                          2302     POP_TOP                       304     JUMP_BACKWARD                 79```
Next the map values for the current cell are loaded and either a wall sprite is printed or, in empty space, some hex-values coming from `chars`.
```python306     LOAD_GLOBAL                   9: NULL + print318     LOAD_GLOBAL                   14: m330     LOAD_GLOBAL                   10: j342     LOAD_FAST                     0: y344     BINARY_OP                     0 (+)                     j+y348     LOAD_GLOBAL                   17: NULL + len360     LOAD_GLOBAL                   14: m372     PRECALL                       1376     CALL                          1                         len(m)386     BINARY_OP                     6 (%)                     (j+y) % len(m)390     BINARY_SUBSCR                                           m[(j+y)%len(m)]
400     LOAD_GLOBAL                   12: i412     LOAD_FAST                     1: x414     BINARY_OP                     0 (+)                     i+x418     LOAD_GLOBAL                   17: NULL + len430     LOAD_GLOBAL                   14: m442     LOAD_CONST                    6: 0444     BINARY_SUBSCR                                           m[0]454     PRECALL                       1458     CALL                          1                         len(m[0])468     BINARY_OP                     6 (%)                     (i+x) % len(m[0])472     BINARY_SUBSCR                                           cell = m[(j+y)%len(m)][(i+x) % len(m[0])]482     POP_JUMP_FORWARD_IF_FALSE     2 (to 488)                if cell == 1484     LOAD_CONST                    13: '██'                      load wall486     JUMP_FORWARD                  91 (to 670)
488     LOAD_GLOBAL                   18: chars                 if cell == 0500     LOAD_GLOBAL                   10: j512     LOAD_FAST                     0: y514     BINARY_OP                     0 (+)                     j+y518     LOAD_GLOBAL                   17: NULL + len530     LOAD_GLOBAL                   14: m542     PRECALL                       1546     CALL                          1                         len(m)556     BINARY_OP                     6 (%)                     (j+y)%len(m)560     LOAD_GLOBAL                   20: size572     BINARY_OP                     5 (*)                     ((j+y)%len(m))*size576     LOAD_GLOBAL                   12: i588     LOAD_FAST                     1: x590     BINARY_OP                     0 (+)                     i+x594     LOAD_GLOBAL                   17: NULL + len606     LOAD_GLOBAL                   14: m618     LOAD_CONST                    6: 0620     BINARY_SUBSCR                                           m[0]630     PRECALL                       1634     CALL                          1                         len(m[0])644     BINARY_OP                     6 (%)                     (i+x)%len(m[0])648     BINARY_OP                     0 (+)                     ((j+y)%len(m))*size + ((i+x)%len(m[0]))652     BINARY_SUBSCR                                           chars[((j+y)%len(m))*size + ((i+x)%len(m[0]))]662     FORMAT_VALUE                  0664     LOAD_CONST                    14: 2666     BINARY_OP                     5 (*)
# draw cell sprite670     LOAD_CONST                    8: ''672     KW_NAMES                      9674     PRECALL                       2678     CALL                          2688     POP_TOP                       690     JUMP_BACKWARD                 273694     LOAD_GLOBAL                   9: NULL + print706     LOAD_CONST                    8: ''708     PRECALL                       1712     CALL                          1722     POP_TOP                       724     JUMP_BACKWARD                 309728     LOAD_CONST                    0: None730     RETURN_VALUE                  ```
This whole part basically translates to
```pythondef print_board():    for y in range(-2, 3):        for x in range(-2, 3):            if x == 0 and y == 0: print("?", end="")             if y+j == 125 and x+i == 1: print("?", end="")             cell = m[(j+y)%len(m)][(i+x) % len(m[0])]            if cell == 1:                print("██", end = "")            else:                value = chars[((j+y)%len(m))*size + ((i+x)%len(m[0]))]                print(value*2, end = "")```
But we want to see the whole maze, so we adapt this and draw the whole world in one go. As a result we get this beatiful result.
![](https://raw.githubusercontent.com/D13David/ctf-writeups/main/1337uplive/game/smiley_maze/maze.png)
And zoomed in a bit, we can see the empty space is filled with numbers.
![](https://raw.githubusercontent.com/D13David/ctf-writeups/main/1337uplive/game/smiley_maze/maze1.png)
For completeness here the `on_press` handler that handles player movement and wall collision. For each move it checks if the player hit the flag field and if so the screen is cleared and the message `Nice Job, You reached the end!` is printed.
```pythondef on_press(key):    if key == Key.up:        j -= abs(m[j-1][i] - 1)        check = checkfinish()    elif key == Key.down:        j += abs(m[j+1][i] - 1)        check = checkfinish()    elif key == Key.left:        i -= abs(m[j][i-1] - 1)        check = checkfinish()    elif key == Key.right:        i += abs(m[j][i+1] - 1)        check = checkfinish()    elif key == Key.space:        listener.stop()     print_board()     if check:        listener.stop()        if os.name == 'nt':            system('clear')        else:            system('cls')        end()```
Now we know how the game works, we need to find the flag. But where *is* the flag? The hint in the description tells us `Put the flag inside `INTIGRITI{}` and the format of the flag is hex.`, and we have a lot of hex numbers in the maze. But which number are the correct ones?
I did a lot of trial and error, taking the numbers starting at the flag field for instance. But nothing really worked. Another weird thing was, the player spawn position was pretty much right next to the flag field? But what if, the player spawn position override was not really the correct position, but the position which was set *before*? In this case the player would have to go some way until reaching the flag.
Then I had the idea, maybe the player would need to find the shortest way to the flag and collect numbers on his way? This can be done with a `BFS` path scanning.
```pythondef dfs(vis, x, y, stack):    if y*size+x > len(chars) or vis[y*size+x] == True:        return
    stack.append((x,y))    if checkfinish(x,y):        for i in range(len(stack)-1):            print(chars[stack[i][1]*size+stack[i][0]], end = "")        print("")        #print(stack[-1])i        return
    vis[y*size+x] = True
    if m[y-1][x] == 0: dfs(vis, x, y-1, stack)    if m[y+1][x] == 0: dfs(vis, x, y+1, stack)    if m[y][x-1] == 0: dfs(vis, x-1, y, stack)    if m[y][x+1] == 0: dfs(vis, x+1, y, stack)
    del stack[-1]
stack = []vis = [0 for i in range(len(m)*len(m[0]))]dfs(vis, 199, 59, stack)```
This didn't give the correct flag either, so I tried multiple variations (collecting the duplicated numbers vs only single numbers) etc. In the end, it turned out, that the flag path just didn't contain the *start* and *end* position. Removing the values gave the flag.
Flag `INTIGRITI{d4842a6e6519c3838d3387400e6237db2a4d2db5ceb8740d8ce094f22527c25f33615746336af6d897f9d7491ff782cf9e71de332b49b771b7eae33ecce7c8fa26fe722367066d333ac4f30f699b0c103dfd8eedac6bfe504834301da7ea48e63e2b2afa74aa8279bd2df1fd101cd83de6a0882639df1b6be4eff18bb51670fe447a9bbbed22a717733e4e60b6cfaa7b4c2c6a3a0b81a89639d09645959ddfcd30c970436f6137accc3243586581b5c946825202b7e1192}`