2024 KCTF-神秘信号

2024 KCTF-神秘信号

题目要求

给出一组公开的用户名以及其对应的序列号,求用户名为KCTF的序列号.

1
2
3
4
#用户名
D7C4197AF0806891
#序列号
D7CHel419lo 7AFWor080ld!6891

解包反编译

目标程序是PyInstaller打包生成的二进制文件,如下所示:

查壳

可以使用pyinstxtractor-ngpyinstxtractor进行解包.

pyc文件的反编译可以使用uncompyle6decompile3pycdc

对main.pyc进行反编译,结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import CrackMe
print('(账号密码由字母大小写、数字、!、空格组成)')
print('请输入账号:')
h = input()
z = CrackMe.main(h)
if len(z) < 20:
    key = 'dZpKdrsiB6cndrGY' + z
else:
    key = z[0:4] + 'dZpK' + z[4:8] + 'drsi' + z[8:12] + 'B6cn' + z[12:16] + 'drGY' + z[16:]
print('请输入验证码:')
h = input()
m = CrackMe.main(h)
if key == m:
    print('Success')

print('Fail')
continue

在程序目录中没有发现CrackMe模块, 也就是说CrackMe.main代码被隐藏了.

寻找CrackMe模块

使用Procmon对目标程序进行文件监控,发现加载了其目录下的bz2模块.

bz2模块加载

我们可以将原_bz2.pyd重命名为_bz2.pyd.src,在其所在目录新建_bz2.py,添加下述代码,查看CrackMe原型.

1
2
import CrackMe
print(CrackMe)

运行目标程序后,输出结果如下:

1
<module 'base64' from 'C:\\Users\\XiaLuoHun\\Desktop\\main2\\main\\_internal\\base64.pyc'>

没想到,CrackMe模块居然是base64.pyc,对其进行反编译可以看到在base64模块的结尾做了手脚,进行了字节码替换.

base64模块自修改

我们不用反编译上述代码,直接在_bz2.py中添加下述代码,进行CrackMe.main函数代码Dump

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import CrackMe

import marshal
import importlib

code = CrackMe.main.__code__
marshal_data = marshal.dumps(code)
pyc_data = importlib._bootstrap_external._code_to_timestamp_pyc(code)

with open("crackme_main.marshal", "wb") as f:
    f.write(marshal_data)

with open("crackme_main.pyc", "wb") as f:
    f.write(pyc_data)

对Dump下来的pyc文件进行反编译,得到CrackMe.main代码如下:

 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
encoded_str = ''
padding = 0
base64_chars = 'ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/'
ww = b''
for i in data:
    i = i ^ 85
    ww = ww + i.to_bytes(1, 'little')
data = ww
for i in range(0, len(data), 3):
    chunk = data[i:i + 3]
#     binary_str = ''.join((lambda .0: for byte in .0:
# format(byte, '08b'))(chunk))
binary_str = ''.join(format(byte, '08b') for byte in chunk)
    for j in range(0, len(binary_str), 6):
        six_bits = binary_str[j:j + 6]
        if len(six_bits) < 6:
            padding += 6 - len(six_bits)
            six_bits += '0' * (6 - len(six_bits))
        encoded_str += base64_chars[int(six_bits, 2)]
