2024 KCTF-神秘信号
题目要求
给出一组公开的用户名以及其对应的序列号,求用户名为KCTF的序列号.
1
2
3
4
#用户名
D7C4197AF0806891
#序列号
D7CHel419lo 7 AFWor080ld ! 6891
解包反编译
目标程序是PyInstaller打包生成的二进制文件,如下所示:
可以使用pyinstxtractor-ng 或pyinstxtractor 进行解包.
pyc文件的反编译可以使用uncompyle6 、decompile3 或pycdc
对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.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模块的结尾做了手脚,进行了字节码替换.
我们不用反编译上述代码,直接在_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 7 AFWor080ld ! 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 7 AFWor080ld ! 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 ) + 24 i64 );
v20 = ( __int64 * )( v22 + 16 );
v12 = * ( __int64 ** )( v22 + 16 );
v16 = 0 i64 ;
v17 = 0 i64 ;
v18 = 0 i64 ;
v19 = 0 i64 ;
v21 = 0 i64 ;
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 + 4 i64 * i ) + v3 );
if ( * v7 == 1400137031 && v7 [ 1 ] == 1632134260 )
v16 = ( __int64 ( __fastcall * )( __int64 ))( * ( unsigned int * )( v14
+ 4 i64 * * ( unsigned __int16 * )( v13 + 2 i64 * i ))
+ v3 );
if ( * v7 == 1684104530 && v7 [ 1 ] == 1936617283 )
v17 = ( void ( __fastcall * )( __int64 , char * , __int64 , unsigned int * , _QWORD ))( * ( unsigned int * )( v14 + 4 i64 * * ( unsigned __int16 * )( v13 + 2 i64 * i ))
+ v3 );
if ( * v7 == 1684107084 && v7 [ 1 ] == 1919052108 && v7 [ 2 ] == 1098478177 )
v18 = ( __int64 ( __fastcall * )( char * ))( * ( unsigned int * )( v14 + 4 i64
* * ( unsigned __int16 * )( v13 + 2 i64 * i ))
+ v3 );
if ( * v7 == 1349805383 && v7 [ 1 ] == 1097035634 )
v19 = ( __int64 ( __fastcall * )( __int64 , char * ))( * ( unsigned int * )( v14
+ 4 i64
* * ( unsigned __int16 * )( v13 + 2 i64 * i ))
+ v3 );
if ( v16 && v17 && v18 && v19 )
{
v26 = v16 ( 4294967286 i64 );
for ( j = 0 ; j < 100 ; ++ j )
v28 [ j ] = 0 ;
v4 = 0 ;
v17 ( v26 , v28 , 50 i64 , & v4 , 0 i64 );
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 0 i64 ;
}
根据上述代码修改的地方,写出对应的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 )
输出结果为:
参考链接
2024 KCTF 大赛 | 第四题《神秘信号》设计思路及解析
看雪 2024 KCTF 大赛 第四题 神秘信号
Python源码解析-builtin_function_or_method