Разбор реверса отборочного этапа Я-Профи 2019. Уровень - бакалавр
@ Rakovsky Stanislav | Sunday, Dec 8, 2019 | 13 minutes read | Update at Sunday, Dec 8, 2019

reverse, 17 баллов

Данное задание подготовлено сообществом SPbCTF https://vk.com/spbctf


Программа принимает ключ аргументом командной строки, и проверяет его.

Linux: ./main 1234567890qwerty
*ссылка*

Windows: main.exe 1234567890qwerty
*ссылка*

Найдите верный ключ.
Формат ответа: yaprofi{...}

main

main.exe

Находим место с проверками (взял main.exe):

Строка 50. И тут время остановилось, пение птиц гудение ноутбука прекратилось. Ничто не беспокоит меня. Я един с природой. Так сделаем то, что мы должны - поставим точку останова на условный переход с изменением регистра ZF.

Простой таск, который учит ставить conditional breakpoint.

Флаг: yaprofi{SPLuxztrNpxrmipKYOQseRl}


reverse, 24 балла

Данное задание подготовлено сообществом SPbCTF https://vk.com/spbctf


Раньше можно было набрать 1000 очков, и покажут мультик. Здесь можно пройти 1000 уровней, и покажут флаг! 
Файл: *ссылка*

Debian/Ubuntu: установите apt-get install python python-tk python-pip
Windows: установите Python 2.7.15 https://www.python.org/ftp/python/2.7.15/python-_2.7.15.msi

Установите модули: pip install pycryptodome uncompyle6
Windows only: если возникли сложности с установкой пакета pycryptodome на предыдущем шаге, воспользуйтесь следующей ссылкой: http://www.voidspace.org.uk/python/modules.shtml#pycrypto

Запуск: python main.pyc
Декомпиляция: uncompyle6 main.pyc
Формат ответа: yaprofi{...}

main_3.pyc

Запустим файл Декомпилируем с помощью uncompyle6

# uncompyle6 version 3.5.1
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.15+ (default, Oct  7 2019, 17:39:04) 
# [GCC 7.4.0]
# Embedded file name: /tmp/py2/container.py
# Compiled at: 2019-11-12 02:48:46
from Crypto.Cipher import AES
import hashlib, marshal
from Tkinter import *
import base64, types

def unpad(data):
    return data[:-ord(data[(-1)])]


def round():
    pass


def x2(winner_code, win_pic, offset):
    key = win_pic.decode('base64')[offset:offset + 16]
    aes = AES.new(key, mode=AES.MODE_ECB)
    wc = aes.decrypt(winner_code)
    return wc


def entry():
    key = hashlib.md5(entry.__code__.co_code).digest()
    winner_code = round.__code__.co_consts[(-1)]
    new_function = round.__code__.co_consts[(-2)]
    pics_size = round.__code__.co_consts[(-3)]
    aes = AES.new(key, mode=AES.MODE_ECB)
    new_code = marshal.loads(unpad(aes.decrypt(new_function)))
    pics_sel = list(round.__code__.co_consts[-3 - pics_size:-3])
    pics = list(round.__code__.co_consts[-3 - pics_size - 200:-3 - pics_size])
    round.__code__ = new_code
    round(pics, pics_sel, winner_code)


if __name__ == '__main__':
    entry()
# okay decompiling main_3.pyc

Мы видим грустную ситуацию. Функция entry берет свой байт-код и использует его для расшифровки new_function - переменной типа code, которая будет присвоена функции round. Также есть winner_code и лист pics_sel. В качестве контейнера используются константы функции round, которые uncompyle6 не отобразил просто потому, что они не используются в round.__code__.co_code.

Начнем диссекцию:

Python 2.7.15+ (default, Oct  7 2019, 17:39:04) 
[GCC 7.4.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> globals()
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', '__doc__': None, '__package__': None}
>>> from main_3 import *
>>> globals()
{'getdouble': <type 'float'>, 'MULTIPLE': 'multiple', 'TypeType': <type 'type'>, 'mainloop': 
<function mainloop at 0x7f98f26dd848>, 'Canvas': <class Tkinter.Canvas at 0x7f98efaf13f8>, 
'AtSelLast': <function AtSelLast at 0x7f98efaf4b18>, 'UNITS': 'units', 'CodeType': <type 'code'>, 
'TRUE': 1, 'getboolean': <function getboolean at 0x7f98f2705230>, 'LAST': 'last', 'BASELINE': 'baseline', 
'BOTTOM': 'bottom', 'Wm': <class Tkinter.Wm at 0x7f98efaf1050>, 
'base64': <module 'base64' from '/usr/lib/python2.7/base64.pyc'>, 'NUMERIC': 'numeric', 
'Toplevel': <class Tkinter.Toplevel at 0x7f98efaf1328>, 'DictProxyType': <type 'dictproxy'>, 
'Pack': <class Tkinter.Pack at 0x7f98efaf1120>, 'round': <function round at 0x7f98efb15aa0>, 
'ObjectType': <type 'object'>, 'DictType': <type 'dict'>, 'EXTENDED': 'extended', 'OFF': 0, 'ALL': 'all',
 'CURRENT': 'current', 'CallWrapper': <class Tkinter.CallWrapper at 0x7f98f26d0e88>, 
'Scrollbar': <class Tkinter.Scrollbar at 0x7f98efaf1870>, 'ListType': <type 'list'>, 'X': 'x', 
'ModuleType': <type 'module'>, 'FIRST': 'first', 'ON': 1, 'TracebackType': <type 'traceback'>, 
'image_names': <function image_names at 0x7f98efaf4c08>, 'Text': <class Tkinter.Text at 0x7f98efaf18d8>, 
'YES': 1, 'LambdaType': <type 'function'>, 'GROOVE': 'groove', 'XRangeType': <type 'xrange'>, 
'AES': <module 'Crypto.Cipher.AES' from '/usr/lib/python2.7/dist-packages/Crypto/Cipher/AES.pyc'>, 
'Scale': <class Tkinter.Scale at 0x7f98efaf1808>, 'NORMAL': 'normal', 
'Misc': <class Tkinter.Misc at 0x7f98f26d0e20>, 'BooleanType': <type 'bool'>, 
'Label': <class Tkinter.Label at 0x7f98efaf1598>, 'PAGES': 'pages', 
'LabelFrame': <class Tkinter.LabelFrame at 0x7f98efaf1bb0>, 'ROUND': 'round', 
'image_types': <function image_types at 0x7f98efb00b90>, 'AtInsert': <function AtInsert at 0x7f98efaf4a28>, 
'StringType': <type 'str'>, 'NONE': 'none', 'CENTER': 'center', 'FloatType': <type 'float'>, 
'Spinbox': <class Tkinter.Spinbox at 0x7f98efaf1b48>, 'Radiobutton': <class Tkinter.Radiobutton at 0x7f98efaf17a0>,
'Checkbutton': <class Tkinter.Checkbutton at 0x7f98efaf1460>, 're': <module 're' from '/usr/lib/python2.7/re.pyc'>,
'Grid': <class Tkinter.Grid at 0x7f98efaf11f0>, 'StringTypes': (<type 'str'>, <type 'unicode'>), 
'GeneratorType': <type 'generator'>, 'Button': <class Tkinter.Button at 0x7f98efaf1390>, 'FLAT': 'flat', 
'LongType': <type 'long'>, 'END': 'end', 'VERTICAL': 'vertical', '__builtins__': <module '__builtin__' (built-in)>, 
'MITER': 'miter', 'Widget': <class Tkinter.Widget at 0x7f98efaf12c0>, 'DISABLED': 'disabled', 'S': 's',
'COMMAND': 'command', 'EllipsisType': <type 'ellipsis'>, 'W': 'w', 'ACTIVE': 'active', '__name__': '__main__',
'EW': 'ew', 'FrameType': <type 'frame'>, 'TclVersion': 8.6, 'CHORD': 'chord',
'tkinter': <module '_tkinter' from '/usr/lib/python2.7/lib-dynload/_tkinter.so'>,
'Listbox': <class Tkinter.Listbox at 0x7f98efaf1600>, 'Image': <class Tkinter.Image at 0x7f98efaf1a10>,
'BitmapImage': <class Tkinter.BitmapImage at 0x7f98efaf1ae0>, 'AtSelFirst': <function AtSelFirst at 0x7f98efaf4aa0>,
'RADIOBUTTON': 'radiobutton', 'Place': <class Tkinter.Place at 0x7f98efaf1188>, 'HIDDEN': 'hidden',
'marshal': <module 'marshal' (built-in)>, 'Event': <class Tkinter.Event at 0x7f98f26d0bb0>,
'NoDefaultRoot': <function NoDefaultRoot at 0x7f98f26dd6e0>, 'CHAR': 'char', 'SEPARATOR': 'separator',
'BUTT': 'butt', 'HORIZONTAL': 'horizontal', 'TclError': <class '_tkinter.TclError'>, 'MOVETO': 'moveto',
'WORD': 'word', 'SUNKEN': 'sunken', 'NO': 0, 'DictionaryType': <type 'dict'>, 
'NotImplementedType': <type 'NotImplementedType'>, 'READABLE': 2, 'NE': 'ne', 'CHECKBUTTON': 'checkbutton', 
'Variable': <class Tkinter.Variable at 0x7f98f26d0c18>, 'SEL': 'sel', '__doc__': None, 'NW': 'nw', 'RAISED': 'raised', 
'DoubleVar': <class Tkinter.DoubleVar at 0x7f98f26d0d50>, 'RIDGE': 'ridge',
'BooleanVar': <class Tkinter.BooleanVar at 0x7f98f26d0db8>, 'Tributton': <class Tkinter.Tributton at 0x7f98efaf1ce8>,
'SOLID': 'solid', 'N': 'n', 'CASCADE': 'cascade', 'SEL_FIRST': 'sel.first', 'TkVersion': 8.6, 'UNDERLINE': 'underline', 
'x2': <function x2 at 0x7f98efb15b18>, 'TupleType': <type 'tuple'>, 
'OptionMenu': <class Tkinter.OptionMenu at 0x7f98efaf19a8>, 
'hashlib': <module 'hashlib' from '/usr/lib/python2.7/hashlib.pyc'>, 'NS': 'ns', 'FALSE': 0, 
'ClassType': <type 'classobj'>, 'Frame': <class Tkinter.Frame at 0x7f98efaf1530>, 'SEL_LAST': 'sel.last', 'SW': 'sw', 
'SINGLE': 'single', 'ANCHOR': 'anchor', 'MemberDescriptorType': <type 'member_descriptor'>, 'UnicodeType': <type 'unicode'>, 
'unpad': <function unpad at 0x7f98efb15a28>, 'LEFT': 'left', 'FunctionType': <type 'function'>, 'wantobjects': 1, 'SE': 'se', 
'EXCEPTION': 8, 'IntType': <type 'int'>, 'XView': <class Tkinter.XView at 0x7f98f26d0ef0>, 
'Menu': <class Tkinter.Menu at 0x7f98efaf1668>, 'TOP': 'top', 'GetSetDescriptorType': <type 'getset_descriptor'>, 
'DOTBOX': 'dotbox', 'BaseWidget': <class Tkinter.BaseWidget at 0x7f98efaf1258>, 'OUTSIDE': 'outside', 
'Tk': <class Tkinter.Tk at 0x7f98efaf10b8>, 'IntVar': <class Tkinter.IntVar at 0x7f98f26d0ce8>, 
'YView': <class Tkinter.YView at 0x7f98f26d0f58>, 'UnboundMethodType': <type 'instancemethod'>, 
'PanedWindow': <class Tkinter.PanedWindow at 0x7f98efaf1c18>, 'BOTH': 'both', 
'BuiltinMethodType': <type 'builtin_function_or_method'>, 'AtEnd': <function AtEnd at 0x7f98efaf0e60>, 'BROWSE': 'browse', 
'Tcl': <function Tcl at 0x7f98f27052a8>, 'BuiltinFunctionType': <type 'builtin_function_or_method'>, '__package__': None, 
'ARC': 'arc', 'MethodType': <type 'instancemethod'>, 'SliceType': <type 'slice'>, 'BEVEL': 'bevel', 'E': 'e', 
'InstanceType': <type 'instance'>, 'INSERT': 'insert', 'PIESLICE': 'pieslice', 'FileType': <type 'file'>, 
'sys': <module 'sys' (built-in)>, 'Y': 'y', 'Entry': <class Tkinter.Entry at 0x7f98efaf14c8>, 
'Message': <class Tkinter.Message at 0x7f98efaf1738>, 'types': <module 'types' from '/usr/lib/python2.7/types.pyc'>, 
'PhotoImage': <class Tkinter.PhotoImage at 0x7f98efaf1a78>, 'RIGHT': 'right', 'BufferType': <type 'buffer'>, 
'Studbutton': <class Tkinter.Studbutton at 0x7f98efaf1c80>, 'INSIDE': 'inside', 
'Menubutton': <class Tkinter.Menubutton at 0x7f98efaf16d0>, 'WRITABLE': 4, 
'StringVar': <class Tkinter.StringVar at 0x7f98f26d0c80>, 'PROJECTING': 'projecting', 'At': <function At at 0x7f98efaf4b90>, 
'ComplexType': <type 'complex'>, 'entry': <function entry at 0x7f98efb15b90>, 'NSEW': 'nsew', 'SCROLL': 'scroll', 
'NoneType': <type 'NoneType'>, 'getint': <type 'int'>}

За мусор благодарим from Tkinter import *

Нам интересно, как много всего находится в константах неперезаписанного round:

>>> for i in range(len(consts)):
...     if "__len__" in dir(consts[i]):
...         print i, len(consts[i]), type(consts[i])
...     else:
...         print i, type(consts[i])
... 
0 <type 'NoneType'>
1 56903 <type 'str'>
2 40628 <type 'str'>
3 74083 <type 'str'>
...
198 80166 <type 'str'>
199 73835 <type 'str'>
200 76295 <type 'str'>
201 4 <type 'tuple'>
202 4 <type 'tuple'>
203 4 <type 'tuple'>
...
1198 4 <type 'tuple'>
1199 4 <type 'tuple'>
1200 4 <type 'tuple'>
1201 <type 'int'>
1202 2224 <type 'str'>
1203 208 <type 'str'>

Получается, winner_code - последний (1203-й) элемент, зашифрованный экземпляр code - 1202-й элемент, размер картинок - 1201-й элемент, далее идут кортеджи pics_sel и строки pics. Так как длина последних составляет десятки тысяч строк, для исследования просто сохраним любую из них.

Это b64 гифка альпаки (ламы?). Мило)

