【CTF】2023hws冬令营选拔赛逆向部分WP/python3 pyc字节码&花指令
浏览 97 | 评论 2 | 字数 13464
Xunflash
2023年01月09日
  • babyre

    image-20230107112127322

    程序替换了dex,新的dex可以在手机数据目录/data/user/0/com.me.crackme下找到

    image-20230109094517893

    easyre

    image-20230109094607897

    key是伪随机,动调拿出来即可

    image-20230109094627403

    懒得改代码了,脚本的v值我是手动替换拼起来结果的。

    #include <stdio.h>  
    #include <stdint.h>  
      
    /* take 64 bits of data in v[0] and v[1] and 128 bits of key[0] - key[3] */  
      
    void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {  
        unsigned int i;  
        uint32_t v0=v[0], v1=v[1], sum=0, delta=0x9E3779B9;  
        for (i=0; i < num_rounds; i++) {  
            v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);  
            sum += delta;  
            v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);  
        }  
        v[0]=v0; v[1]=v1;  
    }  
      
    void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {  
        unsigned int i;  
        uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*num_rounds;  
        for (i=0; i < num_rounds; i++) {  
            v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);  
            sum -= delta;  
            v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);  
        }  
        v[0]=v0; v[1]=v1;  
    }  
      
    int main()  
    {  
        uint32_t v[2]={0xF191C71E, 0x6790767B};  
        uint32_t const k[4]={0x00000029, 0x00004823, 0x000018BE, 0x00006784};  
        unsigned int r=32;//num_rounds建议取值为32  
        // v为要加密的数据是两个32位无符号整数  
        // k为加密解密密钥,为4个32位无符号整数,即密钥长度为128位  
        // printf("加密前原始数据:%u %u\n",v[0],v[1]);  
        // encipher(r, v, k);  
        // printf("加密后的数据:%u %u\n",v[0],v[1]);  
        decipher(r, v, k);  
        for (int i = 0; i < 2; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                printf("%c", (v[i] >> (j * 8)) & 0xFF);
            }
        }
        // printf("解密后的数据:%x%x\n",v[0],v[1]);  
        return 0;  
    }  

    pyre

    一些牢骚话

    这题我只能说 有出的好的地方,但是不好的地方有点多。

    先说不好的,首先就是我对这种往题目里面硬塞答辩ollvm的感到非常的不认同。十个题目有三个都在往里面塞ollvm,有的是魔改的,说实话这种东西对熟练的来说就是脚本或者插件一把嗦,你塞魔改的那是出题人有能力,能把大家难倒又能让大家学习ollvm相关的东西,但是塞正常的ollvm,每个函数都塞依托,那是纯纯答辩。

    其次,脑洞成分太多了,多到我觉得是misc题目。最首要的就是那个38 giant snakes,还加了个yesterday。不得不说当一个misc题可能是好的,我还特意去google限制时间搜了一下题目文件修改时间的前一天,还真搜到38 giant snakes的相关信息。然后有个python的字眼,这才想到是python38。还有这个aes加密函数 我一开始以为是题目用了一些手法调用了这个函数对.bin文件进行了一些操作,但是交叉引用又找不到,我就猜想可能是不是在init里面调用了 然后没有。我以为这题在玩一些很新的东西,用了一些我不知道的技术去调用这个函数,结果是没有的,就纯是出题人把这个函数留在题目里面,然后让大家猜这个iv和key。好这我也能接受,但是第三部分这个假flag,输入进题目你跳congratulations是什么意思,以前从来没有遇见过哪个题是说输假flag弹出congratulations的,挺抽象的。

    最后说一下我觉得好的地方,第一步这个加密就比较新,在玩一种新的东西。让猜数字和映射相同,我是手动推理出来的,首先肯定能想到偏后位置的一定是0,并且0居多,根据这个逻辑 十几分钟就能推理出来这个结果。

    套个盾 这以上都是我自己的看法,出题人如果看到了也不要打我,我纯彩笔没做出来这个题目 还在群里问flag交不上去。

    wp

    首先打开这个题目可以看到一些IO

    image-20230109230303680

    check1

    很容易就能看出来check1是这个sub_401280,点进去是依托ollvm,这里直接使用d810就能解决。但是我的电脑有点小问题 ,还原出来不是很好看,于是直接断点在比较的这个位置看数据猜一下加密。很容易就猜出来了是一个十位数,让数字出现的次数等于其映射位置上的数。手动推理一下,首先肯定能想到偏后位置的一定是0,并且0居多,根据这个逻辑 十几分钟就能推理出来结果是6210001000

    image-20230109230429957

    check2

    然后是check2,不知道是出题人失误还是怎么回事,这个用来check的md5能在线查到啊。前面的加密都没用了。sub_401600就是checkmd5的位置。image-20230109230824819

    这里可以直接看到这个md5.

    image-20230109230855366

    check3

    这个部分是靠脑洞,首先根据38 giant snakes得知是python38 python即巨蛇。然后题目内有出题人留的aes加密magic.file到secret.bin的函数。往上看看可以知道key和iv是从函数参数传过来的。

    image-20230109231114906

    这一部分密文经过后面的异或就变成了文件名,最后是将magic.file进行aes cbc加密再写入secret.bin这样一个过程。

    image-20230109231200344

    函数参数不知道,就只能猜是已知信息,已知信息里面check2的字符是十六位,那只能用这个当iv和key,解出来一个python38的pyc。可以直接用pycdas还原出字节码,但是pycdc还原python代码只有一部分,decode函数没了。观察一下pycdas的可以发现里面有一些invalid的代码,可以考虑把这一部分代码全部改成09(pyc字节码的NOP。具体的值可以在python对应版本的opcode.h找到)pyc反编译的工具对于指令要求较高,不能识别的指令会直接导致分析中断。image-20230109232123801

    这里说到pyc字节码,根据ctfwiki misc板块下的pyc文件可以知道,python3.6一下的字节码是1位或者3位字节的变长指令,而python3.8则是固定的一位指令,带上参数即是两个字节。那么我们可以根据pycdas出来的代码找到在文件中对应的位置:例如这三句,最左边的是指令位数,中间的是指令名称,右边的是指令参数。我们可以通过查找对应的opcode.h,把下面这三句翻译成字节即是(64 0F)(5A 14)(6E 24)

            152     LOAD_CONST              15: 2
            154     STORE_NAME              20: res
            156     JUMP_FORWARD            36 (to 194)

    image-20230109232522263

    相对应的,我们可以用010editor在文件内找到对应的位置:

    image-20230109233001304

    可以通过相同的方式找到这些invalid指令后面的那一条指令,从而实现用09替换这些指令。

    image-20230109233204192

    题目两处都用这种方法替换之后能识别出decode函数了,但是main函数还是有问题,这里我就不明白具体问题了,只有三种可能,一种是pycdc不支持try except finally这种写法的代码,一种是在其他地方还有一些花指令导致识别出错,一种是nop的修改还不足以让pycdc恢复识别。我觉得比较可能是其他地方还有花指令。

    最后我们使用pycdc能得到一个大致的代码

    import string
    
    ascii_uppercase = string.ascii_uppercase
    ascii_lowercase = string.ascii_lowercase
    digits = string.digits
    
    class II00O0III0o0o0o0oo:
    
        def __init__(self):
            table = 0
            length = 1
            temptable = 2
            for _ in range(10):
                table += length
                length ^= table
                temptable *= temptable
            basetable = ascii_uppercase + ascii_lowercase + digits + '+/'
            self.table = basetable
            length = len(self.table)
            temptable = list(self.table.encode('utf-8', **('encoding',)))
            for i in range(0, length // 2):
                for j in range(0, length - 1 - i):
                    if temptable[j] > temptable[j + 1]:
                        temp = temptable[j]
                        temptable[j] = temptable[j + 1]
                        temptable[j + 1] = temp
                        continue
                        continue
                        basetable = bytes(temptable).decode()
                        self.table = basetable
                        return None
    
    
        def e(self = None, msg = None):
            BASE_CHAR = self.table
            CHARSET = 'ASCII'
            b_msg = bytes(msg, CHARSET)
            CHAR_PRE_GROUP = 3
            zero_cnt = CHAR_PRE_GROUP - len(b_msg) % CHAR_PRE_GROUP
            if zero_cnt == CHAR_PRE_GROUP:
                zero_cnt = 0
            msg += str(chr(0)) * zero_cnt
            b_msg = bytes(msg, CHARSET)
            msg = 0
            encoded = 1
            i_msg = 2
            for _ in range(20):
                encoded += i_msg
                msg ^= encoded
                i_msg *= i_msg
            encoded = ''
            for i in range(0, len(b_msg), 3):
                i_msg = (lambda .0: [ int(i) for i in .0 ])(b_msg[i:i + 3])
                new_msg = [
                    None] * 4
                new_msg[0] = i_msg[0] >> 2
                new_msg[1] = ((i_msg[0] & 3) << 6 | i_msg[1] >> 2) >> 2
                new_msg[2] = ((i_msg[1] & 15) << 4 | i_msg[2] >> 4) >> 2
                new_msg[3] = i_msg[2] & 63
                None += None((lambda .0 = None: [ BASE_CHAR[i] for i in .0 ])(new_msg))        
            return encoded + '=' * zero_cnt
    
    
    
    class IIoo00IIIo0o0oo0oo:
    
        def __init__(self):
            self.d = 0x87654321
            k0 = 1732584193
            k1 = 0xEFCDAB89
            k2 = 0x98BADCFE
            k3 = 271733878
            self.k = [
                k0,
                k1,
                k2,
                k3]
    
    
        def e(self, n, v):
            c_uint32 = c_uint32
            import ctypes
    
            def MX(z = None, y = None, total = None, key = None, p = None, e = None):
                temp1 = (z.value >> 6 ^ y.value << 4) + (y.value >> 2 ^ z.value << 5)
                temp2 = (total.value ^ y.value) + (key[p & 3 ^ e.value] ^ z.value)
                return c_uint32(temp1 ^ temp2)
    
            key = self.k
            delta = self.d
            rounds = 6 + 52 // n
            total = 0
            z = 1
            e = 2
            for _ in range(10):
                z += e
                e ^= total
                e *= z
            total = c_uint32(0)
            z = c_uint32(v[n - 1])
            e = c_uint32(0)
            if rounds > 0:
                total.value += delta
                e.value = total.value >> 2 & 3
                for p in range(n - 1):
                    y = c_uint32(v[p + 1])
                    v[p] = c_uint32(v[p] + MX(z, y, total, key, p, e).value).value
                    z.value = v[p]
                y = c_uint32(v[0])
                v[n - 1] = c_uint32(v[n - 1] + MX(z, y, total, key, n - 1, e).value).value
                z.value = v[n - 1]
                rounds -= 1
            return v
    
    
    
    class chall:
    
        def __init__(self):
            self.c1 = II00O0III0o0o0o0oo()
            self.c2 = IIoo00IIIo0o0oo0oo()
    
    
        def ints2bytes(self = None, v = None):
            n = len(v)
            res = b''
            for i in range(n // 2):
                res += int.to_bytes(v[2 * i], 4, 'little')
                res += int.to_bytes(v[2 * i + 1], 4, 'little')
            return res
    
    
        def bytes2ints(self = None, cs = None):
            new_length = len(cs) + (8 - len(cs) % 8) % 8
            barray = cs.ljust(new_length, b'\x00')
            i = 0
            v = []
            if i < new_length:
                v0 = int.from_bytes(barray[i:i + 4], 'little')
                v1 = int.from_bytes(barray[i + 4:i + 8], 'little')
                v.append(v0)
                v.append(v1)
                i += 8
            return v
    
    
        def decode(self = None, text = None):
            code1 = self.c1.e(text)
            length = len(code1)
            padding_length = length + (8 - length % 8) % 8
            code2 = code1.ljust(padding_length, '\x00').encode()
            v = self.bytes2ints(code2)
            n = len(v)
            code1 = 0
            code3 = 1
            code4 = 2
            code3 = self.c2.e(n, v)
            code4 = self.ints2bytes(code3)
            return code4
    
    
    
    def check(instr = None, checklist = None):
        cipher = chall()
        output = list(cipher.decode(instr))
        i = 0
        if i < len(checklist) and i < len(output) and output[i] == checklist[i]:
            i += 1
    
        if i == len(checklist):
            return 1
    
    if __name__ == '__main__':
        with open('encrypted', 'rb') as rf:
            data = list(rf.read())
    
    check_list = data[:len(data) // 2]
    check_list1 = data[len(data) // 2:]
    print(check_list)
    print(check_list1)
    flag = input('Please input the last part of the flag:')

    main函数可以使用chatgpt恢复一小部分,直接把字节码丢进去是完全错误的。

    以上,可以得出check3的大部分逻辑,小部分需要自己猜测。(我就是因为这部分没猜到没做出来(悲))

    这一小部分的用了一个异常处理,看起来好像是除0异常,然后跳到了finally里面,用encrypt后文件半部分作为xorlist,前半部分作为checklist,然后异或,再调用check函数。

    看代码可以看到就是一个魔改xxtea和一个换表base64,这个换表还是原题的,我直接拿别人wp里面的表就能用。

    总结出来解密流程就是先把encrypt文件前半部分和后半部分异或,再xxtea解密,再换表base64解密。

    魔改xxtea

    #include <stdio.h>  
    #include <stdint.h>  
    #define DELTA 0x87654321
    #define MX (((z>>6^y<<4) + (y>>2^z<<5)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))  
      
    void btea(uint32_t *v, int n, uint32_t const key[4])  
    {  
        uint32_t y, z, sum;  
        unsigned p, rounds, e;  
        if (n > 1)            /* Coding Part */  
        {  
            rounds = 6 + 52/n;  
            sum = 0;  
            z = v[n-1];  
            do  
            {  
                sum += DELTA;  
                e = (sum >> 2) & 3;  
                for (p=0; p<n-1; p++)  
                {  
                    y = v[p+1];  
                    z = v[p] += MX;  
                }  
                y = v[0];  
                z = v[n-1] += MX;  
            }  
            while (--rounds);  
        }  
        else if (n < -1)      /* Decoding Part */  
        {  
            n = -n;  
            rounds = 6 + 52/n;  
            sum = rounds*DELTA;  
            y = v[0];  
            do  
            {  
                e = (sum >> 2) & 3;  
                for (p=n-1; p>0; p--)  
                {  
                    z = v[p-1];  
                    y = v[p] -= MX;  
                }  
                z = v[n-1];  
                y = v[0] -= MX;  
                sum -= DELTA;  
            }  
            while (--rounds);  
        }  
    }  
      
      
    int main()  
    {  
        // uint32_t v[10]= {2381651913,154517149,2207197885,2948914538,1297438674,2778237469,3899242504,536948396,2740260929,2014530248};  
        // uint32_t v[10]= {3374314893,2646488329,3173683075,1792918703,3528938829,495622309,146303464,2888696096,1091589539,3360297848};
        // uint32_t v[10]= {243744876,1449451470,858997228,2635499997,3422466335,2668982149,915379719,3379046531,3428777632,3034478195};
        
        uint32_t v[10]= {0x83725fa5,0x5f516153,0xb0bc6b51,0x32d254b7,0x86abe6cd,0x3a8de598,0xdee62e0f,0xe969062f,0x6f0beee1,0xcccd3cbb};
        uint32_t const k[4]= {1732584193,0xEFCDAB89,0x98BADCFE,271733878};
        int n=10; //n的绝对值表示v的长度,取正表示加密,取负表示解密  
        // v为要加密的数据是两个32位无符号整数  
        // k为加密解密密钥,为4个32位无符号整数,即密钥长度为128位  
        // printf("加密前原始数据:%u %u\n",v[0],v[1]);  
        // btea(v, n, k);  
        // printf("加密后的数据:%u %u\n",v[0],v[1]);  
        btea(v, -n, k);  
        // printf("解密后的数据:%x %x\n",v[0],v[1]);  
        for (int i = 0; i < 10; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                printf("%c", (v[i] >> (j * 8)) & 0xFF);
            }
        }
        return 0;  
    }  

    换表base64直接用cyberchef,怎么快怎么来就行

    image-20230109234148637

    最后拼起来即可

    flag{6210001000_yOUar3g0oD@tc4nd_PytH0n_KZBxDwfkIzbEgUOY}

    本文作者:Xunflash
    本文链接:http://xunflash.top/index.php/archives/2022hwswinter.html
    最后修改时间:2023-01-10 20:40:58
    本站未注明转载的文章均为原创,并采用 CC BY-NC-SA 4.0 授权协议,转载请注明来源,谢谢!
    评论
    114514
    textsms
    支持 Markdown 语法
    email
    link
    评论列表
    已有 2 条评论
    enllus1on
    2023-01-10 16:37
    巡抚好腻害 😘
    2023-01-10 22:15
    @enllus1on 大爹,等你带我