首页
社区
课程
招聘
[原创]看雪 2024 KCTF 大赛 第四题 神秘信号
发表于: 2024-8-22 03:06 4552

[原创]看雪 2024 KCTF 大赛 第四题 神秘信号

2024-8-22 03:06
4552

pyinstaller打包的二进制,用 pyinstxtractorpyinstxtractor-ng 解包(一定要用最新版本,可以省去自己补pyc文件头的步骤)

解释器版本是 Python3.8,看到 main.pyc 熟练的掏出 uncompyle6decompyle3 反编译一下:

看着很友好,但是,CrackMe模块在哪里呢??

(各种失败的尝试,可以跳过)

既然 _internel 目录下的 pyd 文件会被加载,那么不妨试试在这里做注入。

将 _internal/_lzma.pyd 文件删除,放一个 _internal/_lzma.py 文件,里面写上自己想执行的语句:

启动 main.exe,sys.modules真的被打印出来了,但是里面还是没有 CrackMe ??

那就再直接一点,看看它的真面目:

输出:

CrackMe的真身竟然是base64?

找到解包出来的 base64.pyc ,反编译一下,果然在末尾有偷梁换柱:

所以 CrackMe.main 就是 base64.main,而它的字节码也被替换掉了

稳妥起见,不去处理上面反编译的东西,而是直接在_lzma.py的hook中dump原始字节码:

(pyc_data参考自 https://stackoverflow.com/questions/73439775/how-to-convert-marshall-code-object-to-pyc-file

得到的 crackme_main.pyc,uncompyle6和decompyle3都会报错,不过标准库的dis模块能正常反汇编。
人工翻译了一会……突然想起了 pycdc ,试了下效果非常完美(需要小修一下)(对反编译的代码重新生成pyc再dis对比,与dump出来的dis完全相同):

写出逆向算法:

事情看起来解决了?不,问题才刚刚开始

逆向出来的main函数,其参数data的类型显然需要是bytes,但是main.py调用的时候传递的是str

只好先按bytes传递,先用给的序列号做验证:

rev的逻辑没有错误,问题出现在main.py里面,疑似ztokey时实际拼接的几个字符串常量与看到的不同

先不去深究,根据上面输出的key和m的对应关系,猜测真正的ztokey2应该是这样:

得到输出:

最终提交答案为 Hello World!KCTF

拿到题第一时间看了下 _internel 目录下 python38.dll 发现带有官方的数字签名,如蒙大赦,感谢出题人没搞什么自己魔改解释器的恶心套路。

(譬如更换字节码定义之类的……对攻击方真就是纯体力活增加工作量了。今年实在没时间没精力更没兴趣花费大量时间破代码混淆(例如第三题),过于浪费时间收益又极其有限(解法只能针对这道题,又不像vmp之类的能广泛应用)。在校生可能更有动力搞,社畜的周末还不如用来补觉)

题目的未解之谜还有很多,继续通过_lzma.py的hook探索:

最后给sys.modules赋值是必须的,否则会出现异常:

打印出 CrackMe.main 函数的实际输入输出,发现输入 "KCTF" 时,CrackMe.main 函数的输入是 b'QI8F',输出是 'QQMlP7!!'

输入值在此之前被修改过。
输入和输出与逆向出来的算法能对应,至少说明逆向过程没有问题。

回忆一下main.py的片段:

那么只有两种可能:main.py是虚假的,或者,input被修改过

hook一下input看看:

得到输出:

问题得到确认,builtin的input被修改过,它的返回值是经过变换的。但是,repr仍然标记为built-in,所以这里是如何实现的?

知己知彼还是非常重要的。每道题目放出前会习惯性的看一下出题人曾经在论坛发过的文章。出题人今年发了一系列python源码分析的文章,所以有预感第四题可能是python,而事实确实如此。

在此回顾下出题人的几篇文章:

第一篇讲了pyinstaller打包时篡改标准库注入代码
第二篇讲了替换函数的__code__属性改变其逻辑
第三篇讲了修改_frozen_importlib._find_and_load.__code__改变模块加载过程
第四篇讲了内存patch修改builtin函数(builtin函数没有__code__属性,通过内置id函数可以获得PyMethodDef结构的地址,PyMethodDef偏移16字节处是指向PyCFunction结构的指针,PyCFunction偏移8字节的地方是真正的C函数起始地址)

一和二已经观察到了,现在确认一下三和四

老方法通过_lzma.py的hook把_frozen_importlib._find_and_load.__code__的内容dump出来:

然后用pycdc反编译:(有 WARNING: Decompyle incomplete ,可以用 pydas 看反汇编,缺少的地方不重要,先不去管)

前面hook内置input函数时顺便打印了id(input)的值,挂上调试器按照文章的说法找到最终函数所在,dump内存,ida分析:

注意到 v33[m + 5] = (v32[m] ^ 0x77) + 21

做个验证:

与先前的所有观察都能对应上,包括main.py里面的常量。
也能解释为什么胡乱输入偶尔会触发builtin input return NULL的System Error:

至于为什么忽略convertinput的转换也能找到正确的答案,回顾一下CrackMe.main函数的逻辑,依次是单字节异或、换表base64、相邻两字节交换
那么,输出的每4个字节实际只受对应输入的3个字节影响。
而main.py里恰恰是对CrackMe.main输出以4字节为单位做重组,对于"KCTF"这样的短输入,甚至只是直接移动到末尾
所以,最终的serial,一定是输入的name与"Hello World!"这个12字节的常量的组合。
事实上,convertinput可以改成任意的单字节映射,都不影响这个结论

至此,题目最后的谜团只有对_frozen_importlib._find_and_load和input两处的修改是在何处初始化的。
估计是藏在了某个模块的初始化代码里,具体可能要追pyinstaller的初始化流程。不过,从上面SystemError的调用栈来看,这两处修改的位置相当早,至少在加载_lzma.py之前已经完成了。
等出题人公开完整的题目设计了

import CrackMe
while True:
    while True:
        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")
            break
 
    print("Fail")
    continue
import CrackMe
while True:
    while True:
        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")
            break
 
    print("Fail")
    continue
import sys
print(sys.modules)
import sys
print(sys.modules)
import CrackMe
print(CrackMe)
import CrackMe
print(CrackMe)
<module 'base64' from '...\\main\\_internal\\base64.pyc'>
<module 'base64' from '...\\main\\_internal\\base64.pyc'>
a = main.__code__.replace(1, (), b'd\x01}\x01d\x02}\x02d\x03}\x03d\x04}\x04|\x00D\x00]\x1c}\x05|\x05d\x05A\x00}\x05|\x04|\x05\xa0\x00d\x06d\x07\xa1\x02\x17\x00}\x04q\x14|\x04}\x00t\x01d\x02t\x02|\x00\x83\x01d\x08\x83\x03D\x00]\x90}\x05|\x00|\x05|\x05d\x08\x17\x00\x85\x02\x19\x00}\x06d\x01\xa0\x03d\td\n\x84\x00|\x06D\x00\x83\x01\xa1\x01}\x07t\x01d\x02t\x02|\x07\x83\x01d\x0b\x83\x03D\x00]V}\x08|\x07|\x08|\x08d\x0b\x17\x00\x85\x02\x19\x00}\tt\x02|\t\x83\x01d\x0bk\x00r\xc2|\x02d\x0bt\x02|\t\x83\x01\x18\x007\x00}\x02|\td\x0cd\x0bt\x02|\t\x83\x01\x18\x00\x14\x007\x00}\t|\x01|\x03t\x04|\td\r\x83\x02\x19\x007\x00}\x01q~qF|\x01d\x0e|\x02d\r\x1a\x00\x14\x007\x00}\x01t\x01t\x02|\x01\x83\x01d\r\x1a\x00\x83\x01D\x00]L}\x05|\x01|\x05d\r\x14\x00\x19\x00}\n|\x01|\x05d\r\x14\x00d\x06\x17\x00\x19\x00}\x0b|\x01d\x00|\x05d\r\x14\x00\x85\x02\x19\x00|\x0b\x17\x00|\n\x17\x00|\x01|\x05d\r\x14\x00d\r\x17\x00d\x00\x85\x02\x19\x00\x17\x00}\x01q\xf8|\x01S\x00', (None, '', 0, 'ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/', b'', 85, 1, 'little', 3, compile('', '', 'exec').replace(1, (), b'|\x00]\x10}\x01t\x00|\x01d\x00\x83\x02V\x00\x01\x00q\x02d\x01S\x00', ('08b', None), '', 19, 115, (), 0, b'', '', ('format',), 2, 0, 4, ('.0', 'byte'), **('co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_posonlyargcount', 'co_stacksize', 'co_varnames')), '', 6, '0', 2, '!'), '', 4, 67, (), 0, b'', '', ('to_bytes', 'range', 'len', 'join', 'int'), 12, 0, 7, ('data', 'encoded_str', 'padding', 'base64_chars', 'ww', 'i', 'chunk', 'binary_str', 'j', 'six_bits', 'a', 'b'), **('co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_posonlyargcount', 'co_stacksize', 'co_varnames'))
main.__code__ = a
a = main.__code__.replace(1, (), b'd\x01}\x01d\x02}\x02d\x03}\x03d\x04}\x04|\x00D\x00]\x1c}\x05|\x05d\x05A\x00}\x05|\x04|\x05\xa0\x00d\x06d\x07\xa1\x02\x17\x00}\x04q\x14|\x04}\x00t\x01d\x02t\x02|\x00\x83\x01d\x08\x83\x03D\x00]\x90}\x05|\x00|\x05|\x05d\x08\x17\x00\x85\x02\x19\x00}\x06d\x01\xa0\x03d\td\n\x84\x00|\x06D\x00\x83\x01\xa1\x01}\x07t\x01d\x02t\x02|\x07\x83\x01d\x0b\x83\x03D\x00]V}\x08|\x07|\x08|\x08d\x0b\x17\x00\x85\x02\x19\x00}\tt\x02|\t\x83\x01d\x0bk\x00r\xc2|\x02d\x0bt\x02|\t\x83\x01\x18\x007\x00}\x02|\td\x0cd\x0bt\x02|\t\x83\x01\x18\x00\x14\x007\x00}\t|\x01|\x03t\x04|\td\r\x83\x02\x19\x007\x00}\x01q~qF|\x01d\x0e|\x02d\r\x1a\x00\x14\x007\x00}\x01t\x01t\x02|\x01\x83\x01d\r\x1a\x00\x83\x01D\x00]L}\x05|\x01|\x05d\r\x14\x00\x19\x00}\n|\x01|\x05d\r\x14\x00d\x06\x17\x00\x19\x00}\x0b|\x01d\x00|\x05d\r\x14\x00\x85\x02\x19\x00|\x0b\x17\x00|\n\x17\x00|\x01|\x05d\r\x14\x00d\r\x17\x00d\x00\x85\x02\x19\x00\x17\x00}\x01q\xf8|\x01S\x00', (None, '', 0, 'ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/', b'', 85, 1, 'little', 3, compile('', '', 'exec').replace(1, (), b'|\x00]\x10}\x01t\x00|\x01d\x00\x83\x02V\x00\x01\x00q\x02d\x01S\x00', ('08b', None), '', 19, 115, (), 0, b'', '', ('format',), 2, 0, 4, ('.0', 'byte'), **('co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_posonlyargcount', 'co_stacksize', 'co_varnames')), '', 6, '0', 2, '!'), '', 4, 67, (), 0, b'', '', ('to_bytes', 'range', 'len', 'join', 'int'), 12, 0, 7, ('data', 'encoded_str', 'padding', 'base64_chars', 'ww', 'i', 'chunk', 'binary_str', 'j', 'six_bits', 'a', 'b'), **('co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_posonlyargcount', 'co_stacksize', 'co_varnames'))
main.__code__ = a
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)
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)
def main(data):    # def是自己补上的
    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