Создадим файл loader.py, чтобы руками каждый раз не вводить команды из функции entry:

from main_3 import *
import dis

if 1==1:
    key = hashlib.md5(entry.__code__.co_code).digest()
    winner_code = round.__code__.co_consts[(-1)]
    new_function = round.__code__.co_consts[(-2)]
    pics_size = round.__code__.co_consts[(-3)]
    aes = AES.new(key, mode=AES.MODE_ECB)
    new_code = marshal.loads(unpad(aes.decrypt(new_function)))
    pics_sel = list(round.__code__.co_consts[-3 - pics_size:-3])
    pics = list(round.__code__.co_consts[-3 - pics_size - 200:-3 - pics_size])
    #round.__code__ = new_code
    #round(pics, pics_sel, winner_code)

Дизассемблируем new_code:

>>> len(new_code.co_code)
573
>>> dis.dis(new_code)
  9           0 LOAD_CONST               1 ('xx')
              3 LOAD_CONST              26 (())
              6 LOAD_CONST               2 (<code object xx at 0x7ffafe3a9830, file "/tmp/py2/tkinter_t.py", line 9>)
              9 MAKE_FUNCTION            0
             12 CALL_FUNCTION            0
             15 BUILD_CLASS         
             16 STORE_FAST               3 (xx)

 14          19 LOAD_CONST               3 (1)
             22 STORE_FAST               4 (level)

 15          25 SETUP_LOOP             507 (to 535)
             28 LOAD_FAST                1 (pics_sel)
             31 GET_ITER            
        >>   32 FOR_ITER               499 (to 534)
             35 STORE_FAST               5 (i)

 16          38 LOAD_FAST                5 (i)
             41 LOAD_CONST               4 (0)
             44 BINARY_SUBSCR       
             45 LOAD_FAST                5 (i)
             48 LOAD_CONST               3 (1)
             51 BINARY_SUBSCR       
             52 LOAD_FAST                5 (i)
             55 LOAD_CONST               5 (2)
             58 BINARY_SUBSCR       
             59 ROT_THREE           
             60 ROT_TWO             
             61 STORE_FAST               6 (photo1)
             64 STORE_FAST               7 (photo2)
             67 STORE_FAST               8 (res)

 17          70 LOAD_FAST                0 (pics)
             73 LOAD_FAST                6 (photo1)
             76 BINARY_SUBSCR       
             77 STORE_FAST               6 (photo1)

 18          80 LOAD_FAST                0 (pics)
             83 LOAD_FAST                7 (photo2)
             86 BINARY_SUBSCR       
             87 STORE_FAST               7 (photo2)

 19          90 LOAD_FAST                3 (xx)
             93 LOAD_FAST                8 (res)
             96 CALL_FUNCTION            1
             99 STORE_DEREF              1 (v)

 21         102 LOAD_CONST               6 (<code object disable_event at 0x7ffafe3b24b0, file "/tmp/py2/tkinter_t.py", line 21>)
            105 MAKE_FUNCTION            0
            108 STORE_FAST               9 (disable_event)

 23         111 LOAD_CLOSURE             0 (root)
            114 BUILD_TUPLE              1
            117 LOAD_CONST               7 (<code object close_window at 0x7ffafe3b8030, file "/tmp/py2/tkinter_t.py", line 23>)
            120 MAKE_CLOSURE             0
            123 STORE_FAST              10 (close_window)

 25         126 LOAD_CLOSURE             0 (root)
            129 LOAD_CLOSURE             1 (v)
            132 BUILD_TUPLE              2
            135 LOAD_CONST               8 (<code object win at 0x7ffafe3c3f30, file "/tmp/py2/tkinter_t.py", line 25>)
            138 MAKE_CLOSURE             0
            141 STORE_FAST              11 (win)

 29         144 LOAD_GLOBAL              0 (Tk)
            147 CALL_FUNCTION            0
            150 STORE_DEREF              0 (root)

 30         153 LOAD_DEREF               0 (root)
            156 LOAD_ATTR                1 (title)
            159 LOAD_CONST               9 ('Alpacker: Llama or Alpaca?')
            162 CALL_FUNCTION            1
            165 POP_TOP             

 31         166 LOAD_DEREF               0 (root)
            169 LOAD_ATTR                2 (resizable)
            172 LOAD_GLOBAL              3 (False)
            175 LOAD_GLOBAL              3 (False)
            178 CALL_FUNCTION            2
            181 POP_TOP             

 32         182 LOAD_GLOBAL              4 (Button)
            185 LOAD_DEREF               0 (root)
            188 LOAD_CONST              10 ('justify')
            191 LOAD_GLOBAL              5 (LEFT)
            194 LOAD_CONST              11 ('command')
            197 LOAD_FAST               10 (close_window)
            200 CALL_FUNCTION          513
            203 STORE_FAST              12 (b)

 33         206 LOAD_GLOBAL              4 (Button)
            209 LOAD_DEREF               0 (root)
            212 LOAD_CONST              10 ('justify')
            215 LOAD_GLOBAL              6 (RIGHT)
            218 LOAD_CONST              11 ('command')
            221 LOAD_FAST               11 (win)
            224 CALL_FUNCTION          513
            227 STORE_FAST              13 (b2)

 34         230 LOAD_GLOBAL              7 (PhotoImage)
            233 LOAD_CONST              12 ('data')
            236 LOAD_FAST                6 (photo1)
            239 CALL_FUNCTION          256
            242 STORE_FAST              14 (photo)

 35         245 LOAD_GLOBAL              7 (PhotoImage)
            248 LOAD_CONST              12 ('data')
            251 LOAD_FAST                7 (photo2)
            254 CALL_FUNCTION          256
            257 STORE_FAST              15 (photo3)

 36         260 LOAD_FAST               12 (b)
            263 LOAD_ATTR                8 (config)
            266 LOAD_CONST              13 ('image')
            269 LOAD_FAST               14 (photo)
            272 CALL_FUNCTION          256
            275 POP_TOP             

 37         276 LOAD_FAST               12 (b)
            279 LOAD_ATTR                9 (pack)
            282 LOAD_CONST              14 ('side')
            285 LOAD_GLOBAL              5 (LEFT)
            288 CALL_FUNCTION          256
            291 POP_TOP             

 38         292 LOAD_FAST               13 (b2)
            295 LOAD_ATTR                8 (config)
            298 LOAD_CONST              13 ('image')
            301 LOAD_FAST               15 (photo3)
            304 CALL_FUNCTION          256
            307 POP_TOP             

 39         308 LOAD_FAST               13 (b2)
            311 LOAD_ATTR                9 (pack)
            314 LOAD_CONST              14 ('side')
            317 LOAD_GLOBAL              6 (RIGHT)
            320 CALL_FUNCTION          256
            323 POP_TOP             

 40         324 LOAD_GLOBAL             10 (Label)
            327 LOAD_DEREF               0 (root)
            330 LOAD_CONST              15 ('text')
            333 LOAD_CONST              16 ('{} of {}')
            336 LOAD_ATTR               11 (format)
            339 LOAD_FAST                4 (level)
            342 LOAD_CONST              17 (1000)
            345 CALL_FUNCTION            2
            348 CALL_FUNCTION          257
            351 STORE_FAST              16 (w)

 41         354 LOAD_FAST               16 (w)
            357 LOAD_ATTR                9 (pack)
            360 LOAD_CONST              18 ('padx')
            363 LOAD_CONST              19 (5)
            366 LOAD_CONST              20 ('pady')
            369 LOAD_CONST              19 (5)
            372 CALL_FUNCTION          512
            375 POP_TOP             

 42         376 LOAD_GLOBAL             10 (Label)
            379 LOAD_DEREF               0 (root)
            382 LOAD_CONST              15 ('text')
            385 LOAD_CONST              21 ('Choose Alpaca!')
            388 LOAD_ATTR               11 (format)
            391 LOAD_CONST               3 (1)
            394 LOAD_CONST              17 (1000)
            397 CALL_FUNCTION            2
            400 CALL_FUNCTION          257
            403 STORE_FAST              17 (w1)

 43         406 LOAD_FAST               17 (w1)
            409 LOAD_ATTR                9 (pack)
            412 LOAD_CONST              18 ('padx')
            415 LOAD_CONST              19 (5)
            418 LOAD_CONST              20 ('pady')
            421 LOAD_CONST              19 (5)
            424 CALL_FUNCTION          512
            427 POP_TOP             

 44         428 LOAD_DEREF               0 (root)
            431 LOAD_ATTR               12 (protocol)
            434 LOAD_CONST              22 ('WM_DELETE_WINDOW')
            437 LOAD_FAST                9 (disable_event)
            440 CALL_FUNCTION            2
            443 POP_TOP             

 45         444 LOAD_DEREF               0 (root)
            447 LOAD_ATTR               13 (mainloop)
            450 CALL_FUNCTION            0
            453 POP_TOP             

 46         454 LOAD_DEREF               1 (v)
            457 LOAD_ATTR               14 (x)
            460 LOAD_CONST               3 (1)
            463 COMPARE_OP               2 (==)
            466 POP_JUMP_IF_FALSE      487

 47         469 LOAD_CONST              23 ('Nope!')
            472 PRINT_ITEM          
            473 PRINT_NEWLINE       

 48         474 LOAD_GLOBAL             15 (exit)
            477 LOAD_CONST               4 (0)
            480 CALL_FUNCTION            1
            483 POP_TOP             
            484 JUMP_FORWARD             0 (to 487)

 49     >>  487 LOAD_GLOBAL             16 (x2)
            490 LOAD_FAST                2 (wc)
            493 LOAD_FAST                0 (pics)
            496 LOAD_FAST                5 (i)
            499 LOAD_FAST                5 (i)
            502 LOAD_CONST               5 (2)
            505 BINARY_SUBSCR       
            506 BINARY_SUBSCR       
            507 BINARY_SUBSCR       
            508 LOAD_FAST                5 (i)
            511 LOAD_CONST              24 (3)
            514 BINARY_SUBSCR       
            515 CALL_FUNCTION            3
            518 STORE_FAST               2 (wc)

 50         521 LOAD_FAST                4 (level)
            524 LOAD_CONST               3 (1)
            527 INPLACE_ADD         
            528 STORE_FAST               4 (level)
            531 JUMP_ABSOLUTE           32
        >>  534 POP_BLOCK           

 52     >>  535 LOAD_CONST              25 (<code object y at 0x7ffafb7e3730, file "/tmp/py2/tkinter_t.py", line 52>)
            538 MAKE_FUNCTION            0
            541 STORE_FAST              18 (y)

 54         544 LOAD_GLOBAL             17 (marshal)
            547 LOAD_ATTR               18 (loads)
            550 LOAD_FAST                2 (wc)
            553 CALL_FUNCTION            1
            556 LOAD_FAST               18 (y)
            559 STORE_ATTR              19 (__code__)

 55         562 LOAD_FAST               18 (y)
            565 CALL_FUNCTION            0
            568 POP_TOP             
            569 LOAD_CONST               0 (None)
            572 RETURN_VALUE      

