Post

SpookyCheck

Challenge

  • CTF: Hack The Boo 2023
  • Name: SpookyCheck
  • Category: Reversing
  • Difficulty: Medium
  • Points: 350
  • Description: My new tool will check if your password is spooky enough for use during Halloween - but watch out for snakes…
  • Objective: Python .pyc Reversing

Files

Download: rev_spookycheck.zip

Writeup

The challenge file of check.pyc was identified as a byte-compiled Python file after analyzing it using the file utility.

1
2
$ file check.pyc
check.pyc: Byte-compiled Python module for CPython 3.11, timestamp-based, .py timestamp: Mon Sep  4 15:32:51 2023 UTC, .py size: 656 bytes

Analyzing the binary using strings, we can identify a SUP3RS3CR3TK3Y:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ strings check.pyc
SUP3RS3CR3TK3Ys
len)
check.py
<listcomp>z
transform.<locals>.<listcomp>
	enumerate
flags
	transformr
CHECKr
checkr
__main__u
 Welcome to SpookyCheck
 Enter your password for spooky evaluation
 Well done, you're spookier than most!
 Not spooky enough, please try again later
	bytearrayr
__name__
print
input
encode
<module>r

We can do dynamic analysis by importing the library and see what type of functions/variables are available in check.pyc.

1
2
3
4
5
6
python3
import check
>>> print(check.CHECK)
bytearray(b'\xe9\xef\xc0V\x8d\x8a\x05\xbe\x8ek\xd9yX\x8b\x89\xd3\x8c\xfa\xdexu\xbe\xdf1\xde\xb6\\')
>>> print(check.KEY)
b'SUP3RS3CR3TK3Y'

This file can be attempted to be decompiled to source-code using pycdc of Python Decompyle++. Unfortunately, it isn’t fully supported for python 3.11.

1
2
3
4
5
6
7
8
9
10
11
12
13
pycdc check.pyc
# Source Generated with Decompyle++
# File: check.pyc (Python 3.11)

KEY = b'SUP3RS3CR3TK3Y'
CHECK = bytearray(b'\xe9\xef\xc0V\x8d\x8a\x05\xbe\x8ek\xd9yX\x8b\x89\xd3\x8c\xfa\xdexu\xbe\xdf1\xde\xb6\\')

def transform(flag):
    return enumerate(flag)()


def check(flag):
Error decompyling check.pyc: vector::_M_range_check: __n (which is 2) >= this->size() (which is 2)

