From d9e3f1d2c1c322275cf9cd561b31b8fef8085dee Mon Sep 17 00:00:00 2001 From: Felix Kehrer Date: Tue, 24 Dec 2019 02:46:41 +0100 Subject: [PATCH] fkehrer: ctfzone19 --- writeups/fkehrer/ctfzone19.md | 47 +++++++++++++ writeups/fkehrer/ctfzone19/solve.py | 83 +++++++++++++++++++++++ writeups/fkehrer/ctfzone19/solve_bfs.py | 88 +++++++++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 writeups/fkehrer/ctfzone19.md create mode 100644 writeups/fkehrer/ctfzone19/solve.py create mode 100644 writeups/fkehrer/ctfzone19/solve_bfs.py diff --git a/writeups/fkehrer/ctfzone19.md b/writeups/fkehrer/ctfzone19.md new file mode 100644 index 0000000..b957945 --- /dev/null +++ b/writeups/fkehrer/ctfzone19.md @@ -0,0 +1,47 @@ +# CTFZone 2019 Quals + +## Fridge + +The service sends you some 0s and 1s, which are arranged as a board. You send your input, which consists of two numbers. The response is either the updated board or the phrase "this one is solved", followed by a new board. Presumably, there is an end to this game, but we didn't reach it. + +### Level 1 - 4x4 + +The first level seems to be the same every time. I did not record it, but going from memory, I think it looked like this: +``` +0 1 0 0 +0 1 0 0 +1 1 1 1 +0 1 0 0 +``` +After sending `2,1` (or maybe it was the other way around), the board would be solved. + +Now, I assumed this to be a chess- or reversi-like situation: We have a quadratic board, and can choose one position on it, prompting the flip of the chosen position and some others, based on some rules. In this level, this would be like putting a (chess) tower on the position 2,1 (with the first number being the downward and the second number the rightward direction), and flipping all positions it could reach with one move. This results in a board of just 0s, which is the "solved" state. + +I noticed two important properties about moves made in this and the next level: First of, applying a move twice has the same effect as not applying them, meaning any made move can be reversed by applying it again. And secondly, which positions get flipped do not depend on the board in any way, meaning also that moves can be applied in any order. + +### Level 2 - 4x4 + +Based on what we learned in level 1, we would assume this to be the same situation, with the added difficulty of having to make more moves, right? Wrong. For some reason, the moves do not follow the cross-shape of level 1 anymore, or any shape we could recognize. So we had to come up with something smarter. + +The solution for level 2 is always easy (only a few moves), but since we do not know which move does what, we can't just solve the board by reading in the start state. + +So I created a script, which tries every possible move once, records which fields are flipped, and once all moves have been recorded, reads the current state of the board and finds a combination of moves which solves it. +One glaring limitation of my implementation is that I just assume that while trying moves, the board will never be solved by accident. This actually happens occassionally, but it's not often enough to be a serious problem. + +This approach is actually a bit overkill, since we apply every move, and normally have to undo almost all of them in order to solve it. There are two reasons I decided to not apply every move twice though: First of, it would take twice as long to do all moves (the network is the limiting factor here), and more importantly, the chance of accidentally solving the board is much smaller (which would be bad, because I threw the script together in the last hours of the CTF, so I didn't want to handle the possibility). + +### Level 3 - 10x10(?) + +So far, things were looking good. The script I made solved both level 1 and 2 almost instantly, but level 3 is where things became problematic. Just trying the moves took noticable time, and solutions were either not found within the time I let it running, or at all. Time was running out, and waiting forever was just not an option anymore. + +The major change I made to the script was to switch the solution search from depth first to breadth first, since both level 1 and 2 had very short solutions, I assumed this would hold true for level 3 as well. This change turned out to be a pain, but I did eventually make it. However, tracking multiple attempts, my script found no solutions for any combination with less than five moves. Obviously, every stage of the search took a lot longer than the one before it, and at some point, I think it was a depth of 5 (checking all combinations of 5 moves), it ran for many minutes without finding anything, so I eventually stopped it. + +Level 3 and beyond stayed unsolved. + +Both the [DFS](ctfzone19/solve.py) and [BFS](ctfzone19/solve_bfs.py) versions are included. Note that both boards and moves are encoded as lists of True and False, where for boards this encodes the 0s and 1s of the board, and for boards it encodes whether a field is flipped or not. This allows me to easily apply any number of moves to a board by XORing their fields. + +### Closing thoughts + +I fear that the reason I found no solutions for level 3 (within the timeconstraints of "oh no, the CTF ends in less than an hour") was that one of the many found properties and made assumptions simply did not hold for level 3. Maybe the shape was not strictly quadratic anymore, maybe moves worked differently, maybe other symbols were introduced, the things I do not know about this service far outweigh the things I do know. There's also a very real chance of my script being buggy. + +Even with all these setbacks, this challenge was actually really entertaining to work on. My colleague Raphael, who was actually trying to study Maths next to me, was a great help by listening and contributing ideas, and without him, I would have probably failed to solve the challenge even harder. :) \ No newline at end of file diff --git a/writeups/fkehrer/ctfzone19/solve.py b/writeups/fkehrer/ctfzone19/solve.py new file mode 100644 index 0000000..3019f0b --- /dev/null +++ b/writeups/fkehrer/ctfzone19/solve.py @@ -0,0 +1,83 @@ +import pwnlib + +dimensions = [4,4,10] + +ZERO = False +ONE = True + +# check if board is all 0s +def winningboard(board): + for elem in board: + if elem == ONE: + return False + return True + +# xors two boards, result is a new board (originals unchanged) +def xorboard(boardA, boardB): + newBoard = [] + for i in range(len(boardA)): + newBoard.append(boardA[i] != boardB[i]) + return newBoard + +# read a board from pipe, return representation with True/False +def readboard(pipe, dimension): + move = [] + for _ in range(dimension): + line = pipe.recvline() + #print(line) + for field in line.split(b" "): + if field == b"0" or field == b"0\n": + move.append(ZERO) + elif field == b"1" or field == b"1\n": + move.append(ONE) + else: + print("assumption fail: ") + print(line) + print(field) + quit() + return move + +# search recursively for solutions +def recursive_search(currentBoard, moves, maxindex): + for i in range(maxindex): + board = xorboard(currentBoard, moves[i][0]) + if winningboard(board): + return [i] + result = recursive_search(board, moves, i) + if result != None: + return result + [i] + return None + + +fridge = pwnlib.tubes.remote.remote("ppc-fridge.ctfz.one", 31337) + +# do all this once per stage +for stage in range(len(dimensions)): + dim = dimensions[stage] + moves= [] + before = readboard(fridge, dim) + # does all possible moves once, records the changes they make + for x in range(dim): + for y in range(dim): + move_str = "{0},{1}".format(x,y) + fridge.sendline(move_str) + after = readboard(fridge, dim) + moves.append((xorboard(before, after),move_str,)) + before = after + # from the current board, find a solution + moves_list = recursive_search(before, moves, len(moves)) + if moves_list == None: + print("ERROR: No moves found") + quit() + print("BEFORE: ",before) + # apply moves, skipping the last one because it doesn't print a board + for i in moves_list[:-1]: + print(moves[i][1]) + fridge.sendline(moves[i][1]) + print(readboard(fridge, dim)) + # handle last move + print(moves[-1][1]) + fridge.sendline(moves[-1][1]) + print(fridge.recvline()) # this one is solved + +fridge.interactive() \ No newline at end of file diff --git a/writeups/fkehrer/ctfzone19/solve_bfs.py b/writeups/fkehrer/ctfzone19/solve_bfs.py new file mode 100644 index 0000000..40a6c14 --- /dev/null +++ b/writeups/fkehrer/ctfzone19/solve_bfs.py @@ -0,0 +1,88 @@ +import pwnlib + +dimensions = [4,4,10] + +ZERO = False +ONE = True + +# check if board is all 0s +def winningboard(board): + for elem in board: + if elem == ONE: + return False + return True + +# xors two boards, result is a new board (originals unchanged) +def xorboard(boardA, boardB): + newBoard = [] + for i in range(len(boardA)): + newBoard.append(boardA[i] != boardB[i]) + return newBoard + +# read a board from pipe, return representation with True/False +def readboard(pipe, dimension): + move = [] + for _ in range(dimension): + line = pipe.recvline() + #print(line) + for field in line.split(b" "): + if field == b"0" or field == b"0\n": + move.append(ZERO) + elif field == b"1" or field == b"1\n": + move.append(ONE) + else: + print("assumption fail: ") + print(line) + print(field) + quit() + return move + +def bfs(currentBoard, moves, maxindex, depth): + for i in range(maxindex): + board = xorboard(currentBoard, moves[i][0]) + if depth == 1: + if winningboard(board): + return [i] + else: + result = bfs(xorboard(currentBoard, moves[i][0]), moves, i, depth - 1) + if result != None: + return result + [i] + return None + + + +fridge = pwnlib.tubes.remote.remote("ppc-fridge.ctfz.one", 31337) + +# do all this once per stage +for stage in range(len(dimensions)): + dim = dimensions[stage] + moves= [] + before = readboard(fridge, dim) + # does all possible moves once, records the changes they make + for x in range(dim): + for y in range(dim): + move_str = "{0},{1}".format(x,y) + fridge.sendline(move_str) + after = readboard(fridge, dim) + moves.append((xorboard(before, after),move_str,)) + before = after + # from the current board, find a solution + for depth in range(1, len(moves)): + print("SEARCH DEPTH: ", depth) + moves_list = bfs(before, moves, len(moves), depth) + if moves_list != None: + break + if moves_list == None: + print("NO MOVES FOUND") + print("BEFORE: ",before) + # apply moves, skipping the last one because it doesn't print a board + for i in moves_list[:-1]: + print(moves[i][1]) + fridge.sendline(moves[i][1]) + print(readboard(fridge, dim)) + # handle last move + print(moves[-1][1]) + fridge.sendline(moves[-1][1]) + print(fridge.recvline()) # this one is solved + +fridge.interactive() \ No newline at end of file -- 2.43.0