Окей, 55 строк, 573 байта.

Задача - выбрать 1000 альпак, остерегаясь лам.

За полчаса можно пройти игру эмпирическим путем на виртуалке, делая снапшот каждые 10-50 решений. Но это разве наш вариант?)

Так что будем патчить код функции. Я решил таск, просто за-nop-ив проверку правильности выбора (байты 454-486) и прокликав 1000 раз, т.к. не увидел использование своего байт-кода как части ключа, IV либо других параметров для расшифровки. Данная статья приводит функцию по замене байт-кода. Давайте модифицируем наш загрузчик, чтобы он запатчил функцию:

from main_3 import *
import dis

from types import CodeType

def fix_function(fn_code, payload):
    return CodeType(fn_code.co_argcount,
                             #fn_code.co_kwonlyargcount,
                             fn_code.co_nlocals,
                             fn_code.co_stacksize,
                             fn_code.co_flags,
                             payload,
                             fn_code.co_consts,
                             fn_code.co_names,
                             fn_code.co_varnames,
                             fn_code.co_filename,
                             fn_code.co_name,
                             fn_code.co_firstlineno,
                             fn_code.co_lnotab,
                             fn_code.co_freevars,
                             fn_code.co_cellvars,
                             )


if 1==1:
    key = hashlib.md5(entry.__code__.co_code).digest()
    winner_code = round.__code__.co_consts[(-1)]
    new_function = round.__code__.co_consts[(-2)]
    pics_size = round.__code__.co_consts[(-3)]
    aes = AES.new(key, mode=AES.MODE_ECB)
    new_code = marshal.loads(unpad(aes.decrypt(new_function)))
    pics_sel = list(round.__code__.co_consts[-3 - pics_size:-3])
    pics = list(round.__code__.co_consts[-3 - pics_size - 200:-3 - pics_size])

    # nopping...
    new_co_code = list(new_code.co_code)
    for i in range(454, 486+1):
        new_co_code[i] = chr(0x9) # NOP opcode
    new_co_code = "".join(new_co_code)
    new_code = fix_function(new_code, new_co_code)
    round.__code__ = new_code
    round(pics, pics_sel, winner_code)

