CISCN 2021 [rev] 复盘

imnotavirus

先看到 main.exe,使用DIE发现有UPX壳,

1
upx.exe -d main.exe

根据另一个文件夹 pyinstaller-4.2 ,可知这是一个pyinstaller打包成的EXE。使用pyinstxtractor解包:

1
python3 pyinstxtractor.py main.exe

可能会观察到类似类似下面的字样:

1
[!] Error: Failed to decompress xxxxx.pyc, probably encrypted. Extracting as is.

作为一个决赛当然不会让我们直接看到pyc文件= =,这应该是使用了pyinstaller里的加密功能。

看一看 pyinstaller里的pyimod02_archive.py所定义的Cipher类:

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
class Cipher(object):
"""
This class is used only to decrypt Python modules.
"""
def __init__(self):
# At build-type the key is given to us from inside the spec file, at
# bootstrap-time, we must look for it ourselves by trying to import
# the generated 'pyi_crypto_key' module.
import pyimod00_crypto_key
key = pyimod00_crypto_key.key

assert type(key) is str
if len(key) > CRYPT_BLOCK_SIZE:
self.key = key[0:CRYPT_BLOCK_SIZE]
else:
self.key = key.zfill(CRYPT_BLOCK_SIZE)
assert len(self.key) == CRYPT_BLOCK_SIZE

import tinyaes
self._aesmod = tinyaes
# Issue #1663: Remove the AES module from sys.modules list. Otherwise
# it interferes with using 'tinyaes' module in users' code.
del sys.modules['tinyaes']

def __create_cipher(self, iv):
# The 'AES' class is stateful, this factory method is used to
# re-initialize the block cipher class with each call to xcrypt().
return self._aesmod.AES(self.key.encode(), iv)

def decrypt(self, data):
cipher = self.__create_cipher(data[:CRYPT_BLOCK_SIZE])
return cipher.CTR_xcrypt_buffer(data[CRYPT_BLOCK_SIZE:])

再看看 PyiBlockCipher 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PyiBlockCipher(object):
"""
This class is used only to encrypt Python modules.
"""
def __init__(self, key=None):
assert type(key) is str
if len(key) > BLOCK_SIZE:
self.key = key[0:BLOCK_SIZE]
else:
self.key = key.zfill(BLOCK_SIZE)
assert len(self.key) == BLOCK_SIZE

import tinyaes
self._aesmod = tinyaes

def encrypt(self, data):
iv = os.urandom(BLOCK_SIZE)
return iv + self.__create_cipher(iv).CTR_xcrypt_buffer(data)

def __create_cipher(self, iv):
# The 'AES' class is stateful, this factory method is used to
# re-initialize the block cipher class with each call to xcrypt().
return self._aesmod.AES(self.key.encode(), iv)

最后是writer.py里的代码片段:

1
2
3
4
5
6
7
    obj = zlib.compress(data, self.COMPRESSION_LEVEL)
# First compress then encrypt.
if self.cipher:
obj = self.cipher.encrypt(obj)
self.toc.append((name, (typ, self.lib.tell(), len(obj))))
self.lib.write(obj)
###############################

重点:AES、key、iv、CRYPT_BLOCK_SIZE(16)、zlib

出题人特意考察了代码审计(但我感觉是信息搜集23333),可以直接在pyinstaller源码基础上实现解密,但是因为这个解密方法比较简单,我们直接写一个脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def decrypt_pyc():
import zlib
import tinyaes

pyc_path = r"sign.pyc.encrypted"
with open(pyc_path, 'rb') as f:
buf = f.read()
iv = buf[:16]
data = buf[16:]
key = b'NoneOfUrBusiness'

cipher = tinyaes.AES(key, iv)
output = cipher.CTR_xcrypt_buffer(data)

output = zlib.decompress(output)

with open(r"sign.pyc", "wb") as f:
f.write(iv + output)