def main(data):    # def是自己补上的
    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
def rev(encoded_str):
    tmp = encoded_str
    tmp = "".join(tmp[2*i+1]+tmp[2*i] for i in range(len(tmp)//2))
    tmp = tmp.rstrip("!")
    trans = str.maketrans("ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
    tmp2 = tmp.translate(trans)
    tmp2 += "=" * ((4-len(tmp2))%4)
    tmp3 = base64.b64decode(tmp2)
    data = bytes(c ^ 85 for c in tmp3)
    return data.decode()
def rev(encoded_str):
    tmp = encoded_str
    tmp = "".join(tmp[2*i+1]+tmp[2*i] for i in range(len(tmp)//2))
    tmp = tmp.rstrip("!")
    trans = str.maketrans("ZQ+U7tSBEKVzyf5coCwb94Dd6raT0eLNin12Hp8mOxFuvMgIPlhRY3WjksqJAXG/", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
    tmp2 = tmp.translate(trans)
    tmp2 += "=" * ((4-len(tmp2))%4)
    tmp3 = base64.b64decode(tmp2)
    data = bytes(c ^ 85 for c in tmp3)
    return data.decode()
def ztokey(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 = main(b"D7C4197AF0806891")
key = ztokey(z)
m = main(b"D7CHel419lo 7AFWor080ld!6891")
print(z)
print(key)
print(m)
print(rev(m))
def ztokey(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 = main(b"D7C4197AF0806891")
key = ztokey(z)
m = main(b"D7CHel419lo 7AFWor080ld!6891")
print(z)
print(key)
print(m)
print(rev(m))
D7DED6vCn6boDrp3W6v3Zr!!
D7DEdZpKD6vCdrsin6boB6cnDrp3drGYW6v3Zr!!
D7DEbBsZD6vCb53xn6bo2ZmODrp3b5YtW6v3Zr!!
D7CHel419lo 7AFWor080ld!6891
D7DED6vCn6boDrp3W6v3Zr!!
D7DEdZpKD6vCdrsin6boB6cnDrp3drGYW6v3Zr!!
D7DEbBsZD6vCb53xn6bo2ZmODrp3b5YtW6v3Zr!!
D7CHel419lo 7AFWor080ld!6891
def ztokey2(z):
    if len(z) < 20:
        key = 'bBsZb53x2ZmOb5Yt' + z
    else:
        key = z[0:4] + 'bBsZ' + z[4:8] + 'b53x' + z[8:12] + '2ZmO' + z[12:16] + 'b5Yt' + z[16:]
    return key
 
z = main(b"KCTF")
key = ztokey(z)
key2 = ztokey2(z)
print(z)
print(key)
print(key2)
print(rev(key))
print(rev(key2))
def ztokey2(z):
    if len(z) < 20:
        key = 'bBsZb53x2ZmOb5Yt' + z
    else:
        key = z[0:4] + 'bBsZ' + z[4:8] + 'b53x' + z[8:12] + '2ZmO' + z[12:16] + 'b5Yt' + z[16:]
    return key
 
z = main(b"KCTF")
key = ztokey(z)
key2 = ztokey2(z)
print(z)
print(key)
print(key2)
print(rev(key))
print(rev(key2))
nBQ6P7!!
dZpKdrsiB6cndrGYnBQ6P7!!
bBsZb53x2ZmOb5YtnBQ6P7!!
T'00-l5-0(kKCTF
Hello World!KCTF
nBQ6P7!!
dZpKdrsiB6cndrGYnBQ6P7!!
bBsZb53x2ZmOb5YtnBQ6P7!!
T'00-l5-0(kKCTF
Hello World!KCTF
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
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
Traceback (most recent call last):
  File "main.py", line 6, in <module>
AttributeError: 'NoneType' object has no attribute 'main'
Traceback (most recent call last):
  File "main.py", line 6, in <module>
AttributeError: 'NoneType' object has no attribute 'main'
...
print("请输入账号:")
h = input()
z = CrackMe.main(h)
...
...
print("请输入账号:")
h = input()
z = CrackMe.main(h)
...
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
 
__builtins__["input"] = hook_input

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//