Этот способ действительно даст ключ:

You win! here is your flag: yaprofi{MgrfVhFUqApexYrdfvuijjP}

Но ведь хочется облегчить анализ кода, а не смотреть на эти длинные строки dis.dis в необходимости разобраться, где именно патчить)

Уровень 1: uncompyle6.

loader.py:

from main_3 import *
from uncompyle6.main import decompile
import sys


if 1==1:
    key = hashlib.md5(entry.__code__.co_code).digest()
    winner_code = round.__code__.co_consts[(-1)]
    new_function = round.__code__.co_consts[(-2)]
    pics_size = round.__code__.co_consts[(-3)]
    aes = AES.new(key, mode=AES.MODE_ECB)
    new_code = marshal.loads(unpad(aes.decrypt(new_function)))
    pics_sel = list(round.__code__.co_consts[-3 - pics_size:-3])
    pics = list(round.__code__.co_consts[-3 - pics_size - 200:-3 - pics_size])

    round.__code__ = new_code
    decompile(2.7, round.__code__, sys.stdout)

Вывод:

>>> from loader import *
# uncompyle6 version 3.5.1
# Python bytecode 2.7
# Decompiled from: Python 2.7.15+ (default, Oct  7 2019, 17:39:04) 
# [GCC 7.4.0]
# Embedded file name: /tmp/py2/tkinter_t.py


