Back

Reverse-DigitalCircuits

SUSCTF 2022 Reverse

SUSCTF 2022 DigitalCircuits

前置分析

下载是一个main.exe,根据ico可以知道是python生成的exe文件,python中通过pyinstaller进行py转exe,在默认情况下,并且只有一个exe文件,所以猜测应该是-F进行转换,所以此时可以尝试用pyinstxtractor进行解包:

image-20220302164341202

生成一个_extracted文件夹:

image-20220302164535713

进入文件夹找到同名的pyc文件,对比正常的stuct.pyc文件发现少了16个字节的py头:

image-20220302171504559

将py头复制过去即可反编译,这里无法用在线反编译猜测应该是文件过大,所以采用python进行反编译:

1
pip3 install uncompyle6

反编译(需要注意python的版本,3.9以上的好像反编译会失败)

1
uncompyle6 -o ./ DigitalCircuits.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
 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
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
# uncompyle6 version 3.8.0
# Python bytecode 3.7.0 (3394)
# Decompiled from: Python 3.9.6 (tags/v3.9.6:db3ff76, Jun 28 2021, 15:26:21) [MSC v.1929 64 bit (AMD64)]
# Embedded file name: DigitalCircuits.py
# Compiled at: 1995-09-28 00:18:56
# Size of source mod 2**32: 257 bytes
import time

def f1(a, b):
    if a == '1':
        if b == '1':
            return '1'
    return '0'


def f2(a, b):
    if a == '0':
        if b == '0':
            return '0'
    return '1'


def f3(a):
    if a == '1':
        return '0'
    if a == '0':
        return '1'


def f4(a, b):
    return f2(f1(a, f3(b)), f1(f3(a), b))


def f5(x, y, z):
    s = f4(f4(x, y), z)
    c = f2(f1(x, y), f1(z, f2(x, y)))
    return (s, c)


def f6(a, b):
    ans = ''
    z = '0'
    a = a[::-1]
    b = b[::-1]
    for i in range(32):
        ans += f5(a[i], b[i], z)[0]
        z = f5(a[i], b[i], z)[1]

    return ans[::-1]


def f7(a, n):
    return a[n:] + '0' * n


def f8(a, n):
    return n * '0' + a[:-n]


def f9(a, b):
    ans = ''
    for i in range(32):
        ans += f4(a[i], b[i])

    return ans


def f10(v0, v1, k0, k1, k2, k3):
    s = '00000000000000000000000000000000'
    d = '10011110001101110111100110111001'
    for i in range(32):
        s = f6(s, d)
        v0 = f6(v0, f9(f9(f6(f7(v1, 4), k0), f6(v1, s)), f6(f8(v1, 5), k1)))
        v1 = f6(v1, f9(f9(f6(f7(v0, 4), k2), f6(v0, s)), f6(f8(v0, 5), k3)))

    return v0 + v1


k0 = '0100010001000101'.zfill(32)
k1 = '0100000101000100'.zfill(32)
k2 = '0100001001000101'.zfill(32)
k3 = '0100010101000110'.zfill(32)
flag = input('please input flag:')
if flag[0:7] != 'SUSCTF{' or flag[(-1)] != '}':
    print('Error!!!The formate of flag is SUSCTF{XXX}')
    time.sleep(5)
    exit(0)
flagstr = flag[7:-1]
if len(flagstr) != 24:
    print('Error!!!The length of flag 24')
    time.sleep(5)
    exit(0)
else:
    res = ''
    for i in range(0, len(flagstr), 8):
        v0 = flagstr[i:i + 4]
        v0 = bin(ord(flagstr[i]))[2:].zfill(8) + bin(ord(flagstr[(i + 1)]))[2:].zfill(8) + bin(ord(flagstr[(i + 2)]))[2:].zfill(8) + bin(ord(flagstr[(i + 3)]))[2:].zfill(8)
        v1 = bin(ord(flagstr[(i + 4)]))[2:].zfill(8) + bin(ord(flagstr[(i + 5)]))[2:].zfill(8) + bin(ord(flagstr[(i + 6)]))[2:].zfill(8) + bin(ord(flagstr[(i + 7)]))[2:].zfill(8)
        res += f10(v0, v1, k0, k1, k2, k3)

    if res == '001111101000100101000111110010111100110010010100010001100011100100110001001101011000001110001000001110110000101101101000100100111101101001100010011100110110000100111011001011100110010000100111':
        print('True')
    else:
        print('False')
time.sleep(5)

根据if可以得知传入的参数非0即1,所以可以知道应该是逻辑指令,f1就是&,f2就是|,f3取非,所以这部分就跟题目名称对上了,数电三大基础逻辑运算:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def f1(a, b):
    if a == '1':
        if b == '1':
            return '1'
    return '0'


def f2(a, b):
    if a == '0':
        if b == '0':
            return '0'
    return '1'

def f3(a):
    if a == '1':
        return '0'
    if a == '0':
        return '1'

代入f1-f3得到:(a & ~b) | (~a & b),emm,就是xor,在数电中的基础逻辑语句只有三个,异或虽然在python中是有^这个符号代替的,但实际原理就是题目中的那样进行逻辑操作:

1
2
def f4(a, b):
    return f2(f1(a, f3(b)), f1(f3(a), b))

简化:

1
2
def f4(a, b):
	return a ^ b

f5同样代入:

1
2
3
4
def f5(x, y, z):
    s = f4(f4(x, y), z)
    c = f2(f1(x, y), f1(z, f2(x, y)))
    return (s, c)

得到,没看懂在干啥:

1
2
3
4
def f5(x, y, z):
	s = x ^ y ^ z
	c = (x & y) | (z & (x | y))
	return (s, c)

找到f6调用的位置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def f6(a, b):
    ans = ''
    z = '0'
    a = a[::-1]
    b = b[::-1]
    for i in range(32):
        ans += f5(a[i], b[i], z)[0]
        z = f5(a[i], b[i], z)[1]

    return ans[::-1]


image-20220303120139056

测试一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def f6(a, b):
    ans = ''
    z = '0'
    a = a[::-1]
    b = b[::-1]
    for i in range(32):
        tmp = f5(a[i], b[i], z)
        print(tmp)
        ans += tmp[0]
        z = tmp[1]

    return ans[::-1]

s = '00000000000000000000000000000001'
d = '10011110001101110111100110111001'

res = f6(s, d)
print(res)

得到res: 10011110001101110111100110111010

可以发现f6其实就是做了个二进制加法,传入的两个参数为二进制字符串,对于二进制字符串,字符串的高位为数据低位,所以传入之后会先对字符串进行翻转:

1
2
a = a[::-1]
b = b[::-1]

那么f5的三次异或就可以理解了,最开始z=0,然后传入的x=1, y=1s = x ^ y ^ z = 0,然后第二个c是用来检测是否产生进位,明显0b01 + 0b01 = 0b10,会产生进位,所以c = 1,然后将进位值给到z,代入下一轮循环,最终生成

image-20220303120229954

f7,f8都是传入一个二进制字符串,然后传入一个n,很明显,f7就是左移,f8就是右移,n为位数

1
2
3
4
5
def f7(a, n):
    return a[n:] + '0' * n

def f8(a, n):
    return n * '0' + a[:-n]

f9逐位异或,最终就是整体异或:

1
2
3
4
5
6
def f9(a, b):
    ans = ''
    for i in range(32):
        ans += f4(a[i], b[i])

    return ans

加密代码分析

 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
def f10(v0, v1, k0, k1, k2, k3):
    s = '00000000000000000000000000000000'
    d = '10011110001101110111100110111001'
    for i in range(32):
        s = f6(s, d)
        v0 = f6(v0, f9(f9(f6(f7(v1, 4), k0), f6(v1, s)), f6(f8(v1, 5), k1)))
        v1 = f6(v1, f9(f9(f6(f7(v0, 4), k2), f6(v0, s)), f6(f8(v0, 5), k3)))

    return v0 + v1


k0 = '0100010001000101'.zfill(32)
k1 = '0100000101000100'.zfill(32)
k2 = '0100001001000101'.zfill(32)
k3 = '0100010101000110'.zfill(32)
flag = input('please input flag:')
if flag[0:7] != 'SUSCTF{' or flag[(-1)] != '}':
    print('Error!!!The formate of flag is SUSCTF{XXX}')
    time.sleep(5)
    exit(0)
flagstr = flag[7:-1]
if len(flagstr) != 24:
    print('Error!!!The length of flag 24')
    time.sleep(5)
    exit(0)
else:
    res = ''
    for i in range(0, len(flagstr), 8):
        v0 = flagstr[i:i + 4]
        v0 = bin(ord(flagstr[i]))[2:].zfill(8) + bin(ord(flagstr[(i + 1)]))[2:].zfill(8) + bin(ord(flagstr[(i + 2)]))[2:].zfill(8) + bin(ord(flagstr[(i + 3)]))[2:].zfill(8)
        v1 = bin(ord(flagstr[(i + 4)]))[2:].zfill(8) + bin(ord(flagstr[(i + 5)]))[2:].zfill(8) + bin(ord(flagstr[(i + 6)]))[2:].zfill(8) + bin(ord(flagstr[(i + 7)]))[2:].zfill(8)
        res += f10(v0, v1, k0, k1, k2, k3)

    if res == '001111101000100101000111110010111100110010010100010001100011100100110001001101011000001110001000001110110000101101101000100100111101101001100010011100110110000100111011001011100110010000100111':
        print('True')
    else:
        print('False')

首先是限制了flag格式为SUSCTF{xxx}flag正文内容为24字节,进行三次循环,每次循环取8个字节,分成前后4个字节,然后拼接成32位的字符串,bin(ord(flagstr[i]))[2:].zfill(8)字符转二进制字符串

