【CTF】2023hws冬令营选拔赛逆向部分WP/python3 pyc字节码&花指令

· 2023-01-09 · 1541 人浏览

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}

  1. enllus1on 2023-01-10

    巡抚好腻害 😘

    1. Xunflash (作者)  2023-01-10
      @enllus1on

      大爹,等你带我

Theme Jasmine by Kent Liao