class xx:

    def __init__(self, v):
        self.x = v

    def toggle(self):
        self.x = self.x ^ 1


level = 1
for i in pics_sel:
    photo1, photo2, res = i[0], i[1], i[2]
    photo1 = pics[photo1]
    photo2 = pics[photo2]
    v = xx(res)

    def disable_event():
        pass


    def close_window():
        root.destroy()


    def win():
        v.toggle()
        root.destroy()


    root = Tk()
    root.title('Alpacker: Llama or Alpaca?')
    root.resizable(False, False)
    b = Button(root, justify=LEFT, command=close_window)
    b2 = Button(root, justify=RIGHT, command=win)
    photo = PhotoImage(data=photo1)
    photo3 = PhotoImage(data=photo2)
    b.config(image=photo)
    b.pack(side=LEFT)
    b2.config(image=photo3)
    b2.pack(side=RIGHT)
    w = Label(root, text=('{} of {}').format(level, 1000))
    w.pack(padx=5, pady=5)
    w1 = Label(root, text=('Choose Alpaca!').format(1, 1000))
    w1.pack(padx=5, pady=5)
    root.protocol('WM_DELETE_WINDOW', disable_event)
    root.mainloop()
    if v.x == 1:
        print 'Nope!'
        exit(0)
    wc = x2(wc, pics[i[i[2]]], i[3])
    level += 1