得到sign.pyc已经成功百分之五十,使用HxD修改文件前16个字节为55 0D 0D 0A 00 00 00 00 DA 73 8F 60 0A 12 00 00 ,反编译pyc:

1
uncompyle6.exe .\sign.pyc

得到的反编译结果如下:

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
# uncompyle6 version 3.7.4
# Python bytecode 3.8 (3413)
# Decompiled from: Python 3.8.10 (tags/v3.8.10:3d8993a, May 3 2021, 11:48:03) [MSC v.1928 64 bit (AMD64)]
# Embedded file name: sign.py
# Compiled at: 2021-05-03 11:54:02
# Size of source mod 2**32: 4618 bytes
import ctypes, urllib, base64, hashlib, ast

def ppp(bbb):
qaq = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(bbb)), ctypes.c_int(12288), ctypes.c_int(64))
bbb[31:35] = (qaq + 59).to_bytes(4, 'little')
bbb[36:40] = (qaq + 178).to_bytes(4, 'little')
qwq = (ctypes.c_char * len(bbb)).from_buffer(bbb)
eval(base64.b64decode('Y3R5cGVzLndpbmRsbC5rZXJuZWwzMi5SdGxNb3ZlTWVtb3J5KGN0eXBlcy5jX3VpbnQzMihxYXEpLHF3cSxjdHlwZXMuY19pbnQobGVuKGJiYikpKQ==').decode())
handle = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0), ctypes.c_int(0), ctypes.c_uint32(qaq), ctypes.c_int(0), ctypes.c_int(0), ctypes.pointer(ctypes.c_int(0)))
ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle), ctypes.c_int(-1))


def aaa(ttt, ggg):
ggg = hashlib.md5(ggg).hexdigest()
ttt = base64.b64decode(ttt)
result = b''
ggg_len = len(ggg)
box = list(range(256))
j = 0
for i in range(256):
j = (j + box[i] + ord(ggg[(i % ggg_len)])) % 256
box[i], box[j] = box[j], box[i]
else:
i = j = 0
for iii in ttt:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
k = iii ^ box[((box[i] + box[j]) % 256)]
result += bytes([k])
else:
return result