1
2
3
4
5
for i in range(0, len(flagstr), 8):
        v0 = flagstr[i:i + 4]
       v0 = bin(ord(flagstr[i]))[2:].zfill(8) + bin(ord(flagstr[(i + 1)]))[2:].zfill(8) + bin(ord(flagstr[(i + 2)]))[2:].zfill(8) + bin(ord(flagstr[(i + 3)]))[2:].zfill(8)
        v1 = bin(ord(flagstr[(i + 4)]))[2:].zfill(8) + bin(ord(flagstr[(i + 5)]))[2:].zfill(8) + bin(ord(flagstr[(i + 6)]))[2:].zfill(8) + bin(ord(flagstr[(i + 7)]))[2:].zfill(8)
        res += f10(v0, v1, k0, k1, k2, k3)

image-20220303160031426

处理完字符串之后通过f10进行处理,这里传入了k0-k3应该是4个key:

1
2
3
4
k0 = '0100010001000101'.zfill(32)
k1 = '0100000101000100'.zfill(32)
k2 = '0100001001000101'.zfill(32)
k3 = '0100010101000110'.zfill(32)

转成16进制看看:

1
2
3
4
5
6
7
def bin2hex(k):
	return hex(int(k, 2))
	
k0 = bin2hex(k0)
k1 = bin2hex(k1)
k2 = bin2hex(k2)
k3 = bin2hex(k3)

image-20220303161759144

将d也转成16进制,发现TEA算法特征

image-20220304103329684

f10代入功能代码进行简化,可以得到f10就是TEA算法的加密过程:

1
2
3
4
5
6
7
8
9
def f10(v0, v1, k0, k1, k2, k3):
    s = '00000000000000000000000000000000'
    d = '10011110001101110111100110111001' # 0x9e3779b9
    for i in range(32):
        s = s + d
        v0 = v0 + ((v1 << 4) + k0) ^ (v1 + s) ^ ((v1 >> 5) + k1)	
		v1 = v1 + ((v0 << 4) + k2) ^ (v0 + s) ^ ((v0 >> 5) + k3)

    return v0 + v1

测试:

1
2
3
4
5
6
7
8
9
flagstr = 'abcdefgh'
v0 = bin(ord(flagstr[0]))[2:].zfill(8) + bin(ord(flagstr[1]))[2:].zfill(8) + bin(ord(flagstr[2]))[2:].zfill(8) + bin(ord(flagstr[3]))[2:].zfill(8)
v1 = bin(ord(flagstr[4]))[2:].zfill(8) + bin(ord(flagstr[5]))[2:].zfill(8) + bin(ord(flagstr[6]))[2:].zfill(8) + bin(ord(flagstr[7]))[2:].zfill(8)
k0 = '0100010001000101'.zfill(32)
k1 = '0100000101000100'.zfill(32)
k2 = '0100001001000101'.zfill(32)
k3 = '0100010101000110'.zfill(32)

print(f10(v0, v1, k0, k1, k2, k3))

每次循环的结果是二进制字符串的拼接:

image-20220304105641879

每次循环取flag的8个字节,加密结果互不影响,所以最终拼接会得到192个字节的长度,与res进行比较:

那么只需要将res按64字节分成三份,分别进行解密即可得到真正的flag

Exp

 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
from ctypes import *

class DigitalCircuits():
    def __init__(self) :
        self.k0 = int('0100010001000101', 2)
        self.k1 = int('0100000101000100', 2)
        self.k2 = int('0100001001000101', 2)
        self.k3 = int('0100010101000110', 2)
        self.res = '001111101000100101000111110010111100110010010100010001100011100100110001001101011000001110001000001110110000101101101000100100111101101001100010011100110110000100111011001011100110010000100111'

    def decrypt(self, v):
        v0, v1 = c_uint32(v[0]), c_uint32(v[1])
        delta = 0x9e3779b9 

        total = c_uint32(delta * 32)
        for i in range(32):                       
            v1.value -= ((v0.value<<4) + self.k2) ^ (v0.value + total.value) ^ ((v0.value>>5) + self.k3) 
            v0.value -= ((v1.value<<4) + self.k0) ^ (v1.value + total.value) ^ ((v1.value>>5) + self.k1)  
            total.value -= delta

        return hex(v0.value)[2:] + hex(v1.value)[2:]

    def bin2hex(self, a):
        return hex(int(a, 2))

    def bin2int(self, a):
        return int(a, 2)

    def split_flag(self):
        flag = []
        for i in range(3):
            flag.append(self.bin2int(self.res[i*64:(i+1)*64]))
        return flag

    def run(self):
        flag_enc = self.split_flag()
        flag = ''
        for flag_seg in flag_enc:
            tmp = self.decrypt([flag_seg >> 32, flag_seg & 0xffffffff])
            for i in range(0, len(tmp), 2):
                flag += chr(int(tmp[i:i+2], 16))
        
        print('SUSCTF{'+ flag + '}')

dc = DigitalCircuits()
dc.run()

得到flag:

1
SUSCTF{XBvfaEdQvbcrxPBh8AOcJ6gA}

总结

考点

  • pyinstaller打包exe文件的反编译
  • 基础TEA算法加解密

比起以往的XCTF联赛逆向,这次的题目感觉像是入门题了,整体难度不高,该题的难点在于将TEA算法用二进制的形式进行描述,但只要将功能代码分析代入简化,便能轻松秒杀

YuSec Github Blog
Built with Hugo
Theme Stack designed by Jimmy