def y():
    pass


y.__code__ = marshal.loads(wc)
y()

Оформим решение в виде файла writeup.py:

from main_3 import *


def x2(winner_code, win_pic, offset):
    key = win_pic.decode('base64')[offset:offset + 16]
    aes = AES.new(key, mode=AES.MODE_ECB)
    wc = aes.decrypt(winner_code)
    return wc

def rround(pics, pics_sel, wc):
    class xx:

        def __init__(self, v):
            self.x = v

        def toggle(self):
            self.x = self.x ^ 1

    for i in pics_sel:
        wc = x2(wc, pics[i[i[2]]], i[3])

    def y():
        pass

    y.__code__ = marshal.loads(wc)
    y()


if 1==1:
    winner_code = round.__code__.co_consts[(-1)]
    pics_size = round.__code__.co_consts[(-3)]
    pics_sel = list(round.__code__.co_consts[-3 - pics_size:-3])
    pics = list(round.__code__.co_consts[-3 - pics_size - 200:-3 - pics_size])
    rround(pics, pics_sel, winner_code)
python2 ./writeup.py
You win! here is your flag: yaprofi{MgrfVhFUqApexYrdfvuijjP}

Давайте заглянем внутрь финальной версии функции y с помощью uncompyle6:

print 'You win! here is your flag: yaprofi{MgrfVhFUqApexYrdfvuijjP}'

