Разбор реверса отборочного этапа Я-Профи 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…