encoded_str += '!' * (padding // 2)
for i in range(len(encoded_str) // 2):
    a = encoded_str[i * 2]
    b = encoded_str[i * 2 + 1]
    encoded_str = encoded_str[:i * 2] + b + a + encoded_str[i * 2 + 2:]
return encoded_str

反编译出来的结果没有函数声明,结合上下文理解,可得到CrackMe.main代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def crack_main(data):
    encoded_str = ''
    padding = 0
    base64_chars = 'ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/'
    ww = b''
    for i in data:
        i ^= 85
        ww += i.to_bytes(1, 'little')
    data = ww
    for i in range(0, len(data), 3):
        chunk = data[i:i+3]
        binary_str = ''.join(format(byte, '08b') for byte in chunk)
        for j in range(0, len(binary_str), 6):
            six_bits = binary_str[j:j+6]
            if len(six_bits) < 6:
                padding += 6 - len(six_bits)
                six_bits += '0' * (6 - len(six_bits))
            encoded_str += base64_chars[int(six_bits, 2)]
    encoded_str += '!' * (padding // 2)
    for i in range(len(encoded_str) // 2):
        a = encoded_str[2 * i]
        b = encoded_str[2 * i + 1]
        encoded_str = encoded_str[:2 * i] + b + a + encoded_str[2 * i + 2:]
    return encoded_str

同时写出上述过程的逆运算代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def crack_main_reverse(encoded_str):
    base64_chars = 'ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/'
    padding_count = encoded_str.count('!')
    encoded_str = encoded_str.replace('!', '')
    decoded_str = ''
    for i in range(len(encoded_str) // 2):
        b = encoded_str[2 * i]
        a = encoded_str[2 * i + 1]
        decoded_str += a + b
    if len(encoded_str) % 2 != 0:
        decoded_str += encoded_str[-1]
    binary_str = ''
    for char in decoded_str:
        index = base64_chars.index(char)
        binary_str += format(index, '06b')
    data = bytearray()
    for i in range(0, len(binary_str) - padding_count * 2, 8):
        byte = int(binary_str[i:i+8], 2)
        data.append(byte)
    for i in range(len(data)):
        data[i] ^= 85
    return bytes(data)

Flag

使用下述代码进行公开用户名以及序列号验证:

 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
def crack_main(data):
    encoded_str = ''
    padding = 0
    base64_chars = 'ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/'
    ww = b''
    for i in data:
        i ^= 85
        ww += i.to_bytes(1, 'little')
    data = ww
    for i in range(0, len(data), 3):
        chunk = data[i:i+3]
        binary_str = ''.join(format(byte, '08b') for byte in chunk)
        for j in range(0, len(binary_str), 6):
            six_bits = binary_str[j:j+6]
            if len(six_bits) < 6:
                padding += 6 - len(six_bits)
                six_bits += '0' * (6 - len(six_bits))
            encoded_str += base64_chars[int(six_bits, 2)]
    encoded_str += '!' * (padding // 2)
    for i in range(len(encoded_str) // 2):
        a = encoded_str[2 * i]
        b = encoded_str[2 * i + 1]
        encoded_str = encoded_str[:2 * i] + b + a + encoded_str[2 * i + 2:]
    return encoded_str

def crack_main_reverse(encoded_str):
    base64_chars = 'ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/'
    padding_count = encoded_str.count('!')
    encoded_str = encoded_str.replace('!', '')
    decoded_str = ''
    for i in range(len(encoded_str) // 2):
        b = encoded_str[2 * i]
        a = encoded_str[2 * i + 1]
        decoded_str += a + b
    if len(encoded_str) % 2 != 0:
        decoded_str += encoded_str[-1]
    binary_str = ''
    for char in decoded_str:
        index = base64_chars.index(char)
        binary_str += format(index, '06b')
    data = bytearray()
    for i in range(0, len(binary_str) - padding_count * 2, 8):
        byte = int(binary_str[i:i+8], 2)
        data.append(byte)
    for i in range(len(data)):
        data[i] ^= 85
    return bytes(data)

def convert_key(z):
    if len(z) < 20:
        key = 'dZpKdrsiB6cndrGY' + z
    else:
        key = z[0:4] + 'dZpK' + z[4:8] + 'drsi' + z[8:12] + 'B6cn' + z[12:16] + 'drGY' + z[16:]
    return key

z = crack_main(b"D7C4197AF0806891")
key = convert_key(z)
m = crack_main(b"D7CHel419lo 7AFWor080ld!6891")
print(z)
print(key)
print(m)
print(crack_main_reverse(m))

输出结果如下:

1
2
3
4
D7DED6vCn6boDrp3W6v3Zr!!
D7DEdZpKD6vCdrsin6boB6cnDrp3drGYW6v3Zr!!
D7DEbBsZD6vCb53xn6bo2ZmODrp3b5YtW6v3Zr!!
b'D7CHel419lo 7AFWor080ld!6891'

根据程序流程来说,公开的用户名计算出来的key应该和序列号计算出来的结果相同,但是从结果来看明显不同,如下:

1
2
key: D7DEdZpKD6vCdrsin6boB6cnDrp3drGYW6v3Zr!!
m:   D7DEbBsZD6vCb53xn6bo2ZmODrp3b5YtW6v3Zr!!

从上述可以看到main.pyc在运行过程中有地方被修改了, 我们采用排除法,在_bz2.py中添加下述代码,对CrackMe.main函数进行Hook, 查看输入参数和输出参数.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import CrackMe
 
import sys
 
origin_crackme_main = CrackMe.main
 
def hook_decompile_crackme_main(data):
    print(repr(data))
    r = origin_crackme_main(data)
    print(repr(r))
    return r
  
CrackMe.main = hook_decompile_crackme_main
 
sys.modules["CrackMe"] = CrackMe

运行目标程序,结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
PS C:\Users\XiaLuoHun\Desktop\main2\main> .\main.exe
(账号密码由字母大小写数字、!、空格组成)
请输入账号
D7C4197AF0806891
b'HUIX[cUKF\\d\\Vdc['
'oB0ZoUWkQZbkb+K7RZW7iU!!'
请输入验证码
D7CHel419lo 7AFWor080ld!6891
b"HUIT'0X[c0-lUKF5-\x1a\\d\\0(kVdc["
'oB0ZdZpKoUWkdrsiQZbkB6cnb+K7drGYRZW7iU!!'
Success

从结果可以看到,我们输入的内容被修改了,也就是说input函数有问题.可以在在_bz2.py中添加下述代码,对input函数进行Hook,进一步确认修改点.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import builtins

old_input = input
def hook_input(*args, **kwargs):
    r = old_input(*args, **kwargs)
    print(repr(old_input), hex(id(old_input)), repr(r))
    return r
 
#print(input.__module__)
builtins.input = hook_input

运行目标程序,结果如下:

1
2
3
4
5
6
7
8
9
PS C:\Users\XiaLuoHun\Desktop\main2\main> .\main.exe
(账号密码由字母大小写数字、!、空格组成)
请输入账号
D7C4197AF0806891
<built-in function input> 0x1aa7afd0a90 b'HUIX[cUKF\\d\\Vdc['
请输入验证码
D7CHel419lo 7AFWor080ld!6891
<built-in function input> 0x1aa7afd0a90 b"HUIT'0X[c0-lUKF5-\x1a\\d\\0(kVdc["
Success

通过python内置的id函数可以得到PyCFunctionObject结构体地址,其结构定义如下:

 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
typedef __int64 Py_ssize_t;
typedef PyObject* (*PyCFunction)(PyObject*, PyObject*);
typedef PyObject* (*vectorcallfunc)(PyObject* callable, PyObject* const* args, size_t nargsf, PyObject* kwnames);

typedef struct _object {
	Py_ssize_t ob_refcnt;
	struct _typeobject* ob_type;
} PyObject;
#define PyObject_HEAD                   PyObject ob_base;

struct PyMethodDef {
	const char* ml_name;   /* The name of the built-in function/method */
	PyCFunction ml_meth;    /* The C function that implements it */
	int         ml_flags;   /* Combination of METH_xxx flags, which mostly
							   describe the args expected by the C func */
	const char* ml_doc;    /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

typedef struct {
	PyObject_HEAD
	PyMethodDef* m_ml; /* Description of the C function to call */
	PyObject* m_self; /* Passed as 'self' arg to the C func, can be NULL */
	PyObject* m_module; /* The __module__ attribute, can be anything */
	PyObject* m_weakreflist; /* List of weak references */
	vectorcallfunc vectorcall;
} PyCFunctionObject;

//对于python中字节码函数对象在cpython中对应的结构体是PyFunctionObject

PyCFunctionObject结构体偏移16字节的地方就拿到了PyMethodDef结构体地址,然后再偏移8字节的地方就是python builtin_function_or_method函数真正的地址.

根据上述结构体偏移,可以拿到input函数地址,反编译代码如下:

 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
__int64 sub_26CE91C8690()
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v22 = *(_QWORD *)(*(_QWORD *)(((__int64 (*)(void))unk_26CE91C8D90)() + 96) + 24i64);
  v20 = (__int64 *)(v22 + 16);
  v12 = *(__int64 **)(v22 + 16);
  v16 = 0i64;
  v17 = 0i64;
  v18 = 0i64;
  v19 = 0i64;
  v21 = 0i64;
  while ( v12 != v20 )
  {
    v23 = v12;
    v12 = (__int64 *)*v12;
    v3 = v23[6];
    v24 = *(int *)(v3 + 60) + v3;
    v11 = *(_DWORD *)(v24 + 136);
    if ( v11 )
    {
      v10 = (unsigned int *)(v11 + v3);
      if ( v10[6] )
      {
        v15 = (_DWORD *)(v10[3] + v3);
        if ( (*v15 | 0x20202020) == 1852990827
          && (v15[1] | 0x20202020) == 842230885
          && (v15[2] | 0x20202020) == 1819042862 )
        {
          v14 = v10[7] + v3;
          v25 = v10[8] + v3;
          v13 = v10[9] + v3;
          for ( i = 0; i < v10[6]; ++i )
          {
            v7 = (_DWORD *)(*(unsigned int *)(v25 + 4i64 * i) + v3);
            if ( *v7 == 1400137031 && v7[1] == 1632134260 )
              v16 = (__int64 (__fastcall *)(__int64))(*(unsigned int *)(v14
                                                                      + 4i64 * *(unsigned __int16 *)(v13 + 2i64 * i))
                                                    + v3);
            if ( *v7 == 1684104530 && v7[1] == 1936617283 )
              v17 = (void (__fastcall *)(__int64, char *, __int64, unsigned int *, _QWORD))(*(unsigned int *)(v14 + 4i64 * *(unsigned __int16 *)(v13 + 2i64 * i))
                                                                                          + v3);
            if ( *v7 == 1684107084 && v7[1] == 1919052108 && v7[2] == 1098478177 )
              v18 = (__int64 (__fastcall *)(char *))(*(unsigned int *)(v14 + 4i64
                                                                           * *(unsigned __int16 *)(v13 + 2i64 * i))
                                                   + v3);
            if ( *v7 == 1349805383 && v7[1] == 1097035634 )
              v19 = (__int64 (__fastcall *)(__int64, char *))(*(unsigned int *)(v14
                                                                              + 4i64
                                                                              * *(unsigned __int16 *)(v13 + 2i64 * i))
                                                            + v3);
            if ( v16 && v17 && v18 && v19 )
            {
              v26 = v16(4294967286i64);
              for ( j = 0; j < 100; ++j )
                v28[j] = 0;
              v4 = 0;
              v17(v26, v28, 50i64, &v4, 0i64);
              for ( k = 0; k < 200; ++k )
                v29[k] = 0;
              v29[0] = 's';
              for ( m = 0; m < v4; ++m )
              {
                if ( v28[m] == '\r' || v28[m] == '0x00' || v28[m] == '\n' )
                {
                  v4 = m;
                  break;
                }
                v29[m + 5] = (v28[m] ^ 0x77) + 21;
              }
              *(_DWORD *)&v29[1] = v4;
              strcpy(v8, "python38.dll");
              v27 = v18(v8);
              strcpy(v9, "PyMarshal_ReadObjectFromString");
              v21 = (__int64 (__fastcall *)(char *, _QWORD))v19(v27, v9);
              return v21(v29, v4 + 5);
            }
          }
        }
      }
    }
  }
  return 0i64;
}

根据上述代码修改的地方,写出对应的python代码以及逆运算如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def input_mod(data):
    data_len = len(data)
    out_data = bytearray(data_len + 5)
    for i in range(data_len):
        if data[i] == '\r' or data[i] == '0x00' or data[i] == '\n':
            data_len = i
            break
        out_data[i + 5] = (data[i] ^ 0x77) + 21
    return out_data[5:]

def input_mod_inverse(data):
    out_data = bytearray(len(data))
    for i in range(len(data)):
        out_data[i] = (data[i] - 21) ^ 0x77
    return bytes(out_data)

可使用下述代码,进行KCTF用户名的序列号计算.

 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
def crack_main(data):
    encoded_str = ''
    padding = 0
    base64_chars = 'ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/'
    ww = b''
    for i in data:
        i ^= 85
        ww += i.to_bytes(1, 'little')
    data = ww
    for i in range(0, len(data), 3):
        chunk = data[i:i+3]
        binary_str = ''.join(format(byte, '08b') for byte in chunk)
        for j in range(0, len(binary_str), 6):
            six_bits = binary_str[j:j+6]
            if len(six_bits) < 6:
                padding += 6 - len(six_bits)
                six_bits += '0' * (6 - len(six_bits))
            encoded_str += base64_chars[int(six_bits, 2)]
    encoded_str += '!' * (padding // 2)
    for i in range(len(encoded_str) // 2):
        a = encoded_str[2 * i]
        b = encoded_str[2 * i + 1]
        encoded_str = encoded_str[:2 * i] + b + a + encoded_str[2 * i + 2:]
    return encoded_str

def crack_main_reverse(encoded_str):
    base64_chars = 'ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/'
    padding_count = encoded_str.count('!')
    encoded_str = encoded_str.replace('!', '')
    decoded_str = ''
    for i in range(len(encoded_str) // 2):
        b = encoded_str[2 * i]
        a = encoded_str[2 * i + 1]
        decoded_str += a + b
    if len(encoded_str) % 2 != 0:
        decoded_str += encoded_str[-1]
    binary_str = ''
    for char in decoded_str:
        index = base64_chars.index(char)
        binary_str += format(index, '06b')
    data = bytearray()
    for i in range(0, len(binary_str) - padding_count * 2, 8):
        byte = int(binary_str[i:i+8], 2)
        data.append(byte)
    for i in range(len(data)):
        data[i] ^= 85
    return bytes(data)

def convert_key(z):
    if len(z) < 20:
        key = 'dZpKdrsiB6cndrGY' + z
    else:
        key = z[0:4] + 'dZpK' + z[4:8] + 'drsi' + z[8:12] + 'B6cn' + z[12:16] + 'drGY' + z[16:]
    return key

def input_mod(data):
    data_len = len(data)
    out_data = bytearray(data_len + 5)
    for i in range(data_len):
        if data[i] == '\r' or data[i] == '0x00' or data[i] == '\n':
            data_len = i
            break
        out_data[i + 5] = (data[i] ^ 0x77) + 21
    return out_data[5:]

def input_mod_inverse(data):
    out_data = bytearray(len(data))
    for i in range(len(data)):
        out_data[i] = (data[i] - 21) ^ 0x77
    return bytes(out_data)

input_data = input_mod(b"KCTF")
z = crack_main(input_data)
key = convert_key(z)
key_reverse = crack_main_reverse(key)
flag = input_mod_inverse(key_reverse)
print(flag)

输出结果为:

1
b'Hello World!KCTF'

参考链接

2024 KCTF 大赛 | 第四题《神秘信号》设计思路及解析

看雪 2024 KCTF 大赛 第四题 神秘信号

Python源码解析-builtin_function_or_method


相关内容

0%