Т.е. никакой криптографии, которая бы зависела от внешних факторов, нет.

Уровень 2: control flow graph

Представим, что организаторы пошли дальше и испортили часть инструкций так, чтобы uncompyle6 не мог справиться, и нам следовало бы ковырять бинарщину) У fireeye есть замечательная тулза, которая позволяет строить cfg для питоновского байт-кода. Вот так выглядит функция round в ее представлении:

С CFG жизнь становится проще. Используем нашу патч-машину, чтобы за-nop-ить 102-486 байты

from main_3 import *
import dis

from types import CodeType

def fix_function(fn_code, payload):
    return CodeType(fn_code.co_argcount,
                             #fn_code.co_kwonlyargcount,
                             fn_code.co_nlocals,
                             fn_code.co_stacksize,
                             fn_code.co_flags,
                             payload,
                             fn_code.co_consts,
                             fn_code.co_names,
                             fn_code.co_varnames,
                             fn_code.co_filename,
                             fn_code.co_name,
                             fn_code.co_firstlineno,
                             fn_code.co_lnotab,
                             fn_code.co_freevars,
                             fn_code.co_cellvars,
                             )


if 1==1:
    key = hashlib.md5(entry.__code__.co_code).digest()
    winner_code = round.__code__.co_consts[(-1)]
    new_function = round.__code__.co_consts[(-2)]
    pics_size = round.__code__.co_consts[(-3)]
    aes = AES.new(key, mode=AES.MODE_ECB)
    new_code = marshal.loads(unpad(aes.decrypt(new_function)))
    pics_sel = list(round.__code__.co_consts[-3 - pics_size:-3])
    pics = list(round.__code__.co_consts[-3 - pics_size - 200:-3 - pics_size])

    # nopping...
    new_co_code = list(new_code.co_code)
    for i in range(102, 486+1):
        new_co_code[i] = chr(0x9) # NOP opcode
    new_co_code = "".join(new_co_code)
    new_code = fix_function(new_code, new_co_code)
    round.__code__ = new_code
    round(pics, pics_sel, winner_code)
python2 loader.py
You win! here is your flag: yaprofi{MgrfVhFUqApexYrdfvuijjP}

author, editor: Rakovsky Stanislav, Unicorn CTF

Cats!

Under construction

Wow! Flipable!

Hello from another side of the Moon!

Looking for a flag? Okay, take this:
LFXXKIDBOJSSA43XMVSXIIC6LYQCAIA=

About me

Under construction. You can try to contact me and fill this field… haha… ha…