def main():
ctypes.windll.kernel32.VirtualAlloc.restyle = ctypes.c_uint32
fff = input('Your input: ')
b = 'pB2M/eG2iz1pB4kej4yXXCYvTFV3b/6NDPvMVc+iOs2QwI7Tg7QcItIK6KtB5seaZhd67NGYh6xyMPAocLhd0NeJhweg5/rsEuYnzxFxqMrysaizRAiD6HQhe50rwF5UnByay04giUxuLxy6zL8me5sAQqaUAuCv0c1EsDKBxv1B1zV8MEDav5bkgsTd2t3X3Jt0+lfGKk98bxg1FIXoUhtyZjhV489SpMi+Gzs2+/zaZ0d7p12KoppTYPNs3sj9l74Q8EJPYIAecUEnMSmKDF7yPaKIFloCdW5ghVaSeiaskr5OzfzfeccpvRPevL7PW9uW1R8WmcW2oWjN9aWsinwG2Gk1B7JPa+HusBGpIxxSQhK0wBrEjQpYlIMC7fTpFZca373+p/A2oXuaXqfmOoWtE62JCM74tWqZFrSWyLdnu1/vHaClrzcdzpHLum9shEOcNHYi88Dj11mYufJxH/sEx0CBtWkTCHwEhxVs1sYkGBHlDFUpFbfpY0UagbyPJdq+bmXBdmLKEhJ/M/2cCjmsjuma6IKvo+riA7/B8+T3GB06x31G5tibi3rJMDb4bVWBswzI3gg06mc4Q9EW5dQ4+/SRKbVoYhAfbGKlBeuxSIARKzSAuJPlfm+dmJ6pHx9a4qek2UIEKr4zxA0bSMALoYOSETDF1JWZX+K0HEJLY/hXajmzq5qSTX0EAKBLJtIPkJ0e+XsaQCXyhy4Cg8mYNlQGIORNo5vyNe4QPAD8d3GKr/PZnEMwJ5WsgyoSBOGDke4PGUJd70thEyGHKN9QfTXWknC8HBZdcEojvtC3Prj7LlxXI6y8uZ7ie/1HltGogj3EsUkqU0d3WDuBPZec1Tzj/7Vs44MFGRjEJ0IuSA0U0vOCShyeHUB23qhrXqODsrO/t+s/Zohmd2H0xS46qdoquQj8L1RY2fCt3H0US3Wffk0FKf7qYboKeW/7vlkOYlchgP/HXf0Mfo5gBXhJg3e9jGJ8K5J0gt6Zra9dhPINGgekDMIoxXE='
c = aaa(b, 'blackhand'.encode('utf-8'))
c = ast.literal_eval("b'" + c.decode().strip() + "'")
qaq = eval(base64.b64decode('Y3R5cGVzLndpbmRsbC5rZXJuZWwzMi5WaXJ0dWFsQWxsb2MoY3R5cGVzLmNfaW50KDApLCBjdHlwZXMuY19pbnQobGVuKGZmZikpLCBjdHlwZXMuY19pbnQoMTIyODgpLCBjdHlwZXMuY19pbnQoNjQpKQ==').decode())
qwq = (ctypes.c_char * (len(fff) + 1)).from_buffer(bytearray(fff.encode()) + bytes([0]))
eval(base64.b64decode('Y3R5cGVzLndpbmRsbC5rZXJuZWwzMi5SdGxNb3ZlTWVtb3J5KGN0eXBlcy5jX3VpbnQzMihxYXEpLHF3cSxjdHlwZXMuY19pbnQobGVuKGZmZikpKQ==').decode())
c = bytearray(c)
c[55:59] = qaq.to_bytes(4, 'little')
ppp(c)
r = ctypes.create_string_buffer(len(fff))
ctypes.windll.kernel32.ReadProcessMemory(ctypes.windll.kernel32.GetCurrentProcess(), ctypes.c_uint32(qaq), ctypes.byref(r), ctypes.c_ulong(len(fff)), 0)
if r.raw == b'J\x04`~~s Q"Y!C [j\x05e\x06aB&N#B!E Qp\\ S{\x05{\x05{\x05':
print('Yes! You got it!')
else:
print('Nope. Try harder :)')
# okay decompiling .\sign.pyc

首先对最长的base64密文进行解密,再RC4解密,得到的其实包含了机器指令,我们写一个脚本将其dump出来:

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
def get_binary():
import ctypes, base64, hashlib, ast

def aaa(ttt, ggg):
ggg = hashlib.md5(ggg).hexdigest()
ttt = base64.b64decode(ttt)
result = b''
ggg_len = len(ggg)
box = list(range(256))
j = 0
for i in range(256):
j = (j + box[i] + ord(ggg[(i % ggg_len)])) % 256
box[i], box[j] = box[j], box[i]
else:
i = j = 0
for iii in ttt:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
k = iii ^ box[((box[i] + box[j]) % 256)]
result += bytes([k])
else:
return result

