Last weekend I played in Hack.lu CTF with Maple Bacon, where we placed 24th out of 174 teams. For once, I decided to look at a reversing challenge and ended up solving it!
For a more concise version, check out this post.
Pycoin [146]
A friend gave me this and he says he can not reverse this… but this is just python?
We’re given a Python bytecode file (pycoin.pyc) and asked to reverse it.
Running python3 pycoin.pyc
, we’re greeted with
please supply a valid key:
Supposedly, entering anything other than the valid key/flag leaves us with the output of “invalid key”.
What are .pyc files?
The typical implementation of Python we use is CPython — it compiles python source code into lower-level Python machine instructions that look something like this:
L. 1 0 JUMP_FORWARD 4 'to 4'
2 LOAD_GLOBAL 99 99
4_0 COME_FROM 112 '112'
4_1 COME_FROM 0 '0'
4 LOAD_CONST 0
6 LOAD_CONST ('md5',)
8 IMPORT_NAME hashlib
10 IMPORT_FROM md5
The main point of having bytecode (.pyc) files is to reduce compilation time when you reuse the same files without changes.
After a bit of searching around, I found uncompyle6: a decompiler that translates Python bytecode back into source code. Running pip install uncompyle6
followed by uncompyle6 pycoin.pyc
successfully decompiles the bytecode:
# uncompyle6 version 3.7.4
# Python bytecode 3.8 (3413)
# Decompiled from: Python 3.8.10 (default, Jun 2 2021, 10:49:15)
# [GCC 9.4.0]
# Embedded file name: challenge_generated.py
# Compiled at: 2021-10-27 14:40:15
# Size of source mod 2**32: 2836 bytes
import marshal
marshalled = b'\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\xf3\xf0\x01\x00\x00n\x02tcd\x00d\x01l\x00m\x01Z\x01\x01\x00e\x02e\x03d\x02\x83\x01\x83\x01\xa0\x04\xa1\x00Z\x05e\x06e\x05\x83\x01d\x03k\x02\x90\x01o\xcee\x05d\x00\x19\x00d\x04k\x02\x90\x01o\xcee\x05d\x05\x19\x00e\x05d\x00\x19\x00d\x06\x17\x00k\x02\x90\x01o\xcee\x05d\x07\x19\x00e\x05d\x05\x19\x00e\x05d\x00\x19\x00\x18\x00d\x08n\n]\x02n\x02n\x02r\x04q\x04\x17\x00k\x02\x90\x01o\xcee\x05d\t\x19\x00d\nk\x02\x90\x01o\xcee\x05d\x0b\x19\x00e\x05d\x0c\x19\x00d\t\x14\x00d\r\x18\x00k\x02\x90\x01o\xcee\x05d\x0e\x19\x00e\x07e\x05\x83\x01d\x0f\x18\x00k\x02\x90\x01o\xcee\x05d\x06\x19\x00e\x05d\x10\x19\x00\x17\x00e\x05d\x11\x19\x00\x17\x00d\x12k\x02\x90\x01o\xcee\x08e\te\x05d\x10\x19\x00\x83\x01d\x07\x14\x00\x83\x01d\x05\x17\x00e\x05d\x13\x19\x00k\x02\x90\x01o\xcee\x05d\x14\x19\x00d\x15\x16\x00d\x03k\x02\x90\x01o\xcee\x05d\x13\x19\x00e\x05d\x14\x19\x00d\x07\x14\x00k\x02\x90\x01o\xcee\x01e\x05d\x11\x19\x00d\x16\x14\x00\x83\x01\xa0\n\xa1\x00d\x00\x19\x00d\x05\x18\x00e\x05d\t\x19\x00k\x02\x02\x00\x02\x00\x90\x01o\xcee\x05d\x0c\x19\x00d\x17k\x02\x90\x01o\xcee\x05d\x18\x19\x00e\x05d\x19\x19\x00d\x07\x1b\x00d\x07\x18\x00k\x02\x90\x01o\xcee\x05d\x1a\x19\x00e\x05d\x11\x19\x00e\x05d\x14\x19\x00\x14\x00d\x1b\x16\x00d\x07\x14\x00d\x05\x18\x00k\x02\x90\x01o\xcee\x05d\x19\x19\x00e\x05d\x18\x19\x00e\x05d\x13\x19\x00A\x00e\x05d\x1c\x19\x00A\x00d\t\x14\x00d\x1d\x18\x00k\x02\x90\x01o\xcee\x05d\x1c\x19\x00d\x1ek\x02Z\x0be\x0ce\x0b\x90\x01r\xe6d\x1fe\x05\xa0\r\xa1\x00\x9b\x00\x9d\x02n\x02d \x83\x01\x01\x00d!S\x00)"\xe9\x00\x00\x00\x00)\x01\xda\x03md5z\x1aplease supply a valid key:\xe9\x10\x00\x00\x00\xe9f\x00\x00\x00\xe9\x01\x00\x00\x00\xe9\x06\x00\x00\x00\xe9\x02\x00\x00\x00\xe9[\x00\x00\x00\xe9\x03\x00\x00\x00\xe9g\x00\x00\x00\xe9\x04\x00\x00\x00\xe9\x0b\x00\x00\x00\xe9*\x00\x00\x00\xe9\x05\x00\x00\x00i*\x05\x00\x00\xe9\x07\x00\x00\x00\xe9\n\x00\x00\x00i\x04\x01\x00\x00\xe9\t\x00\x00\x00\xe9\x08\x00\x00\x00\xe9\x11\x00\x00\x00\xf3\x01\x00\x00\x00a\xe97\x00\x00\x00\xe9\x0c\x00\x00\x00\xe9\x0e\x00\x00\x00\xe9\r\x00\x00\x00\xe9 \x00\x00\x00\xe9\x0f\x00\x00\x00\xe9\x17\x00\x00\x00\xe9}\x00\x00\x00z\x0bvalid key! z\x0einvalid key :(N)\x0eZ\x07hashlibr\x03\x00\x00\x00\xda\x03str\xda\x05input\xda\x06encode\xda\x01k\xda\x03len\xda\x03sum\xda\x03int\xda\x03chr\xda\x06digestZ\x07correct\xda\x05print\xda\x06decode\xa9\x00r)\x00\x00\x00r)\x00\x00\x00\xfa\r<disassembly>\xda\x08<module>\x01\x00\x00\x00sF\x00\x00\x00\x0c\x02\x10\x03\x0e\x01\n\xff\x04\x02\x12\xfe\x04\x03\x1a\xfd\x04\x04\n\xfc\x04\x05\x16\xfb\x04\x06\x12\xfa\x04\x07\x1a\xf9\x04\x08\x1e\xf8\x04\t\x0e\xf7\x04\n\x12\xf6\x04\x0b"\xf5\x04\x0c\n\xf4\x04\r\x16\xf3\x04\x0e"\xf2\x04\x0f&\xf1\x04\x10\n\xee\x02\x15'
exec(marshal.loads(marshalled))
# okay decompiling pycoin.pyc
Running this file in Python produces the same prompt asking the user for a key, so at least we’re doing something right.
So… what does marshal do?
After looking more into the marshal module, I realized it was a way of serializing Python objects (converting them into a binary format) — however, the documentation says that ‘marshal exists mainly to support reading and writing the “pseudo-compiled” code for Python modules of .pyc files’. A similar (and more common) module for serialization is pickle.
Notably, the function marshal.loads
converts the serialized bytes-like object to a Python code object, which we can then analyze with other libraries.
For example, if we want to print out the constants of the marshalled object, we can write:
import marshal
marshalled = b'\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\xf3\xf0\x01\x00\x00n\x02tcd\x00d\x01l\x00m\x01Z\x01\x01\x00e\x02e\x03d\x02\x83\x01\x83\x01\xa0\x04\xa1\x00Z\x05e\x06e\x05\x83\x01d\x03k\x02\x90\x01o\xcee\x05d\x00\x19\x00d\x04k\x02\x90\x01o\xcee\x05d\x05\x19\x00e\x05d\x00\x19\x00d\x06\x17\x00k\x02\x90\x01o\xcee\x05d\x07\x19\x00e\x05d\x05\x19\x00e\x05d\x00\x19\x00\x18\x00d\x08n\n]\x02n\x02n\x02r\x04q\x04\x17\x00k\x02\x90\x01o\xcee\x05d\t\x19\x00d\nk\x02\x90\x01o\xcee\x05d\x0b\x19\x00e\x05d\x0c\x19\x00d\t\x14\x00d\r\x18\x00k\x02\x90\x01o\xcee\x05d\x0e\x19\x00e\x07e\x05\x83\x01d\x0f\x18\x00k\x02\x90\x01o\xcee\x05d\x06\x19\x00e\x05d\x10\x19\x00\x17\x00e\x05d\x11\x19\x00\x17\x00d\x12k\x02\x90\x01o\xcee\x08e\te\x05d\x10\x19\x00\x83\x01d\x07\x14\x00\x83\x01d\x05\x17\x00e\x05d\x13\x19\x00k\x02\x90\x01o\xcee\x05d\x14\x19\x00d\x15\x16\x00d\x03k\x02\x90\x01o\xcee\x05d\x13\x19\x00e\x05d\x14\x19\x00d\x07\x14\x00k\x02\x90\x01o\xcee\x01e\x05d\x11\x19\x00d\x16\x14\x00\x83\x01\xa0\n\xa1\x00d\x00\x19\x00d\x05\x18\x00e\x05d\t\x19\x00k\x02\x02\x00\x02\x00\x90\x01o\xcee\x05d\x0c\x19\x00d\x17k\x02\x90\x01o\xcee\x05d\x18\x19\x00e\x05d\x19\x19\x00d\x07\x1b\x00d\x07\x18\x00k\x02\x90\x01o\xcee\x05d\x1a\x19\x00e\x05d\x11\x19\x00e\x05d\x14\x19\x00\x14\x00d\x1b\x16\x00d\x07\x14\x00d\x05\x18\x00k\x02\x90\x01o\xcee\x05d\x19\x19\x00e\x05d\x18\x19\x00e\x05d\x13\x19\x00A\x00e\x05d\x1c\x19\x00A\x00d\t\x14\x00d\x1d\x18\x00k\x02\x90\x01o\xcee\x05d\x1c\x19\x00d\x1ek\x02Z\x0be\x0ce\x0b\x90\x01r\xe6d\x1fe\x05\xa0\r\xa1\x00\x9b\x00\x9d\x02n\x02d \x83\x01\x01\x00d!S\x00)"\xe9\x00\x00\x00\x00)\x01\xda\x03md5z\x1aplease supply a valid key:\xe9\x10\x00\x00\x00\xe9f\x00\x00\x00\xe9\x01\x00\x00\x00\xe9\x06\x00\x00\x00\xe9\x02\x00\x00\x00\xe9[\x00\x00\x00\xe9\x03\x00\x00\x00\xe9g\x00\x00\x00\xe9\x04\x00\x00\x00\xe9\x0b\x00\x00\x00\xe9*\x00\x00\x00\xe9\x05\x00\x00\x00i*\x05\x00\x00\xe9\x07\x00\x00\x00\xe9\n\x00\x00\x00i\x04\x01\x00\x00\xe9\t\x00\x00\x00\xe9\x08\x00\x00\x00\xe9\x11\x00\x00\x00\xf3\x01\x00\x00\x00a\xe97\x00\x00\x00\xe9\x0c\x00\x00\x00\xe9\x0e\x00\x00\x00\xe9\r\x00\x00\x00\xe9 \x00\x00\x00\xe9\x0f\x00\x00\x00\xe9\x17\x00\x00\x00\xe9}\x00\x00\x00z\x0bvalid key! z\x0einvalid key :(N)\x0eZ\x07hashlibr\x03\x00\x00\x00\xda\x03str\xda\x05input\xda\x06encode\xda\x01k\xda\x03len\xda\x03sum\xda\x03int\xda\x03chr\xda\x06digestZ\x07correct\xda\x05print\xda\x06decode\xa9\x00r)\x00\x00\x00r)\x00\x00\x00\xfa\r<disassembly>\xda\x08<module>\x01\x00\x00\x00sF\x00\x00\x00\x0c\x02\x10\x03\x0e\x01\n\xff\x04\x02\x12\xfe\x04\x03\x1a\xfd\x04\x04\n\xfc\x04\x05\x16\xfb\x04\x06\x12\xfa\x04\x07\x1a\xf9\x04\x08\x1e\xf8\x04\t\x0e\xf7\x04\n\x12\xf6\x04\x0b"\xf5\x04\x0c\n\xf4\x04\r\x16\xf3\x04\x0e"\xf2\x04\x0f&\xf1\x04\x10\n\xee\x02\x15'
code = marshal.loads(marshalled)
print(code.co_consts)
This outputs: (0, ('md5',), 'please supply a valid key:', 16, 102, 1, 6, 2, 91, 3, 103, 4, 11, 42, 5, 1322, 7, 10, 260, 9, 8, 17, b'a', 55, 12, 14, 13, 32, 15, 23, 125, 'valid key! ', 'invalid key :(', None)
Now, we can again use the uncompyle6 library to decompile the code object:
from uncompyle6.main import decompile
import marshal
marshalled = b'\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\xf3\xf0\x01\x00\x00n\x02tcd\x00d\x01l\x00m\x01Z\x01\x01\x00e\x02e\x03d\x02\x83\x01\x83\x01\xa0\x04\xa1\x00Z\x05e\x06e\x05\x83\x01d\x03k\x02\x90\x01o\xcee\x05d\x00\x19\x00d\x04k\x02\x90\x01o\xcee\x05d\x05\x19\x00e\x05d\x00\x19\x00d\x06\x17\x00k\x02\x90\x01o\xcee\x05d\x07\x19\x00e\x05d\x05\x19\x00e\x05d\x00\x19\x00\x18\x00d\x08n\n]\x02n\x02n\x02r\x04q\x04\x17\x00k\x02\x90\x01o\xcee\x05d\t\x19\x00d\nk\x02\x90\x01o\xcee\x05d\x0b\x19\x00e\x05d\x0c\x19\x00d\t\x14\x00d\r\x18\x00k\x02\x90\x01o\xcee\x05d\x0e\x19\x00e\x07e\x05\x83\x01d\x0f\x18\x00k\x02\x90\x01o\xcee\x05d\x06\x19\x00e\x05d\x10\x19\x00\x17\x00e\x05d\x11\x19\x00\x17\x00d\x12k\x02\x90\x01o\xcee\x08e\te\x05d\x10\x19\x00\x83\x01d\x07\x14\x00\x83\x01d\x05\x17\x00e\x05d\x13\x19\x00k\x02\x90\x01o\xcee\x05d\x14\x19\x00d\x15\x16\x00d\x03k\x02\x90\x01o\xcee\x05d\x13\x19\x00e\x05d\x14\x19\x00d\x07\x14\x00k\x02\x90\x01o\xcee\x01e\x05d\x11\x19\x00d\x16\x14\x00\x83\x01\xa0\n\xa1\x00d\x00\x19\x00d\x05\x18\x00e\x05d\t\x19\x00k\x02\x02\x00\x02\x00\x90\x01o\xcee\x05d\x0c\x19\x00d\x17k\x02\x90\x01o\xcee\x05d\x18\x19\x00e\x05d\x19\x19\x00d\x07\x1b\x00d\x07\x18\x00k\x02\x90\x01o\xcee\x05d\x1a\x19\x00e\x05d\x11\x19\x00e\x05d\x14\x19\x00\x14\x00d\x1b\x16\x00d\x07\x14\x00d\x05\x18\x00k\x02\x90\x01o\xcee\x05d\x19\x19\x00e\x05d\x18\x19\x00e\x05d\x13\x19\x00A\x00e\x05d\x1c\x19\x00A\x00d\t\x14\x00d\x1d\x18\x00k\x02\x90\x01o\xcee\x05d\x1c\x19\x00d\x1ek\x02Z\x0be\x0ce\x0b\x90\x01r\xe6d\x1fe\x05\xa0\r\xa1\x00\x9b\x00\x9d\x02n\x02d \x83\x01\x01\x00d!S\x00)"\xe9\x00\x00\x00\x00)\x01\xda\x03md5z\x1aplease supply a valid key:\xe9\x10\x00\x00\x00\xe9f\x00\x00\x00\xe9\x01\x00\x00\x00\xe9\x06\x00\x00\x00\xe9\x02\x00\x00\x00\xe9[\x00\x00\x00\xe9\x03\x00\x00\x00\xe9g\x00\x00\x00\xe9\x04\x00\x00\x00\xe9\x0b\x00\x00\x00\xe9*\x00\x00\x00\xe9\x05\x00\x00\x00i*\x05\x00\x00\xe9\x07\x00\x00\x00\xe9\n\x00\x00\x00i\x04\x01\x00\x00\xe9\t\x00\x00\x00\xe9\x08\x00\x00\x00\xe9\x11\x00\x00\x00\xf3\x01\x00\x00\x00a\xe97\x00\x00\x00\xe9\x0c\x00\x00\x00\xe9\x0e\x00\x00\x00\xe9\r\x00\x00\x00\xe9 \x00\x00\x00\xe9\x0f\x00\x00\x00\xe9\x17\x00\x00\x00\xe9}\x00\x00\x00z\x0bvalid key! z\x0einvalid key :(N)\x0eZ\x07hashlibr\x03\x00\x00\x00\xda\x03str\xda\x05input\xda\x06encode\xda\x01k\xda\x03len\xda\x03sum\xda\x03int\xda\x03chr\xda\x06digestZ\x07correct\xda\x05print\xda\x06decode\xa9\x00r)\x00\x00\x00r)\x00\x00\x00\xfa\r<disassembly>\xda\x08<module>\x01\x00\x00\x00sF\x00\x00\x00\x0c\x02\x10\x03\x0e\x01\n\xff\x04\x02\x12\xfe\x04\x03\x1a\xfd\x04\x04\n\xfc\x04\x05\x16\xfb\x04\x06\x12\xfa\x04\x07\x1a\xf9\x04\x08\x1e\xf8\x04\t\x0e\xf7\x04\n\x12\xf6\x04\x0b"\xf5\x04\x0c\n\xf4\x04\r\x16\xf3\x04\x0e"\xf2\x04\x0f&\xf1\x04\x10\n\xee\x02\x15'
code = marshal.loads(marshalled)
print(decompile(3.8, code))
Aside from a parse error, we receive a full 285 lines of Python assembly:
L. 1 0 JUMP_FORWARD 4 'to 4'
2 LOAD_GLOBAL 99 99
4_0 COME_FROM 112 '112'
4_1 COME_FROM 0 '0'
4 LOAD_CONST 0
6 LOAD_CONST ('md5',)
8 IMPORT_NAME hashlib
10 IMPORT_FROM md5
L. 3 12 STORE_NAME md5
14 POP_TOP
16 LOAD_NAME str
18 LOAD_NAME input
20 LOAD_STR 'please supply a valid key:'
22 CALL_FUNCTION_1 1 ''
24 CALL_FUNCTION_1 1 ''
26 LOAD_METHOD encode
L. 6 28 CALL_METHOD_0 0 ''
30 STORE_NAME k
32 LOAD_NAME len
34 LOAD_NAME k
36 CALL_FUNCTION_1 1 ''
38 LOAD_CONST 16
40 COMPARE_OP ==
L. 7 42_44 JUMP_IF_FALSE_OR_POP 462 'to 462'
46 LOAD_NAME k
48 LOAD_CONST 0
50 BINARY_SUBSCR
L. 6 52 LOAD_CONST 102
54 COMPARE_OP ==
L. 8 56_58 JUMP_IF_FALSE_OR_POP 462 'to 462'
60 LOAD_NAME k
62 LOAD_CONST 1
64 BINARY_SUBSCR
66 LOAD_NAME k
68 LOAD_CONST 0
70 BINARY_SUBSCR
72 LOAD_CONST 6
L. 6 74 BINARY_ADD
76 COMPARE_OP ==
L. 9 78_80 JUMP_IF_FALSE_OR_POP 462 'to 462'
82 LOAD_NAME k
84 LOAD_CONST 2
86 BINARY_SUBSCR
88 LOAD_NAME k
90 LOAD_CONST 1
92 BINARY_SUBSCR
94 LOAD_NAME k
96 LOAD_CONST 0
98 BINARY_SUBSCR
100 BINARY_SUBTRACT
102 LOAD_CONST 91
L. 6 104 BREAK_LOOP 116 'to 116'
106 FOR_ITER 110 'to 110'
L. 10 108 BREAK_LOOP 112 'to 112'
110 JUMP_FORWARD 114 'to 114'
112_0 COME_FROM 108 '108'
112 POP_JUMP_IF_FALSE 4 'to 4'
114_0 COME_FROM 110 '110'
114 JUMP_BACK 4 'to 4'
116_0 COME_FROM 104 '104'
116 BINARY_ADD
L. 6 118 COMPARE_OP ==
120_122 JUMP_IF_FALSE_OR_POP 462 'to 462'
124 LOAD_NAME k
126 LOAD_CONST 3
128 BINARY_SUBSCR
130 LOAD_CONST 103
132 COMPARE_OP ==
134_136 JUMP_IF_FALSE_OR_POP 462 'to 462'
138 LOAD_NAME k
140 LOAD_CONST 4
142 BINARY_SUBSCR
L. 6 144 LOAD_NAME k
146 LOAD_CONST 11
L. 12 148 BINARY_SUBSCR
150 LOAD_CONST 3
152 BINARY_MULTIPLY
154 LOAD_CONST 42
156 BINARY_SUBTRACT
158 COMPARE_OP ==
160_162 JUMP_IF_FALSE_OR_POP 462 'to 462'
164 LOAD_NAME k
L. 6 166 LOAD_CONST 5
168 BINARY_SUBSCR
L. 13 170 LOAD_NAME sum
172 LOAD_NAME k
174 CALL_FUNCTION_1 1 ''
176 LOAD_CONST 1322
178 BINARY_SUBTRACT
180 COMPARE_OP ==
182_184 JUMP_IF_FALSE_OR_POP 462 'to 462'
186 LOAD_NAME k
188 LOAD_CONST 6
190 BINARY_SUBSCR
192 LOAD_NAME k
194 LOAD_CONST 7
L. 6 196 BINARY_SUBSCR
198 BINARY_ADD
L. 14 200 LOAD_NAME k
202 LOAD_CONST 10
204 BINARY_SUBSCR
206 BINARY_ADD
208 LOAD_CONST 260
210 COMPARE_OP ==
212_214 JUMP_IF_FALSE_OR_POP 462 'to 462'
216 LOAD_NAME int
218 LOAD_NAME chr
220 LOAD_NAME k
222 LOAD_CONST 7
224 BINARY_SUBSCR
226 CALL_FUNCTION_1 1 ''
228 LOAD_CONST 2
L. 6 230 BINARY_MULTIPLY
232 CALL_FUNCTION_1 1 ''
L. 15 234 LOAD_CONST 1
236 BINARY_ADD
238 LOAD_NAME k
240 LOAD_CONST 9
242 BINARY_SUBSCR
244 COMPARE_OP ==
246_248 JUMP_IF_FALSE_OR_POP 462 'to 462'
250 LOAD_NAME k
L. 16 252 LOAD_CONST 8
254 BINARY_SUBSCR
256 LOAD_CONST 17
258 BINARY_MODULO
260 LOAD_CONST 16
262 COMPARE_OP ==
264_266 JUMP_IF_FALSE_OR_POP 462 'to 462'
268 LOAD_NAME k
L. 6 270 LOAD_CONST 9
272 BINARY_SUBSCR
L. 17 274 LOAD_NAME k
276 LOAD_CONST 8
278 BINARY_SUBSCR
280 LOAD_CONST 2
282 BINARY_MULTIPLY
284 COMPARE_OP ==
286_288 JUMP_IF_FALSE_OR_POP 462 'to 462'
290 LOAD_NAME md5
292 LOAD_NAME k
294 LOAD_CONST 10
296 BINARY_SUBSCR
298 LOAD_CONST b'a'
300 BINARY_MULTIPLY
302 CALL_FUNCTION_1 1 ''
304 LOAD_METHOD digest
306 CALL_METHOD_0 0 ''
L. 6 308 LOAD_CONST 0
310 BINARY_SUBSCR
L. 18 312 LOAD_CONST 1
314 BINARY_SUBTRACT
316 LOAD_NAME k
318 LOAD_CONST 3
320 BINARY_SUBSCR
L. 6 322 COMPARE_OP ==
324 ROT_TWO
L. 19 326 ROT_TWO
328_330 JUMP_IF_FALSE_OR_POP 462 'to 462'
332 LOAD_NAME k
334 LOAD_CONST 11
336 BINARY_SUBSCR
338 LOAD_CONST 55
340 COMPARE_OP ==
342_344 JUMP_IF_FALSE_OR_POP 462 'to 462'
346 LOAD_NAME k
L. 6 348 LOAD_CONST 12
350 BINARY_SUBSCR
L. 20 352 LOAD_NAME k
354 LOAD_CONST 14
356 BINARY_SUBSCR
358 LOAD_CONST 2
360 BINARY_TRUE_DIVIDE
362 LOAD_CONST 2
364 BINARY_SUBTRACT
366 COMPARE_OP ==
368_370 JUMP_IF_FALSE_OR_POP 462 'to 462'
372 LOAD_NAME k
374 LOAD_CONST 13
376 BINARY_SUBSCR
378 LOAD_NAME k
380 LOAD_CONST 10
382 BINARY_SUBSCR
384 LOAD_NAME k
L. 6 386 LOAD_CONST 8
388 BINARY_SUBSCR
L. 21 390 BINARY_MULTIPLY
392 LOAD_CONST 32
394 BINARY_MODULO
396 LOAD_CONST 2
398 BINARY_MULTIPLY
400 LOAD_CONST 1
402 BINARY_SUBTRACT
404 COMPARE_OP ==
406_408 JUMP_IF_FALSE_OR_POP 462 'to 462'
410 LOAD_NAME k
412 LOAD_CONST 14
414 BINARY_SUBSCR
416 LOAD_NAME k
418 LOAD_CONST 12
420 BINARY_SUBSCR
422 LOAD_NAME k
424 LOAD_CONST 9
426 BINARY_SUBSCR
L. 6 428 BINARY_XOR
430 LOAD_NAME k
L. 22 432 LOAD_CONST 15
434 BINARY_SUBSCR
436 BINARY_XOR
438 LOAD_CONST 3
440 BINARY_MULTIPLY
L. 4 442 LOAD_CONST 23
L. 25 444 BINARY_SUBTRACT
446 COMPARE_OP ==
448_450 JUMP_IF_FALSE_OR_POP 462 'to 462'
452 LOAD_NAME k
454 LOAD_CONST 15
456 BINARY_SUBSCR
458 LOAD_CONST 125
460 COMPARE_OP ==
462_0 COME_FROM 448 '448'
462_1 COME_FROM 406 '406'
462_2 COME_FROM 368 '368'
462_3 COME_FROM 342 '342'
462_4 COME_FROM 328 '328'
462_5 COME_FROM 286 '286'
462_6 COME_FROM 264 '264'
462_7 COME_FROM 246 '246'
462_8 COME_FROM 212 '212'
462_9 COME_FROM 182 '182'
462_10 COME_FROM 160 '160'
462_11 COME_FROM 134 '134'
462_12 COME_FROM 120 '120'
462_13 COME_FROM 78 '78'
462_14 COME_FROM 56 '56'
462_15 COME_FROM 42 '42'
462 STORE_NAME correct
464 LOAD_NAME print
466 LOAD_NAME correct
468_470 POP_JUMP_IF_FALSE 486 'to 486'
472 LOAD_STR 'valid key! '
474 LOAD_NAME k
476 LOAD_METHOD decode
478 CALL_METHOD_0 0 ''
480 FORMAT_VALUE 0 ''
482 BUILD_STRING_2 2
484 JUMP_FORWARD 488 'to 488'
486_0 COME_FROM 468 '468'
486 LOAD_STR 'invalid key :('
488_0 COME_FROM 484 '484'
488 CALL_FUNCTION_1 1 ''
490 POP_TOP
Analyzing the bytecode instructions
At first glance, the code seems to perform checks on specific characters of the key, relying on simple operations such as BINARY_ADD and BINARY_XOR. Luckily, Python has a documentation detailing each of the instructions.
After loading the hashlib library, the instructions perform multiple comparisons, each jumping to
JUMP_IF_FALSE_OR_POP 462 ‘to 462’
if the comparison produces false. Looking at the last section of the code, we can infer that this likely terminates the program and produces “invalid key :(”. So, we need to find a string that satisfies all of these comparisons. There are too many to go into detail, but some examples include:
46 LOAD_NAME k
48 LOAD_CONST 0
50 BINARY_SUBSCR # k[0]
L. 6 52 LOAD_CONST 102
54 COMPARE_OP == # k[0] == 102
L. 8 56_58 JUMP_IF_FALSE_OR_POP 462 'to 462'
Which performs a simple check to see if the first character of the string (k) is 102 (‘f’).
And
290 LOAD_NAME md5
292 LOAD_NAME k
294 LOAD_CONST 10
296 BINARY_SUBSCR # k[10]
298 LOAD_CONST b'a'
300 BINARY_MULTIPLY # k[10] * b'a'
302 CALL_FUNCTION_1 1 ''
304 LOAD_METHOD digest # md5(k[10] * b'a')
306 CALL_METHOD_0 0 ''
L. 6 308 LOAD_CONST 0
310 BINARY_SUBSCR # md5(k[10] * b'a')[0]
L. 18 312 LOAD_CONST 1
314 BINARY_SUBTRACT # md5(k[10] * b'a')[0] - 1
316 LOAD_NAME k
318 LOAD_CONST 3
320 BINARY_SUBSCR # k[3]
L. 6 322 COMPARE_OP == # md5(k[10] * b'a')[0] - 1 == k[3]
324 ROT_TWO
L. 19 326 ROT_TWO
328_330 JUMP_IF_FALSE_OR_POP 462 'to 462'
Which calculates the first byte of the md5 hash for (k[10] * ‘a’), subtracts 1, and compares it with k[3]
After going through each of these comparisons, we can compile a list of the different checks:
k[0] = 102
k[0] + 6 = k[1]
k[2] = k[1] - k[0] + 91
k[3] = 103
k[4] = k[11] * 3 - 42
k[5] = sum_of_chars(k) - 1332
k[6] + k[7] + k[10] = 260
int(chr(k[7]) * 2) + 1 = k[9]
k[8] % 17 = 16
k[9] = 2 * k[8]
md5(k[10] * b'a')[0] - 1 = k[3]
k[11] = 55
k[12] = k[14]/2 - 2
k[13] = 2 * [(k[10] * k[8]) % 32] - 1
k[14] = (k[12] ^ k[9] ^ k[15]) * 3 - 23
Notice that there are 15 equations and 15 characters to solve for, which works out perfectly! We can find the first few characters with some simple arithmetic. Similarly, we can solve for k[7], k[8], and k[9] since there are only a few possibilities. Finally, with a little brute force, we can uncover the rest of the characters.
Flag: flag{5f92de703d}
whatthehecc [198]
Go hecc it! nc flu.xxx 20085
#!/usr/bin/env python3
import sys
import shlex
import subprocess
from Cryptodome.PublicKey import ECC
from Cryptodome.Hash import SHA3_256
from Cryptodome.Math.Numbers import Integer
import time
# util
def run_cmd(cmd):
try:
args = shlex.split(cmd)
return subprocess.check_output(args).decode('utf-8')
except Exception as ex:
return str(ex)
def read_message():
return sys.stdin.readline()
def send_message(message):
sys.stdout.write('### {0}\r\n>'.format(message))
sys.stdout.flush()
# crypto stuff
def hash(msg):
h_obj = SHA3_256.new()
h_obj.update(msg.encode())
return Integer.from_bytes(h_obj.digest())
def setup(curve):
key = ECC.generate(curve=curve)
return key
def blind(msg, pub):
r = pub.pointQ * hash(msg)
return r
def sign(r, key):
r_prime = r * key.d.inverse(key._curve.order)
date = int(time.time())
nonce = Integer.random_range(min_inclusive=1,max_exclusive=key._curve.order)
z = f'{nonce}||{date}'
R = r_prime + (key._curve.G * hash(z))
s = (key.d - hash(z)) % key._curve.order
# return (R, s, z)
# we can not give away z or this is unsafe: x = s+h(z)
return R, s
def verify(msg, sig, pub):
R, s = sig
if s in [0,1,''] and s > 0:
return False
tmp1 = s * pub._curve.G
tmp2 = - pub.pointQ
tmp3 = tmp2 + R
return tmp1 + tmp3 == hash(msg) * pub._curve.G
## ok ok here we go
def main():
while True:
send_message('Enter your command:')
cmd = read_message().strip()
if cmd == 'sign':
send_message('Send cmd to sign:')
cmd = read_message().strip()
if(cmd in ['id', 'uname', 'ls', 'date']):
r = blind(cmd, pubkey)
sig = sign(r, key)
send_message(f'Here you go: {sig[0].x}|{sig[0].y}|{sig[1]}|{cmd}')
else:
send_message('Not allowed!')
elif cmd == 'run':
send_message('Send sig:')
sig = read_message().strip()
tmp = sig.split('|')
if len(tmp) == 4:
x = int(tmp[0])
y = int(tmp[1])
s = int(tmp[2])
c = tmp[3]
sig = (ECC.EccPoint(x, y, curve='P-256'), s)
if(verify(c, sig, pubkey)):
out = run_cmd(c)
send_message(out)
else:
send_message('Invalid sig!')
else:
send_message('Invalid amount of params!')
elif cmd == 'show':
send_message(pubkey)
elif cmd == 'help':
send_message('Commands: exit, help, show, run, sign')
elif cmd == 'exit':
send_message('Bye :) Have a nice day!')
break
else:
send_message('Invalid command!')
if __name__ == '__main__':
key = setup('P-256')
pubkey = key.public_key()
main()
We’re given a server that can sign certain messages with elliptic curves; since the flag is located at ./flag.txt, we need to forge a signature for cat flag
or a similar command.
Examining the sign and verify functions, we notice that they differ from the standard ECDSA signature algorithm.
Signing
We can begin by looking at how the signature and verification processes work. Here, I’ll use the notation:
- Q: public key
- d: private key
- G: generator
- H(msg): hash(msg)
The signature process starts with r = blind(cmd, pubkey)
, where cmd is limited to ‘id’, ‘uname’, ‘ls’, ‘date’. Looking at the blind function:
$$r = H(\text{cmd}) * Q$$
def sign(r, key):
r_prime = r * key.d.inverse(key._curve.order)
date = int(time.time())
nonce = Integer.random_range(min_inclusive=1,max_exclusive=key._curve.order)
z = f'{nonce}||{date}'
R = r_prime + (key._curve.G * hash(z))
...
The function then creates an r_prime variable and a random nonce. Writing R in terms of our previous expressions:
$$R = r' + G * H(z) = H(\text{cmd}) * Q * d^{-1} + G * H(z) = H(\text{cmd}) * G + G * H(z)$$
s = (key.d - hash(z)) % key._curve.order
Additionally, we have:
$$s = d - H(z)$$
Verifying
Now, we can look at how the verification algorithms works if we pass in:
$$(R, s) = (H(\text{cmd}) * G + G * H(z), d - H(z))$$
tmp1 = s * pub._curve.G
tmp2 = - pub.pointQ
tmp3 = tmp2 + R
Then, the values of the tmp variables are:
$$\begin{align}\text{tmp1} &= s * G = (d - H(z)) * G = d * G - G * H(z) = Q - G * H(z)\\ \text{tmp2} &= -Q\\ \text{tmp3} &= R - Q = H(\text{cmd}) * G + G * H(z) - Q\end{align}$$
return tmp1 + tmp3 == hash(msg) * pub._curve.G
Notice that when we add tmp1 and tmp3 to perform the final check, almost all of the variables cancel out:
$$\text{tmp1} + \text{tmp3} = [H(\text{cmd}) * G + G * H(z) - Q] + [Q - G * H(z)] = H(\text{cmd}) * G$$
Thus, the verification holds if we supply $R$ and $s$ that satisfy the equation above:
$$(R, s) = (H(\text{cmd}) * G + G * H(z), d - H(z))$$
Forging a signature
Initially, I had thought of recovering the private key $d$ from a signature since we have two unknowns ($H(z)$ and $d$) and two equations. Unfortunately, this turned out to be just as hard as the ECDLP, so I turned toward directly forging a signature instead.
Notice that we can rearrange the equation for $s$ to:
$$\begin{align}H(z) = d - s\end{align}$$
Substituting this into the expression for R gives:
$$R = H(\text{cmd}) * G + G * H(z) = H(\text{cmd}) * G + G * (d - s) = H(\text{cmd}) * G + Q - G * s$$
In other words, we’ve managed to get rid of the nonce ($H(z)$) and construct a (seemingly) valid signature of:
$$(R, s) = (H(\text{cmd}) * G + Q - G * s, s)$$
Now we can check if our forged signature passes the verification.
tmp1 = s * pub._curve.G
tmp2 = - pub.pointQ
tmp3 = tmp2 + R
The values of the tmp variables are:
$$\begin{align}\text{tmp1} &= s * G \\ \text{tmp2} &= -Q\\ \text{tmp3} &= R - Q = H(\text{cmd}) * G + Q - G * s - Q = H(\text{cmd}) * G - G * s\end{align}$$
And so,
$$\text{tmp1} + \text{tmp3} = s * G + H(\text{cmd}) * G - G * s = H(\text{cmd})$$
We’ve succesfully forged a signature for any $\text{cmd}$ we want!
A local proof of concept:
...
Q = pubkey.pointQ
G = key._curve.G
r = blind('ls', pubkey)
R1, s1 = sign(r, key)
R = hash('cat flag') * G + Q + s1 * (-G)
sig = (ECC.EccPoint(R.x, R.y, curve='P-256'), int(s1))
assert(verify('cat flag', sig, pubkey))
Flag: flag{d1d_you_f1nd_chakraborty_mehta}