This file can be attempted to be disassembled to byte-code using pycdas of Python Decompyle++. This was successful.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
$ pycdas check.pyc
check.pyc (Python 3.11)
[Code]
    File Name: check.py
    Object Name: <module>
    Qualified Name: <module>
    Arg Count: 0
    Pos Only Arg Count: 0
    KW Only Arg Count: 0
    Stack Size: 4
    Flags: 0x00000000
    [Names]
        'KEY'
        'bytearray'
        'CHECK'
        'transform'
        'check'
        '__name__'
        'print'
        'input'
        'inp'
        'encode'
    [Locals+Names]
    [Constants]
        b'SUP3RS3CR3TK3Y'
        b'\xe9\xef\xc0V\x8d\x8a\x05\xbe\x8ek\xd9yX\x8b\x89\xd3\x8c\xfa\xdexu\xbe\xdf1\xde\xb6\\'
        [Code]
            File Name: check.py
            Object Name: transform
            Qualified Name: transform
            Arg Count: 1
            Pos Only Arg Count: 0
            KW Only Arg Count: 0
            Stack Size: 4
            Flags: 0x00000003 (CO_OPTIMIZED | CO_NEWLOCALS)
            [Names]
                'enumerate'
            [Locals+Names]
                'flag'
            [Constants]
                None
                [Code]
                    File Name: check.py
                    Object Name: <listcomp>
                    Qualified Name: transform.<locals>.<listcomp>
                    Arg Count: 1
                    Pos Only Arg Count: 0
                    KW Only Arg Count: 0
                    Stack Size: 8
                    Flags: 0x00000013 (CO_OPTIMIZED | CO_NEWLOCALS | CO_NESTED)
                    [Names]
                        'KEY'
                        'len'
                    [Locals+Names]
                        '.0'
                        'i'
                        'f'
                    [Constants]
                        24
                        255
                        74
                    [Disassembly]
                        0       RESUME                        0
                        2       BUILD_LIST                    0
                        4       LOAD_FAST                     0: .0
                        6       FOR_ITER                      54 (to 116)
                        8       UNPACK_SEQUENCE               2
                        12      STORE_FAST                    1: i
                        14      STORE_FAST                    2: f
                        16      LOAD_FAST                     2: f
                        18      LOAD_CONST                    0: 24
                        20      BINARY_OP                     0 (+)
                        24      LOAD_CONST                    1: 255
                        26      BINARY_OP                     1 (&)
                        30      LOAD_GLOBAL                   0: KEY
                        42      LOAD_FAST                     1: i
                        44      LOAD_GLOBAL                   3: NULL + len
                        56      LOAD_GLOBAL                   0: KEY
                        68      PRECALL                       1
                        72      CALL                          1
                        82      BINARY_OP                     6 (%)
                        86      BINARY_SUBSCR
                        96      BINARY_OP                     12 (^)
                        100     LOAD_CONST                    2: 74
                        102     BINARY_OP                     10 (-)
                        106     LOAD_CONST                    1: 255
                        108     BINARY_OP                     1 (&)
                        112     LIST_APPEND                   2
                        114     JUMP_BACKWARD                 55
                        116     RETURN_VALUE
            [Disassembly]
                0       RESUME                        0
                2       LOAD_CONST                    1: <CODE> <listcomp>
                4       MAKE_FUNCTION                 0
                6       LOAD_GLOBAL                   1: NULL + enumerate
                18      LOAD_FAST                     0: flag
                20      PRECALL                       1
                24      CALL                          1
                34      GET_ITER
                36      PRECALL                       0
                40      CALL                          0
                50      RETURN_VALUE
        [Code]
            File Name: check.py
            Object Name: check
            Qualified Name: check
            Arg Count: 1
            Pos Only Arg Count: 0
            KW Only Arg Count: 0
            Stack Size: 3
            Flags: 0x00000003 (CO_OPTIMIZED | CO_NEWLOCALS)
            [Names]
                'transform'
                'CHECK'
            [Locals+Names]
                'flag'
            [Constants]
                None
            [Disassembly]
                0       RESUME                        0
                2       LOAD_GLOBAL                   1: NULL + transform
                14      LOAD_FAST                     0: flag
                16      PRECALL                       1
                20      CALL                          1
                30      LOAD_GLOBAL                   2: CHECK
                42      COMPARE_OP                    2 (==)
                48      RETURN_VALUE
        '__main__'
        'πŸŽƒ Welcome to SpookyCheck πŸŽƒ'
        'πŸŽƒ Enter your password for spooky evaluation πŸŽƒ'
        'πŸ‘» '
        "πŸ¦‡ Well done, you're spookier than most! πŸ¦‡"
        'πŸ’€ Not spooky enough, please try again later πŸ’€'
        None
    [Disassembly]
        0       RESUME                        0
        2       LOAD_CONST                    0: b'SUP3RS3CR3TK3Y'
        4       STORE_NAME                    0: KEY
        6       PUSH_NULL
        8       LOAD_NAME                     1: bytearray
        10      LOAD_CONST                    1: b'\xe9\xef\xc0V\x8d\x8a\x05\xbe\x8ek\xd9yX\x8b\x89\xd3\x8c\xfa\xdexu\xbe\xdf1\xde\xb6\\'
        12      PRECALL                       1
        16      CALL                          1
        26      STORE_NAME                    2: CHECK
        28      LOAD_CONST                    2: <CODE> transform
        30      MAKE_FUNCTION                 0
        32      STORE_NAME                    3: transform
        34      LOAD_CONST                    3: <CODE> check
        36      MAKE_FUNCTION                 0
        38      STORE_NAME                    4: check
        40      LOAD_NAME                     5: __name__
        42      LOAD_CONST                    4: '__main__'
        44      COMPARE_OP                    2 (==)
        50      POP_JUMP_FORWARD_IF_FALSE     88 (to 228)
        52      PUSH_NULL
        54      LOAD_NAME                     6: print
        56      LOAD_CONST                    5: 'πŸŽƒ Welcome to SpookyCheck πŸŽƒ'
        58      PRECALL                       1
        62      CALL                          1
        72      POP_TOP
        74      PUSH_NULL
        76      LOAD_NAME                     6: print
        78      LOAD_CONST                    6: 'πŸŽƒ Enter your password for spooky evaluation πŸŽƒ'
        80      PRECALL                       1
        84      CALL                          1
        94      POP_TOP
        96      PUSH_NULL
        98      LOAD_NAME                     7: input
        100     LOAD_CONST                    7: 'πŸ‘» '
        102     PRECALL                       1
        106     CALL                          1
        116     STORE_NAME                    8: inp
        118     PUSH_NULL
        120     LOAD_NAME                     4: check
        122     LOAD_NAME                     8: inp
        124     LOAD_METHOD                   9: encode
        146     PRECALL                       0
        150     CALL                          0
        160     PRECALL                       1
        164     CALL                          1
        174     POP_JUMP_FORWARD_IF_FALSE     13 (to 202)
        176     PUSH_NULL
        178     LOAD_NAME                     6: print
        180     LOAD_CONST                    8: "πŸ¦‡ Well done, you're spookier than most! πŸ¦‡"
        182     PRECALL                       1
        186     CALL                          1
        196     POP_TOP
        198     LOAD_CONST                    10: None
        200     RETURN_VALUE
        202     PUSH_NULL
        204     LOAD_NAME                     6: print
        206     LOAD_CONST                    9: 'πŸ’€ Not spooky enough, please try again later πŸ’€'
        208     PRECALL                       1
        212     CALL                          1
        222     POP_TOP
        224     LOAD_CONST                    10: None
        226     RETURN_VALUE
        228     LOAD_CONST                    10: None
        230     RETURN_VALUE