def main():
ctypes.windll.kernel32.VirtualAlloc.restyle = ctypes.c_uint32
b = 'pB2M/eG2iz1pB4kej4yXXCYvTFV3b/6NDPvMVc+iOs2QwI7Tg7QcItIK6KtB5seaZhd67NGYh6xyMPAocLhd0NeJhweg5/rsEuYnzxFxqMrysaizRAiD6HQhe50rwF5UnByay04giUxuLxy6zL8me5sAQqaUAuCv0c1EsDKBxv1B1zV8MEDav5bkgsTd2t3X3Jt0+lfGKk98bxg1FIXoUhtyZjhV489SpMi+Gzs2+/zaZ0d7p12KoppTYPNs3sj9l74Q8EJPYIAecUEnMSmKDF7yPaKIFloCdW5ghVaSeiaskr5OzfzfeccpvRPevL7PW9uW1R8WmcW2oWjN9aWsinwG2Gk1B7JPa+HusBGpIxxSQhK0wBrEjQpYlIMC7fTpFZca373+p/A2oXuaXqfmOoWtE62JCM74tWqZFrSWyLdnu1/vHaClrzcdzpHLum9shEOcNHYi88Dj11mYufJxH/sEx0CBtWkTCHwEhxVs1sYkGBHlDFUpFbfpY0UagbyPJdq+bmXBdmLKEhJ/M/2cCjmsjuma6IKvo+riA7/B8+T3GB06x31G5tibi3rJMDb4bVWBswzI3gg06mc4Q9EW5dQ4+/SRKbVoYhAfbGKlBeuxSIARKzSAuJPlfm+dmJ6pHx9a4qek2UIEKr4zxA0bSMALoYOSETDF1JWZX+K0HEJLY/hXajmzq5qSTX0EAKBLJtIPkJ0e+XsaQCXyhy4Cg8mYNlQGIORNo5vyNe4QPAD8d3GKr/PZnEMwJ5WsgyoSBOGDke4PGUJd70thEyGHKN9QfTXWknC8HBZdcEojvtC3Prj7LlxXI6y8uZ7ie/1HltGogj3EsUkqU0d3WDuBPZec1Tzj/7Vs44MFGRjEJ0IuSA0U0vOCShyeHUB23qhrXqODsrO/t+s/Zohmd2H0xS46qdoquQj8L1RY2fCt3H0US3Wffk0FKf7qYboKeW/7vlkOYlchgP/HXf0Mfo5gBXhJg3e9jGJ8K5J0gt6Zra9dhPINGgekDMIoxXE='
c = aaa(b, 'blackhand'.encode('utf-8'))
c = ast.literal_eval("b'" + c.decode().strip() + "'")
bc = bytearray(c)
with open('ciscn_2021_imnotvirus_b64_1', 'wb') as f:
i = 0x3D
while i < len(c):
bc[i] ^= 0x77
i += 1
f.write(bc)

main()


get_binary()

IDA打开:

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
    push    rbp
mov ebp, esp
sub esp, 0E4h
push rbx
push rsi
push rdi
lea edi, [rbp-0E4h]
mov ecx, 39h ; '9'
mov eax, 0CCCCCCCCh
rep stosd
mov eax, 41178Bh
mov ecx, 411802h
sub ecx, eax
loc_2A:
mov ebx, [rax]
xor ebx, 77h
mov [rax], ebx
loop loc_2A
mov dword ptr [rbp+8], 0FFFFFFFFh
mov al, 32h ; '2'
clc
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;分割线;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
loc_75:
mov eax, [rbp-20h]
add eax, 2
mov [rbp-20h], eax
mov eax, [rbp-20h]
cmp eax, [rbp-8]
jge short loc_B2
mov eax, [rbp+8]
add eax, [rbp-20h]
movsx ecx, byte ptr [rax]
xor ecx, 13h
mov edx, [rbp+8]
add edx, [rbp-20h]
mov [rdx], cl
mov eax, [rbp+8]
add eax, [rbp-20h]
movsx ecx, byte ptr [rax+1]
xor ecx, 37h
mov edx, [rbp+8]
add edx, [rbp-20h]
mov [rdx+1], cl
jmp short loc_75
loc_B2:
sub [rcx], ch
sub al, 0F6h
mov bl, 93h
loc_B8:
ja short near ptr 131h
ja short loc_B8
xchg eax, edx

分割线上面的汇编是一个SMC,我在py脚本提前处理了。分割线下面的是真实的加密算法。

解密脚本如下:

1
2
3
4
5
6
7
8
9
def get_flag():
bc = bytearray(b'J\x04`~~s Q"Y!C [j\x05e\x06aB&N#B!E Qp\\ S{\x05{\x05{\x05')
for i in range(0, len(bc), 2):
bc[i] ^= 0x13
bc[i + 1] ^= 0x37
print(bc)


get_flag()

完成。