--- tags: writeup, hardware, z3, googlectf2021 --- # **Write-up Parking (Google CTF 2021)** > [name=User1] > [time=Thu, Jul 22, 2021 7:37 PM] ![](https://i.imgur.com/JoucNBT.jpg) > [color=#1cefda] Вы выиграли @@@@@@@@@@@@[@ch4nnel1](https://t.me/ch4nnel1). Original write-up: https://hackmd.io/@osogi/parking_writeup Russian version: https://hackmd.io/@osogi/parking_writeup_ru ## История + разбор ![](https://i.imgur.com/esugZLZ.png) <p style="text-align: center;">Сам таск (https://capturetheflag.withgoogle.com/challenges/hw-parking или https://github.com/google/google-ctf/tree/master/2021/quals/hw-parking)</p> ### Пролог Качаем зип архив, открываем, в нем 4 файла ![](https://i.imgur.com/9XGq4Zc.png) Про `run.sh` забудем почти сразу, так как все что он делает, это запускает игру вместе с первым уровнем и говорит ей, что сейчас не надо давать флаг + просто запускает второй уровень. (Надеюсь гугл не забанит меня за то, что я исползую код из их таска. Там просто была какая-то лицензия всё такое, а я ленивый не прочитал). ```bash=1 echo "Welcome to our game. You need to enter the red vehicle into a crowded parking lot." echo "There are two levels: the first is introductory, just so you know the rules; the second, when solved, will reveal the flag. Good luck!" FLAG=0 python3 game.py level1 echo "Good job! Now that you know the rules, let's move to the real level. Note that it is quite large, so allow some loading time." python3 game.py level2 ``` Ну и само собой, запустим прогу, для начала с первым уровнем, увидим что-то такое ![](https://i.imgur.com/VbMdLHx.png) Из текста, что вещает нам игра понимаем, что нужно сдвинуть красную машинку + что конечное положение зеленой машинки (как окажется позже машинок) кодирует флаг. Потратив пять минут, с трудом, но решаем первый уровень (подсказки как его решить я не дам). Видим следующее, мы получили флаг. ![](https://i.imgur.com/MkHhcBC.png) ~~Окей гугл, я ожидал от тебя чего-то большего. Я конечно знаю, что гений, но решить таск за 5 минут.~~ Ааа, флаг не подходит. ![](https://i.imgur.com/qKBYXL0.png) ### Завязка (идея решения) Окей, меня внаглую обманули и подсунули неверный флаг. Давайте попробуем открыть второй уровень. Чет он долговато грузиться... ![](https://i.imgur.com/YrMtxzi.png) ~~Твою же мать, можно я выйду в окно? После этого автора райтапа никто не видел~~ Ладно, может там не все так плохо, и рядом с красной машинкой уже есть пустая клеточка? (После того как не смог найти красную машинку) Таким бы вопросом задался каждый нормальный человек, но я мега реверсер сразу полез в исходники игры. Тут могла бы быть часть про то, что я тупил и пытаясь менять координаты одной зеленой машины получить флаг. Но это не будет относиться к текущему решению, так что просто упомяну это. Чуть проанализировав код + потыкав файлы уровней. ![](https://i.imgur.com/IUUULon.png) Примерно поймем структуру последних: первая строка размер поля, дальше идут объекты в формате `x, y, ширина, высота, тип` (если тип: 0 - стена; 1 - обычная, серая машина; -1 - зеленая машина; -2 - красная машина). Игра сама написана на питоне и в ней вполне все понятно, но отмечу некоторые полезные штуки, которые будут использоваться при дальнейшем решении. ```python board #словарь сопоставляющий: (x, y) -> объект находящийся на этой позиции blocks #список сопоставляющий "номер", индекс машины с её x,y,ширина,высота,тип target #переменная содержащая номер красной машины def redraw(): #функция отвечающая за перерисовку поля + при первом прогоне сохраняет результат начальной позиции в файл def printflag(): #функция печатающая флаг, вывод зависит от позиции зеленых машинок (если они меняли позицию, то бит флага соответствующий этой машинке будет 1, иначе 0) ``` Зная как устроена структура файла уровня, найдем на рисунке красную и зеленные машинки ![](https://i.imgur.com/Nwpzh7n.png) <p style="text-align: center;">В зеленных секциях (всего их 64) находятся зеленные машинки, туда куда указывает красная стрелочка (правый нижний угол) красная машина</p> Приблизим чтобы рассмотреть это все поближе ![](https://i.imgur.com/hFaKgaE.png) ![](https://i.imgur.com/J6TFqN1.png) ![](https://i.imgur.com/QPgNgZI.png) Окей, знаете что? Это все напоминает логические схемы (+ hw как-никак). Из видео liveoverflow помнил, что их можно решать через z3 (мега решалка уравнений, неравенств и тд), но как точно не знал, так что решил пересмотреть его. {%youtube nI8Q1bqT8QU %} > https://www.youtube.com/watch?v=nI8Q1bqT8QU P.S. В результате им даже не воспользовался, но видео все-равно прикольное, полезное. Кстати оно тоже с hardware Google ctf, только 2019. #### Мини-задача 1 Давайте рассмотрим простейшие задачки с машинками и поймем наши действия. ![](https://i.imgur.com/8ExCG8a.png) Нам нужно подвинуть красную машинку, для этого мы: 1) определяем ее возможные направления движения (горизонтально/вертикально). 2) куда она может продвинуться в теории, то есть где ей мешает двигаться не стена. 3) если туда куда она хочет подвинуться есть, машина повторить все для нее. Используя такой алгоритм, мы сможем решить эту задачку. ![](https://i.imgur.com/FtAdqho.png) #### Мини-задача 2 Давайте рассмотрим задачу посложнее ![](https://i.imgur.com/C0pd3RB.png) Для нее нам придется чуть доработать наш алгоритм, ведь если мы просто подвинем оранжевую машинку вправо (как и должно быть по прошлому алгоритму), то она не освободит путь. Следовательно добавим в наш алгоритм 1 шаг 1) определяем ее возможные направления движения (горизонтально/вертикально). 2) куда она может продвинуться в теории, то есть где ей мешает двигаться не стена. 3) **посмотреть что мешает продвинуться в "возможную позицию".** 4) **повторить для мешающих машин, передав им позицию которую нужно освободить.** ![](https://i.imgur.com/SrGq1h9.png) #### Мини-задача 3 И рассмотрим последний пример, в котором обговорим одну тонкость алгоритма. ![](https://i.imgur.com/T1LuWX1.png) В 4 шаге мы должны сделать логическое сложение ('или', 'дизъюнкция', '|') для всех результатов возможных позиций (в этом таске будет только 1-2). И логическое умножение ('и', 'конъюнкция', '&') внутри самих результатов. > Легче объяснить на примере. Допустим в мини-задаче 2, чтобы подвинуть красную машинку нам в результате надо подвинуть синюю **И** фиолетовую машинку. Тут же (мини-задача 3), чтобы подвинуть красную машинку нам нужно подвинуть зеленую **ИЛИ** голубую. ![](https://i.imgur.com/jsToXce.png) Уххх, осталось только закодить это и скормить z3. ### Кульминация (кодинг) > В этой части опущены большинство моих косяков, так как их было слишком много. #### some_foo() Для начала сделаю функцию `some_foo()`, и помещу ее вызов перед рисованием изначального поля. ```python def some_foo(): print("Yep i'm work") print(blocks[target]) ``` Бум, оно работает. ![](https://i.imgur.com/0Th8v7A.png) #### get_obj(x, y) Добавим: - функцию `get_obj(x, y)`, которая будет возвращать, что находится по позиции `x, y` - функцию определяющую (`isHorizintal(which)`), как может двигаться машинка (горизонтально/вертикально) - список `important`, который будет содержать индексы зеленых машинок. И кусочек кода, который будет его заполнять ```python SPACE=-2 important = [] def get_obj(x, y): if (x,y) in board: return board[(x, y)] else: return SPACE def isHorizintal(which): #horiz==x vertical==y h=blocks[which][3] if(h == 1): return True else: return False def some_foo(): print("Yep i'm work") for i in important: print(isHorizintal(i)) print(getObj(3, 5)) [...] walls = [] for line in boardfile.splitlines(): if not line.strip(): continue x,y,w,h,movable = [int(x) for x in line.split()] if movable == -1: important.append(len(blocks)) #add flagblocks[len(blocks)] = (x,y) [...] ``` Выглядит рабочим ![](https://i.imgur.com/vlibuvy.png) #### getMoveDir > Тут было потрачено много попыток добавить/изменить функцию `getMoveDir()`, но сделаем вид, что она была правильно написана с первого раза Давайте добавим чуть спагети кода (а окей, это не спагети код, но все-равно плохой) и функцию, которая "протестит" наш алгоритм - `getMoveDir(which, free_x, free_y)` - рекурсивная функция, определяющая куда может двинуться машина - `MvDict` - словарь содержащий "индекс машины" -> направление - `MyTestBlocks` - список содержащий номера "спарщенных" машин - `putparsecar(x,y,w,h,m)` - функция закрасывающая машины из`MyTestBlocks` в синий цвет, сделана чтобы понимать, где ломается `getMoveDir()` - еще также было добавлено `sys.setrecursionlimit(150000)`, чтобы питон не ломался при анализе второго уровня ```python import sys sys.setrecursionlimit(150000) MvDict={} MyTestBlocks=[] def getMoveDir(which, free_x, free_y): x,y,w,h,mv=blocks[which] res="" if(isHorizintal(which)): check=1 buf=[] for i in range(free_x+1, free_x+w+1): obj=getObj(i, y) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE): buf.append([obj, i, y]) if(check): res+="Right" for l in buf: getMoveDir(l[0], l[1], l[2]) check=1 buf=[] for i in range(free_x-w, free_x): obj=getObj(i, y) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE) : buf.append([obj, i, y]) if(check): res+="Left" for l in buf: getMoveDir(l[0], l[1], l[2]) else: check=1 buf=[] for i in range(free_y+1, free_y+h+1): obj=getObj(x, i) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE) : buf.append([obj, x, i]) if(check): res+="Down" for l in buf: getMoveDir(l[0], l[1], l[2]) check=1 buf=[] for i in range(free_y-h, free_y): obj=getObj(x, i) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE) : buf.append([obj, x, i]) if(check): res+="Up" for l in buf: getMoveDir(l[0], l[1], l[2]) print(f"{which}: {blocks[which]}| {free_x}, {free_y} |{res}") return res def putparsecar(x,y,w,h,m): x0 = x*SZ x1 = (x+w)*SZ y0 = y*SZ y1 = (y+h)*SZ color = (0, 0, 128) draw.rectangle((x0+1,y0+1,x1-2,y1-2), fill=color) def some_foo(): print("Yep i'm work") x,y,w,h,mv=blocks[target] print(getMoveDir(target, x+w-1, y)) def redraw(): [...] for block in blocks: putcar(*block) for block in MyTestBlocks: #add putparsecar(*block) [...] ``` Проверка на 1 уровне: ![](https://i.imgur.com/07RhMHm.png) Проверка на 2 уровне (пришлось перенести в файл, для более удобного анализа), дошел до всех (64) зеленым машин - успех: ![](https://i.imgur.com/DIJCY5l.png) #### getBranch > Тут тоже было потрачено много попыток для нормальной работы функции `getBranch()`, я же упояну только одну из них. Так теперь нужно переписать функцию `getMoveDir()` в `getBranch()`, изменив ее функционал на то, чтоб получить при каких условиях (относительно зеленых машин) машина может освободить заданые координаты. Для этого было добавлено: - `inputs` - список с переменными (связанными с зелеными машинками) для z3 - `inp_dict` - словарь, который связывает "индекс зеленой машинки" -> соответствующей ей `inputs[i]` - `BranchDict` - замена `MvDict` + была бага. Чтобы устранять бесконечные рекурсии, я запретил находить новые направления для машинок, кооторые есть в `MvDict` -> (см рисунок) когда фиолетовая машинка говорила синей сдвинуться, она "блокировала" к ней доступ. И желтая машинка считала, что синяя==стена. И как следствие `BranchDict` сопоставляет не просто `which`, а `f"{which}_{free_x}_{free_y}"` с результатом `getBranch(which, free_x, free_y)` ![](https://i.imgur.com/XjINgsA.png) - функция `get_green_by_head()` и костыль `GREENHEAD` - им помечается квадрат выше зеленной машинки, чтобы понять, что машинка сможет сдвинуться в него, только если зеленая не будет двигаться - функция `parseBranch` применяется к ветке и если там есть несколько условий, соеденяет их с помощью AND ```python from z3 import * s=Solver() c_OR="Or(" c_NOT="Not(" c_AND="And(" GREENHEAD=-3 BranchDict={}#MvDict={} inp_dict={} inputs=[] def get_green_by_head(x, y): return getObj(x, y+1) def parseBranch(branch): if(len(branch)>0): if(len(branch)==1): res=branch[0] else: res=c_AND for s in branch: res+=s+", " res=res[0:-2]+")" else: res="" return res def getBranch(which, free_x, free_y): res="" #GREENHEAD if(which==GREENHEAD): res= c_NOT+getBranch( get_green_by_head(free_x,free_y),free_x,free_y)+")" else: x,y,w,h,mv=blocks[which] #Gree car if(mv==-1): if(inp_dict.get(which)==None): inputs.append(Bool('inputs[%s]' % len(inputs))) inp_dict.update({which: f"inputs[{len(inputs)-1}]"}) res = inp_dict[which] #common car else: dictbuf=f"{which}_{free_x}_{free_y}" if(BranchDict.get(dictbuf)!=None): res=BranchDict.get(dictbuf) if(res==""): res="False" else: branches=[[], []] BranchDict.update({dictbuf:res}) if(isHorizintal(which)): check=1 buf=[] for i in range(free_x+1, free_x+w+1): obj=getObj(i, y) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE): buf.append([obj, i, y]) if(check): for l in buf: branches[0].append(getBranch(l[0], l[1], l[2])) if(len(buf)==0): branches[0].append("True") check=1 buf=[] for i in range(free_x-w, free_x): obj=getObj(i, y) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE) : buf.append([obj, i, y]) if(check): for l in buf: branches[1].append(getBranch(l[0], l[1], l[2])) if(len(buf)==0): branches[1].append("True") else: check=1 buf=[] for i in range(free_y+1, free_y+h+1): obj=getObj(x, i) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE) : buf.append([obj, x, i]) if(check): for l in buf: branches[0].append(getBranch(l[0], l[1], l[2])) if(len(buf)==0): branches[0].append("True") check=1 buf=[] for i in range(free_y-h, free_y): obj=getObj(x, i) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE) : buf.append([obj, x, i]) if(check): for l in buf: branches[1].append(getBranch(l[0], l[1], l[2])) if(len(buf)==0): branches[1].append("True") print(f"{which}: {blocks[which]}| {branches[0]}, {branches[1]}") if((len(branches[0])+len(branches[1]))==0): res="False" elif (len(branches[0])>0 and len(branches[1])>0): res+=c_OR res+=parseBranch(branches[0])+"," res+=parseBranch(branches[1])+")" else: for i in range(2): res+=parseBranch(branches[i]) MyTestBlocks.append(blocks[which]) BranchDict.update({dictbuf:res}) return res def some_foo(): print("Yep i'm work") x,y,w,h,mv=blocks[target] #print(getMoveDir(target, x+w-1, y)) qua=getBranch(target, x+w-1, y) s.add(eval(qua)) print(s.check()) print("Bits of flag:") flag="" for ind in important: var=eval(inp_dict[ind]) bit=str(int(s.model()[var].__bool__())) print(bit, end="") print() walls = [] for line in boardfile.splitlines(): [...] if movable==-1: #add board[(x,y-1)] = GREENHEAD if movable: blocks.append([x,y,w,h, movable]) [...] ``` Юху, оно смогло решить первый уровень: ![](https://i.imgur.com/8B7YvGl.png) А вот со вторым, происходят некоторые проблемы (конечно если сначала Ваш комп не сдох от кол-ва мусора, которого выкидывает скрипт): ![](https://i.imgur.com/2bP4VDA.png) #### z3.simplify() Хорошо, возможно это стоило сделать сразу, но я додумался только сейчас, это добавить simplify(). Что бы сразу разбираться со всем. Так же были добавлены два условия на `True`, `False`, так как иначе оно ломалось. ```python def getBranch(which, free_x, free_y): [...] if(res!="True") and (res!="False"): return str(simplify(eval(res))) return res ``` **...** Я просто промолчу, и отмечу, что теперь прога падает из-за того, что eval обрезает длиные строчки (~10k символов) с помощью `...` и z3 пытаясь распарсить ломается. ![](https://i.imgur.com/pYIFzrT.png) #### main_dots В результате было решено завести такую штуку как список контрольных точек `main_dots`, и записывать туда z3 переменные, которые бы равнялись формулам, длина которых больше 1000. ```python def getBranch(which, free_x, free_y): [...] if(len(res)>1000): ln=len(main_dots) main_dots.append(Bool('main_dots[%s]' % ln)) s.add(eval(f'main_dots[{ln}]=={res}')) res=f'main_dots[{ln}]' MyTestBlocks.append(blocks[which]) BranchDict.update({dictbuf:res}) if(res!="True") and (res!="False"): return str(simplify(eval(res))) return res ``` Запускаем его, ждем и получаем нужные битики (`0100110000101010000101101010011001000010010011101000110010110110`). ![](https://i.imgur.com/A0ewZ9M.png) ### Развязка (получение флага) Заходим на [cyberchef](https://gchq.github.io/CyberChef/). Пихаем битики... ![](https://i.imgur.com/yot5z5u.png) И не получаем флаг. ~~Все, закончили упражнение.~~ Начинаем реверсить ![](https://i.imgur.com/nw1FWLN.png) Ооо, понятные буковки, только бесмысленные, но ладно попробуем сдать CTF{m1rBehT2}. ![](https://i.imgur.com/JXAzGvo.png) Не сдается( Попробуем реверсить дальше ![](https://i.imgur.com/D7dnzoP.png) Угу, звучит и выглядит круто, сдаем CTF{2TheBr1m} ![](https://i.imgur.com/1efPfPY.png) ~~Easy game, easy life~~ Easy flag, hard CTF ## Решение - понять, что никак нельзя схалтурить и что нужно решить level2 - понять, что level2 похож на логическую схему -> можно засунуть в z3 - преобразовать level2 в логическую схему - засунуть её в z3 - получить биты флага -> получить флаг ## Скрипт-решение Использование: `python3 solver.py level2` и пойти попить чаек (ждать около 10-20 минут или больше). ![](https://i.imgur.com/zG5HEzW.png) ```python=1 from z3 import * import sys sys.setrecursionlimit(150000) WALL = -1 SPACE=-2 GREENHEAD=-3 blocks = [] board = {} s=Solver() c_OR="Or(" c_NOT="Not(" c_AND="And(" BranchDict={} inp_dict={} inputs=[] MyTestBlocks=[] main_dots=[] important = [] def Myprintflag(bits): flag = b"CTF{" while bits: byte, bits = bits[:8], bits[8:] flag += bytes([ int(byte[::-1], 2) ]) flag += b"}" print(flag) def getObj(x, y): if (x,y) in board: return board[(x, y)] else: return SPACE def isHorizintal(which): #horiz==x vertical==y h=blocks[which][3] if(h == 1): return True else: return False def get_green_by_head(x, y): return getObj(x, y+1) def parseBranch(branch): if(len(branch)>0): if(len(branch)==1): res=branch[0] else: res=c_AND for s in branch: res+=s+", " res=res[0:-2]+")" else: res="" return res def getBranch(which, free_x, free_y): res="" #GREENHEAD if(which==GREENHEAD): res= c_NOT+getBranch( get_green_by_head(free_x,free_y),free_x,free_y)+")" else: x,y,w,h,mv=blocks[which] #Gree car if(mv==-1): if(inp_dict.get(which)==None): inputs.append(Bool('inputs[%s]' % len(inputs))) inp_dict.update({which: f"inputs[{len(inputs)-1}]"}) res = inp_dict[which] #common car else: dictbuf=f"{which}_{free_x}_{free_y}" if(BranchDict.get(dictbuf)!=None): res=BranchDict.get(dictbuf) if(res==""): res="False" else: branches=[[], []] BranchDict.update({dictbuf:res}) if(isHorizintal(which)): check=1 buf=[] for i in range(free_x+1, free_x+w+1): obj=getObj(i, y) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE): buf.append([obj, i, y]) if(check): for l in buf: branches[0].append(getBranch(l[0], l[1], l[2])) if(len(buf)==0): branches[0].append("True") check=1 buf=[] for i in range(free_x-w, free_x): obj=getObj(i, y) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE) : buf.append([obj, i, y]) if(check): for l in buf: branches[1].append(getBranch(l[0], l[1], l[2])) if(len(buf)==0): branches[1].append("True") else: check=1 buf=[] for i in range(free_y+1, free_y+h+1): obj=getObj(x, i) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE) : buf.append([obj, x, i]) if(check): for l in buf: branches[0].append(getBranch(l[0], l[1], l[2])) if(len(buf)==0): branches[0].append("True") check=1 buf=[] for i in range(free_y-h, free_y): obj=getObj(x, i) if(obj != which): if((obj==WALL)): check=0 break elif(obj!=SPACE) : buf.append([obj, x, i]) if(check): for l in buf: branches[1].append(getBranch(l[0], l[1], l[2])) if(len(buf)==0): branches[1].append("True") #print(f"{which}: {blocks[which]}| {branches[0]}, {branches[1]}") if((len(branches[0])+len(branches[1]))==0): res="False" elif (len(branches[0])>0 and len(branches[1])>0): res+=c_OR res+=parseBranch(branches[0])+"," res+=parseBranch(branches[1])+")" else: for i in range(2): res+=parseBranch(branches[i]) if(len(res)>1000): ln=len(main_dots) main_dots.append(Bool('main_dots[%s]' % ln)) s.add(eval(f'main_dots[{ln}]=={res}')) res=f'main_dots[{ln}]' MyTestBlocks.append(blocks[which]) BranchDict.update({dictbuf:res}) if(res!="True") and (res!="False"): return str(simplify(eval(res))) return res def some_foo(): print("You should wait!") x,y,w,h,mv=blocks[target] qua=getBranch(target, x+w-1, y) s.add(eval(qua)) print(s.check()) flag="" for ind in important: var=eval(inp_dict[ind]) bit=str(int(s.model()[var].__bool__())) flag+=bit Myprintflag(flag) boardfile = open(sys.argv[1]).read() header, boardfile = boardfile.split("\n", 1) W, H = [int(x) for x in header.split()] target = -1 walls = [] for line in boardfile.splitlines(): if not line.strip(): continue x,y,w,h,movable = [int(x) for x in line.split()] if movable == -1: #add important.append(len(blocks)) elif movable == -2: target = len(blocks) for i in range(x, x+w): for j in range(y, y+h): if movable != 0: if (i,j) in board: print("Car overlap at %d, %d" % (i,j)) #assert False board[(i,j)] = len(blocks) else: if (i,j) in board and board[i,j] != WALL: print("Wall-car overlap at %d, %d" % (i,j)) #assert False board[(i,j)] = WALL if movable==-1:#add board[(x,y-1)] = GREENHEAD if movable: blocks.append([x,y,w,h, movable]) else: walls.append([x,y,w,h]) some_foo() ``` ## Эпилог Пока делал скрипт более красивым, освобождал от не нужных функций, случайно убрал `s.check()`. Запустил чтобы проверить, а он падает. Уже испугался, что пока писал райтап что-то накосячил в скрипте, и как следствие куда-то сюда тоже скинул кривой код, и придется его переписывать чуть ли не с нуля. А еще, когда я переписывал код, я как-то пропустил `GREENHEAD`'ов, что снова заставило меня понервничать, ведь приводило к неверному флагу. Да, в общем мне нельзя давать кодить и все, что с этим связано, если я, переписывая свой код, допускаю такие ляпы. **И простите меня за мой русский** ![](https://i.imgur.com/Ie5G5sX.png)