Условие задачи:
Handcrafted pyc
XXX Teams solved.
Description
Can your brain be a Python VM? (Please use Python 2.7)
crackme
Hint
None
И нашему вниманию представляется crackme.py файл следующего содержимого:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import marshal, zlib, base64
exec(marshal.loads(zlib.decompress(base64.b64decode('eJyNVktv00AQXm/eL0igiaFA01IO4cIVCUGFBBJwqRAckLhEIQmtRfPwI0QIeio/hRO/hJ/CiStH2M/prj07diGRP43Hs9+MZ2fWMxbnP6mux+oK9xVMHPFViLdCTB0xkeKDFEFfTIU4E8KZq8dCvB4UlN3hGEsdddXU9QTLv1eFiGKGM4cKUgsFCNLFH7dFrS9poayFYmIZm1b0gyqxMOwJaU3r6xs9sW1ooakXuRv+un7Q0sIlLVzOCZq/XtsK2oTSYaZlStogXi1HV0iazoN2CV2HZeXqRQ54TlJRb7FUlKyUatISsdzo+P7UU1Gb1POdMruckepGwk9tIXQTftz2yBaT5JQovWvpSa6poJPuqgao+b9l5Aj/R+mLQIP4f6Q8Vb3g/5TB/TJxWGdZr9EQrmn99fwKtTvAZGU7wzS7GNpZpDm2JgCrr8wrmPoo54UqGampFIeS9ojXjc4E2yI06bq/4DRoUAc0nVnng4k6p7Ks0+j/S8z9V+NZ5dhmrJUM/y7JTJeRtnJ2TSYJvsFq3CQt/vnfqmQXt5KlpuRcIvDAmhnn2E0t9BJ3SvB/SfLWhuOWNiNVZ+h28g4wlwUp00w95si43rZ3r6+fUIEdgOZbQAsyFRRvBR6dla8KCzRdslar7WS+a5HFb39peIAmG7uZTHVm17Czxju4m6bayz8e7J40DzqM0jr0bmv9PmPvk6y5z57HU8wdTDHeiUJvBMAM4+0CpoAZ4BPgJeAYEAHmgAUgAHiAj4AVAGORtwd4AVgC3gEmgBBwCPgMWANOAQ8AbwBHgHuAp4D3gLuARwoGmNUizF/j4yDC5BWM1kNvvlxFA8xikRrBxHIUhutFMBlgQoshhPphGAXe/OggKqqb2cibxwuEXjUcQjccxi5eFRL1fDSbKrUhy2CMb2aLyepkegDWsBwPlrVC0/kLHmeCBQ=='))))
Сразу становится понятно что тут используется сериализованный код python. Нам необходимо получить pyc
файл для того чтобы воспользоваться доступными средствами для декомпиляции в читаемый исходный код.
Воспользуемся следующим скриптом, чтобы превратить наш сериализованный код в pyc
файл.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import marshal, zlib, base64, time, py_compile
code = marshal.loads(zlib.decompress(base64.b64decode('eJyNVktv00AQXm/eL0igiaFA01IO4cIVCUGFBBJwqRAckLhEIQmtRfPwI0QIeio/hRO/hJ/CiStH2M/prj07diGRP43Hs9+MZ2fWMxbnP6mux+oK9xVMHPFViLdCTB0xkeKDFEFfTIU4E8KZq8dCvB4UlN3hGEsdddXU9QTLv1eFiGKGM4cKUgsFCNLFH7dFrS9poayFYmIZm1b0gyqxMOwJaU3r6xs9sW1ooakXuRv+un7Q0sIlLVzOCZq/XtsK2oTSYaZlStogXi1HV0iazoN2CV2HZeXqRQ54TlJRb7FUlKyUatISsdzo+P7UU1Gb1POdMruckepGwk9tIXQTftz2yBaT5JQovWvpSa6poJPuqgao+b9l5Aj/R+mLQIP4f6Q8Vb3g/5TB/TJxWGdZr9EQrmn99fwKtTvAZGU7wzS7GNpZpDm2JgCrr8wrmPoo54UqGampFIeS9ojXjc4E2yI06bq/4DRoUAc0nVnng4k6p7Ks0+j/S8z9V+NZ5dhmrJUM/y7JTJeRtnJ2TSYJvsFq3CQt/vnfqmQXt5KlpuRcIvDAmhnn2E0t9BJ3SvB/SfLWhuOWNiNVZ+h28g4wlwUp00w95si43rZ3r6+fUIEdgOZbQAsyFRRvBR6dla8KCzRdslar7WS+a5HFb39peIAmG7uZTHVm17Czxju4m6bayz8e7J40DzqM0jr0bmv9PmPvk6y5z57HU8wdTDHeiUJvBMAM4+0CpoAZ4BPgJeAYEAHmgAUgAHiAj4AVAGORtwd4AVgC3gEmgBBwCPgMWANOAQ8AbwBHgHuAp4D3gLuARwoGmNUizF/j4yDC5BWM1kNvvlxFA8xikRrBxHIUhutFMBlgQoshhPphGAXe/OggKqqb2cibxwuEXjUcQjccxi5eFRL1fDSbKrUhy2CMb2aLyepkegDWsBwPlrVC0/kLHmeCBQ==')))
with open('crackme.pyc', 'wb') as fc:
fc.write('\0\0\0\0')
py_compile.wr_long(fc, long(time.time()))
marshal.dump(code, fc)
fc.flush()
fc.seek(0, 0)
fc.write(py_compile.MAGIC)
Таким образом мы получаем pyc и запусив его удостоверяемся что он рабочий:
root@kali:~/HITCON# python crackme.pyc
password:
Wrong password... Please try again. Do not brute force. =)
root@kali:~/HITCON#
На запрос пароля нажимаем Enter и видим сообщение о неверном пароле и совете не брутить его.
Теперь немного о структуре PYC файла:
- Первые 8 байт это заголовок:
- 4 байта MAGIC число символизирующее версию Python для корректной десериализации и последуюшего испольнения кода
- 4 байта timestamp
- Далее следует сериализованный код
Приступим к декомпиляции, воспользуемся инструментом uncompyle6
и выполним команду
uncompyle6 -o . crackme.pyc
В итоге получем сообщение что все ок! Декомпиляция удалась!
2199 CALL_FUNCTION_1 1
2202 ROT_TWO
2203 BINARY_ADD
2204 ROT_TWO
2205 BINARY_ADD
2206 ROT_TWO
2207 BINARY_ADD
2208 BINARY_ADD
2209 BINARY_ADD
2210 BINARY_ADD
2211 BINARY_ADD
2212 PRINT_ITEM
<string>
Successfully decompiled file
Смотрим полученный в ходе декомпиляции файл crackme.py
и видим, что что-то пошло не так, так как код не декомпилировался полностью и это не радует. Вот такой резульатат в итоге:
2202 ROT_TWO
2203 BINARY_ADD
2204 ROT_TWO
2205 BINARY_ADD
2206 ROT_TWO
2207 BINARY_ADD
2208 BINARY_ADD
2209 BINARY_ADD
2210 BINARY_ADD
2211 BINARY_ADD
2212 PRINT_ITEM
2213 PRINT_NEWLINE_CONT
Parse error at or near `ROT_TWO' instruction at offset 36
if __name__ == '__main__':
main()
Следующая ошибка встречает наc Parse error at or near
ROT_TWO' instruction at offset 36` . Сравним что получается при выводе опкодов если воспользоваться стандартным пакетом и получаем слудующий вывод:
1 0 LOAD_CONST 1 (<code object main at 0x7f08a0a0f830, file "<string>", line 1>)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (main)
4 9 LOAD_NAME 1 (__name__)
12 LOAD_CONST 2 ('__main__')
15 COMPARE_OP 2 (==)
18 POP_JUMP_IF_FALSE 31
5 21 LOAD_NAME 0 (main)
24 CALL_FUNCTION 0
27 POP_TOP
28 JUMP_FORWARD 0 (to 31)
>> 31 LOAD_CONST 0 (None)
34 RETURN_VALUE
None
Видим что у нас первая инструкция это функция main которая содрежит судя по всему основной код и эта функция вызывается при запуске модуля. Для закрепления этого вывода используем конструкцию print dis.dis(code)
где code переменная содержаща результат marshal.loads()
Теперь нам нужно посмотреть опкоды функции main, для этого также воспользуемся стандартным пакетом dis.dis(code.co_consts[1])
и индекс тут 1 потому что если вывести значение co_consts то получится:
(None, <code object main at 0x7fb3e7ff6830, file "<string>", line 1>, '__main__')
Видим что код абсолютно идентичен, значит проблема в инструменте декомпиляции. И тут вот начинается самая интересная часть - чтение опкодов.
Находим в распечатанных опкодах следующее место:
2212 PRINT_ITEM 47
2213 PRINT_NEWLINE 48
2214 LOAD_CONST 0 (None) 64 00 00
2217 RETURN_VALUE 53
справа от инструкций я вставил их шестнадцатеричные коды, то есть представление, которое различно для разных версий питона. В частности у меня версия 2.7.12. Все объявления расположены по следующему пути /usr/include/python2.7/opcode.h
. Слева от называний опкодов указано их смещение. Заметим, что из 4 опкодов самый длинный LOAD_CONST #define LOAD_CONST 100 /* Index in const list */
и занимает она 3 байта. Первый байт 64 это сам опкод инсрукции, а следующие два байта это аргументы, т.е. индекс константы находящейся в листе констант, по 0 индексу там будет находиться предупреждение о том, что пароль не верен.
Теперь таким образом на нужно вставить инструкцию в позицию 2212, перед опкодом PRINT_ITEM, так как Python язык выполнение которого основано на стеке, то мы сможем вывести при помощи опкода LOAD_FAST который тоже содержит 3 байта define LOAD_FAST 124 /* Local variable number */
значение локальной перменной password
. Она находится под индексом 0.
>>> main_fn = code.co_consts[1]
>>> dir(main_fn)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> main_fn.co_varnames
('password',)
>>> main_fn.co_nlocals
1
>>>
Но вот просто взять и проатчить объект code в Python нельзя, так как объект этого класса является immutable, т.е. не может быть изменен. Но есть идея по поводу этого момента, это прежде всего обернуть в новый класс сущесвтвующий code объект c копированием всех значений свойств:
['co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
Для этого создадим класс:
class MutableCodeObject(object):
args_name = ('co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames')
def __init__(self, initial_code):
self.initial_code = initial_code
for attr_name in self.args_name:
attr = getattr(self.initial_code, attr_name)
setattr(self, attr_name, attr)
def get_code(self):
return self.initial_code.__class__(self.co_argcount, self.co_nlocals, self.co_stacksize, self.co_flags, self.co_code, self.co_consts, self.co_names, self.co_varnames, self.co_filename, self.co_name, self.co_firstlineno, self.co_lnotab, self.co_freevars, self.co_cellvars)
Теперь мы можем в конструктор этого класса MutableCodeObject передать в качестве параметра initial_code существующий объект кода нашей функйии main. Далее после модификации значения в co_code мы можетм получить новый объект code, вызывав метод get_code(), который нам вернет новый объект code через передачу праметров в конструктор класса.
Но для того чтобы вставить новую инструкцию в код, нам нужно найти место, можно воспользоваться смещением, но раз мы взялись за чтение опкодов, то можем составить последовательность байт перед которыми нам нужно вставить инструкцию.
2212 PRINT_ITEM 47
2213 PRINT_NEWLINE 48
2214 LOAD_CONST 0 (None) 64 00 00
2217 RETURN_VALUE 53
Таким образом составляем шестнадцатеричную строку "474864000053" для поиска. Получается следующий код:
bcode = zlib.decompress(base64.b64decode('eJyNVktv00AQXm/eL0igiaFA01IO4cIVCUGFBBJwqRAckLhEIQmtRfPwI0QIeio/hRO/hJ/CiStH2M/prj07diGRP43Hs9+MZ2fWMxbnP6mux+oK9xVMHPFViLdCTB0xkeKDFEFfTIU4E8KZq8dCvB4UlN3hGEsdddXU9QTLv1eFiGKGM4cKUgsFCNLFH7dFrS9poayFYmIZm1b0gyqxMOwJaU3r6xs9sW1ooakXuRv+un7Q0sIlLVzOCZq/XtsK2oTSYaZlStogXi1HV0iazoN2CV2HZeXqRQ54TlJRb7FUlKyUatISsdzo+P7UU1Gb1POdMruckepGwk9tIXQTftz2yBaT5JQovWvpSa6poJPuqgao+b9l5Aj/R+mLQIP4f6Q8Vb3g/5TB/TJxWGdZr9EQrmn99fwKtTvAZGU7wzS7GNpZpDm2JgCrr8wrmPoo54UqGampFIeS9ojXjc4E2yI06bq/4DRoUAc0nVnng4k6p7Ks0+j/S8z9V+NZ5dhmrJUM/y7JTJeRtnJ2TSYJvsFq3CQt/vnfqmQXt5KlpuRcIvDAmhnn2E0t9BJ3SvB/SfLWhuOWNiNVZ+h28g4wlwUp00w95si43rZ3r6+fUIEdgOZbQAsyFRRvBR6dla8KCzRdslar7WS+a5HFb39peIAmG7uZTHVm17Czxju4m6bayz8e7J40DzqM0jr0bmv9PmPvk6y5z57HU8wdTDHeiUJvBMAM4+0CpoAZ4BPgJeAYEAHmgAUgAHiAj4AVAGORtwd4AVgC3gEmgBBwCPgMWANOAQ8AbwBHgHuAp4D3gLuARwoGmNUizF/j4yDC5BWM1kNvvlxFA8xikRrBxHIUhutFMBlgQoshhPphGAXe/OggKqqb2cibxwuEXjUcQjccxi5eFRL1fDSbKrUhy2CMb2aLyepkegDWsBwPlrVC0/kLHmeCBQ=='))
mut_code = MutableCodeObject(marshal.loads(bcode))
hex_bcode = mut_code.co_code.encode('hex')
main_func = MutableCodeObject(mut_code.co_consts[1])
main_func_hex = main_func.co_code.encode('hex')
idx1 = main_func_hex.index("474864000053")
print idx1
Выше видим код для получения индекса начала последовательности инструкций. Теперь нужно вставить инструкцию LOAD_FAST 0 (password) 7C 00 00
чтобы отбразить значение локальной переменной password по индексу 0, получается три байта 7C 00 00
. Теперь вставляем эту инструкцию:
mycode = ""
left = main_func_hex[:idx1]
'''
--------------------------------------------------------
LOAD_FAST 0 (password) 7C 00 00
--------------------------------------------------------
'''
load_fast = "7C0000"
right = main_func_hex[idx1:]
mycode = left + load_fast + right
main_func.co_code = mycode.decode('hex')
exec(main_func.get_code())
Теперь запускаем наш получившийся файл на исполнение и после нажатия Enter без ввода пароля, получаем пароль:
root@kali:~/HITCON/TODAY# python crackme_advanced.py
password:
Call me a Python virtual machine! I can interpret Python bytecodes!!!
И теперь имея пароль, можно запустить оригинальный файл и ввести на запрос пароля эту длинную строку.
Но есть еще один способ. Можно заменить инструкцию условного перехода на противоположную, (по принципу Криса Касперского). Если посмотреть на код, то видим:
736 BINARY_ADD
737 LOAD_CONST 0 (None)
740 NOP
741 JUMP_ABSOLUTE 759
>> 744 LOAD_GLOBAL 1 (raw_input)
747 JUMP_ABSOLUTE 1480
>> 750 LOAD_FAST 0 (password)
753 COMPARE_OP 2 (==)
756 JUMP_ABSOLUTE 767
>> 759 ROT_TWO
760 STORE_FAST 0 (password)
763 POP_TOP
764 JUMP_ABSOLUTE 744
>> 767 POP_JUMP_IF_FALSE 1591 72 37 06
770 LOAD_GLOBAL 0 (chr) 74 00 00
773 LOAD_CONST 17 (99) 64 11 00
776 CALL_FUNCTION 1
Нас интересуют следующие инструкции:
767 POP_JUMP_IF_FALSE 1591 72 37 06
770 LOAD_GLOBAL 0 (chr) 74 00 00
773 LOAD_CONST 17 (99) 64 11 00
Видим по смещению 767 инструкцию POP_JUMP_IF_FALSE, при помощи которой, если пароли не совпадают, то производится переход на вывод сообщения о том, что пароль не правильный.
Тут стоит немного уточнить как формируютс байты команды POP_JUMP_IF_FALSE. Из файла с объявлениями опкодов, видим define POP_JUMP_IF_FALSE 114 /* "" */
114 - это в десятичном исчислении а в hex
это 72. Теперь видим в качестве параметра передается число 1591 , в hex
это 637 , но учитывая Little Indian нотацию расположения байт, то у нас hex
число 637 превращается в 37 06
-таким образом старший байт по младшему адресу распологается. И так у нас получается строка для поиска 723706740000641100
из трех инструкций с их парамерами, изображенными выше. Заменяем инструкцию POP_JUMP_IF_FALSE на POP_JUMP_IF_TRUE, код этой инструкции define POP_JUMP_IF_TRUE 115 /* "" */
115 в hex это "73". Перед поиском последовательности байт и редактированием, удобно воспользоваться функцией encode('hex') для co_code содержимого.
Получается следующий кусочек кода:
idx2 = main_func_hex.index("723706740000641100")
left = main_func_hex[:idx2]
pop_jump_if_true = "73"
right = main_func_hex[idx2+2:]
mycode = left + pop_jump_if_true + right
main_func.co_code = mycode.decode('hex')
exec(main_func.get_code())
Таким образом после запуска этого кода, после нажати Enter отображается сразу флаг.
Вот таким образом, эта задача на HITCON 2016 - дала возможность заглянуть под капот Python вируальной машины и получить опыт чтения опкодов. Очень интересно :) Спасибо @orange за классные задачи на этом CTF.
Привожу полный код для двух способов получени пароля и флага.
!/usr/bin/env python
# -*- coding: utf-8 -*-
import marshal, dis, zlib, base64, imp, time
import py_compile
class MutableCodeObject(object):
args_name = ('co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames')
def __init__(self, initial_code):
self.initial_code = initial_code
for attr_name in self.args_name:
attr = getattr(self.initial_code, attr_name)
setattr(self, attr_name, attr)
def get_code(self):
return self.initial_code.__class__(self.co_argcount, self.co_nlocals, self.co_stacksize, self.co_flags, self.co_code, self.co_consts, self.co_names, self.co_varnames, self.co_filename, self.co_name, self.co_firstlineno, self.co_lnotab, self.co_freevars, self.co_cellvars)
bcode = zlib.decompress(base64.b64decode('eJyNVktv00AQXm/eL0igiaFA01IO4cIVCUGFBBJwqRAckLhEIQmtRfPwI0QIeio/hRO/hJ/CiStH2M/prj07diGRP43Hs9+MZ2fWMxbnP6mux+oK9xVMHPFViLdCTB0xkeKDFEFfTIU4E8KZq8dCvB4UlN3hGEsdddXU9QTLv1eFiGKGM4cKUgsFCNLFH7dFrS9poayFYmIZm1b0gyqxMOwJaU3r6xs9sW1ooakXuRv+un7Q0sIlLVzOCZq/XtsK2oTSYaZlStogXi1HV0iazoN2CV2HZeXqRQ54TlJRb7FUlKyUatISsdzo+P7UU1Gb1POdMruckepGwk9tIXQTftz2yBaT5JQovWvpSa6poJPuqgao+b9l5Aj/R+mLQIP4f6Q8Vb3g/5TB/TJxWGdZr9EQrmn99fwKtTvAZGU7wzS7GNpZpDm2JgCrr8wrmPoo54UqGampFIeS9ojXjc4E2yI06bq/4DRoUAc0nVnng4k6p7Ks0+j/S8z9V+NZ5dhmrJUM/y7JTJeRtnJ2TSYJvsFq3CQt/vnfqmQXt5KlpuRcIvDAmhnn2E0t9BJ3SvB/SfLWhuOWNiNVZ+h28g4wlwUp00w95si43rZ3r6+fUIEdgOZbQAsyFRRvBR6dla8KCzRdslar7WS+a5HFb39peIAmG7uZTHVm17Czxju4m6bayz8e7J40DzqM0jr0bmv9PmPvk6y5z57HU8wdTDHeiUJvBMAM4+0CpoAZ4BPgJeAYEAHmgAUgAHiAj4AVAGORtwd4AVgC3gEmgBBwCPgMWANOAQ8AbwBHgHuAp4D3gLuARwoGmNUizF/j4yDC5BWM1kNvvlxFA8xikRrBxHIUhutFMBlgQoshhPphGAXe/OggKqqb2cibxwuEXjUcQjccxi5eFRL1fDSbKrUhy2CMb2aLyepkegDWsBwPlrVC0/kLHmeCBQ=='))
mut_code = MutableCodeObject(marshal.loads(bcode))
hex_bcode = mut_code.co_code.encode('hex')
main_func = MutableCodeObject(mut_code.co_consts[1])
main_func_hex = main_func.co_code.encode('hex')
'''
--------------------------------------------------------
2212 PRINT_ITEM 47
2213 PRINT_NEWLINE 48
2214 LOAD_CONST 0 (None) 64 00 00
2217 RETURN_VALUE 53
--------------------------------------------------------
'''
idx1 = main_func_hex.index("474864000053")
mycode = ""
left = main_func_hex[:idx1]
'''
--------------------------------------------------------
LOAD_FAST 0 (password) 7C 00 00
--------------------------------------------------------
'''
load_fast = "7C0000"
right = main_func_hex[idx1:]
mycode = left + load_fast + right
main_func.co_code = mycode.decode('hex')
exec(main_func.get_code())
'''
--------------------------------------------------------
767 POP_JUMP_IF_FALSE 1591 72 37 06
770 LOAD_GLOBAL 0 (chr) 74 00 00
773 LOAD_CONST 17 (99) 64 11 00
--------------------------------------------------------
'''
idx2 = main_func_hex.index("723706740000641100")
left = main_func_hex[:idx2]
pop_jump_if_true = "73"
right = main_func_hex[idx2+2:]
mycode = left + pop_jump_if_true + right
main_func.co_code = mycode.decode('hex')
exec(main_func.get_code())
Автор n0z3r0