We can then generate the Python source-code from the byte-code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
KEY = b"SUP3RS3CR3TK3Y"
CHECK = b"\xe9\xef\xc0V\x8d\x8a\x05\xbe\x8ek\xd9yX\x8b\x89\xd3\x8c\xfa\xdexu\xbe\xdf1\xde\xb6\\"

def transform(flag):
    result = []
    for i, f in enumerate(flag):
        value = (f + 24) & 255
        value ^= KEY[i % len(KEY)]
        value -= 74
        value &= 255
        result.append(value)
    return result

def check(flag):
    return transform(flag) == CHECK

if __name__ == "__main__":
    print("πŸŽƒ Welcome to SpookyCheck πŸŽƒ")
    print("πŸŽƒ Enter your password for spooky evaluation πŸŽƒ")
    inp = input("πŸ‘» ")
    if check(inp.encode()):
        print("πŸ¦‡ Well done, you're spookier than most! πŸ¦‡")
    else:
        print("πŸ’€ Not spooky enough, please try again later πŸ’€")

The following script was then used to reverse the flag that basically does everything in reverse-order to the original transform() function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
KEY = b"SUP3RS3CR3TK3Y"
CHECK = b"\xe9\xef\xc0V\x8d\x8a\x05\xbe\x8ek\xd9yX\x8b\x89\xd3\x8c\xfa\xdexu\xbe\xdf1\xde\xb6\\"

def reverse_transform(result):
    reversed_flag = []
    for i, value in enumerate(result):
        value += 74
        value &= 255
        value ^= KEY[i % len(KEY)]
        value -= 24
        value &= 255
        reversed_flag.append(value)
    return bytes(reversed_flag)

# Reverse the flag using the transform function
reversed_flag = bytes(reverse_transform(CHECK))
print(reversed_flag.decode('utf-8'))
1
2
$ python3 check_rev.py
HTB{mod3rn_pyth0n_byt3c0d3}

Flag: HTB{mod3rn_pyth0n_byt3c0d3}

This post is licensed under CC BY 4.0 by the author.