BUUCTF clocks in every day on July 24, 2021

Posted by Nic on Fri, 14 Jan 2022 08:55:18 +0100

introduction

Typhoon day, also practiced a day's car, Gan

[SUCTF2019]MT

To fill the hole
First look at the encryption code:

from Crypto.Util import number
from flag import flag

def convert(m):
    m = m ^ m >> 13
    m = m ^ m << 9 & 2029229568
    m = m ^ m << 17 & 2245263360
    m = m ^ m >> 19
    return m

def transform(message):
    assert len(message) % 4 == 0
    new_message = ''
    for i in range(len(message) / 4):
        block = message[i * 4 : i * 4 +4]
        block = number.bytes_to_long(block)
        block = convert(block)
        block = number.long_to_bytes(block, 4)
        new_message += block
    return new_message

transformed_flag = transform(flag[5:-1].decode('hex')).encode('hex')
print 'transformed_flag:', transformed_flag
# transformed_flag: 641460a9e3953b1aaa21f3a2

Seeing this convert function, I was a little timid
This bit operation seems to completely disrupt the encrypted content (and I haven't done similar problems before)
Especially the second and third steps, I feel completely confused
Took a look Other people's wp , inspired
It is found that the last 9 bits of bin(2029229568) are all 0, while the last 17 bits (actually 18 bits) of bin(2245263360) are all 0, which is the same as the shifted bits
First of all, we should clarify one thing: the priority shift (> > and < < > and operation (&) > XOR operation (^) of bit operators
Take 'abcd' as an example to introduce the decryption process of the fourth and third steps (the first and second steps are the same)
Because I didn't install the environment of python2, I rewritten the encryption code with python3 and made a little modification
The code is as follows:

from Crypto.Util.number import *

flag = 'abcd'

def convert(m):
    m = m ^ m >> 13
    m = m ^ m << 9 & 2029229568
    m = m ^ m << 17 & 2245263360
    m = m ^ m >> 19
    return m

def transform(message):
    assert len(message) % 4 == 0
    new_message = ''
    for i in range(len(message) // 4):
        block = message[i * 4: i * 4 + 4]
        block = bytes_to_long(block.encode())
        block = convert(block)
        block = bin(block)[2:].zfill(32)
        new_message += block
    return long_to_bytes(int(new_message, 2))

cipher = transform(flag)

Step 4 operation

First of all, you should know that "> >" means that the binary bit moves to the right, the low bit is discarded, and the high bit is supplemented by 0
The binary result after the first three steps of encryption is (of course, the following results are unknown during decryption):
10100101011101011110111001110111 is recorded as m4
Then the result of M4 > > 19 is
0000000000000000000101001010111 01011110111001110111
High complement 0 < -- > low discard
Next is the XOR operation
Two properties of XOR operation are required for decryption:
1. If a=b ^ c, then c=a ^ b
2,a=a ^ 0
Cipher = M4 ^ M4 > > 19, where cipher is known
In the process of bit-by-bit encryption, the first 19 bits of m4 are XOR with 0, that is, the first 19 bits of cipher are the same as m4, so the first 19 bits of m4 can be obtained
And because the last 13 bits of m4 ^ m4 > > 19 = the last 13 bits of m4 ^ the first 13 bits of m4 = the last 13 bits of cipher
So the last 13 bits of cipher ^ the first 13 bits of m4 = the last 13 bits of m4
Thus, the complete m4 can be obtained
The decryption code is as follows:

block = bytes_to_long((cipher[i * 4: i * 4 + 4]))
block = bin(block)[2:].zfill(32)
# step4 decode
m4 = block[:19] + bin(int(block[:13], 2) ^ int(block[19:], 2))[2:].zfill(13)
print(m4)
print(long_to_bytes(int(m4, 2)))

Step 3 operation

First of all, you should know that "< <" is that the binary bit moves to the left, the high bit is discarded, and the low bit is filled with 0; & " It is a bitwise and operation. There are 0 out of 0 and all 1 out of 1
The binary result after the first two steps of encryption is (of course, the following results are unknown during decryption):
001000010110001111101111001110111 is recorded as m3
00100001101100011 11011100111011100000000000000000
High discard < -- > low complement 0
Next is the sum operation
First, let's look at bin(2245263360) = '0b1000010110101000000000000000000000000000000'
It is found that the last 17 bits of 2245263360 are 0
Then, the last 17 digits of M3 < < 17 & the last 17 digits of 2245263360 = 00000000000000000 (17 zeros)
The same as the decryption operation in step 4, we can get that the last 17 bits of m3 are the last 17 bits of m4
And because the first 15 digits of M3 ^ m3 < < 17 & the first 15 digits of 2245263360 = the first 15 digits of M3 ^ the last 15 digits of M3 & the first 15 digits of 2245263360 = the first 15 digits of m4
Thus, the complete m3 can be obtained
The decryption code is as follows:

# step3 decode
block = m4
m3 = bin((int(block[17:], 2) & int(bin(2245263360)[2:].zfill(32)[:15], 2)) ^ int(block[:15], 2))[2:].zfill(15) + block[15:]
print(m3)
print(long_to_bytes(int(m3, 2)))

The second step is similar to the decryption in the first step, that is, it needs to be repeated several times
The complete test decryption code is as follows:

from Crypto.Util.number import *

flag = 'abcd'

def convert(m):
    m = m ^ m >> 13
    m = m ^ m << 9 & 2029229568
    m = m ^ m << 17 & 2245263360
    m = m ^ m >> 19
    return m

def transform(message):
    assert len(message) % 4 == 0
    new_message = ''
    for i in range(len(message) // 4):
        block = message[i * 4: i * 4 + 4]
        block = bytes_to_long(block.encode())
        block = convert(block)
        block = bin(block)[2:].zfill(32)
        new_message += block
    return long_to_bytes(int(new_message, 2))

cipher = transform(flag)
print(bin(bytes_to_long(cipher))[2:].zfill(32))
print(cipher)
plain = ''
for i in range(len(cipher) // 4):
    block = bytes_to_long((cipher[i * 4: i * 4 + 4]))
    block = bin(block)[2:].zfill(32)
    # step4 decode
    m4 = block[:19] + bin(int(block[:13], 2) ^ int(block[19:], 2))[2:].zfill(13)
    print(m4)
    print(long_to_bytes(int(m4, 2)))
    # step3 decode
    block = m4
    m3 = bin((int(block[17:], 2) & int(bin(2245263360)[2:].zfill(32)[:15], 2)) ^ int(block[:15], 2))[2:].zfill(15) + block[15:]
    print(m3)
    print(long_to_bytes(int(m3, 2)))
    # step2 decode
    block = m3
    m2 = bin((int(block[23:], 2) & int(bin(2029229568)[2:].zfill(32)[14:23], 2)) ^ int(block[14:23], 2))[2:].zfill(9) + block[23:]
    block = m3[:14] + m2
    m2 = bin((int(block[14:23], 2) & int(bin(2029229568)[2:].zfill(32)[5:14], 2)) ^ int(block[5:14], 2))[2:].zfill(9) + block[14:]
    block = m3[:5] + m2
    m2 = bin((int(block[9:14], 2) & int(bin(2029229568)[2:].zfill(32)[:5], 2)) ^ int(block[:5], 2))[2:].zfill(5) + block[5:]
    print(m2)
    print(long_to_bytes(int(m2, 2)))
    # step1 decode
    block = m2
    m1 = block[:13] + bin(int(block[:13], 2) ^ int(block[13:26], 2))[2:].zfill(13)
    block = m1 + block[26:]
    m1 = block[:26] + bin(int(block[13:19], 2) ^ int(block[26:], 2))[2:].zfill(6)
    print(m1)
    print(long_to_bytes(int(m1, 2)))
    plain += m1
print(long_to_bytes(int(plain, 2)))

The result is right

Then the test decrypted code is applied to transformed_ On flag
There is a bit of trouble here, that is, the original encryption code needs another step: transformed_flag = transform(flag[5:-1].decode('hex')).encode('hex')
Python 3 does not have decode('hex ') and encode('hex'), which can be used for reference Conversion between Python 3 string and hex
. decode('hex ') corresponds to hex(),. encode('hex ') corresponds to bytes fromhex()
Therefore, the complete decryption code is as follows:

from Crypto.Util.number import *

cipher = bytes.fromhex('641460a9e3953b1aaa21f3a2')
print(cipher)
plain = ''
for i in range(len(cipher) // 4):
    block = bytes_to_long((cipher[i * 4: i * 4 + 4]))
    block = bin(block)[2:].zfill(32)
    # step4 decode
    m4 = block[:19] + bin(int(block[:13], 2) ^ int(block[19:], 2))[2:].zfill(13)
    print(m4)
    print(long_to_bytes(int(m4, 2)))
    # step3 decode
    block = m4
    m3 = bin((int(block[17:], 2) & int(bin(2245263360)[2:].zfill(32)[:15], 2)) ^ int(block[:15], 2))[2:].zfill(15) + block[15:]
    print(m3)
    print(long_to_bytes(int(m3, 2)))
    # step2 decode
    block = m3
    m2 = bin((int(block[23:], 2) & int(bin(2029229568)[2:].zfill(32)[14:23], 2)) ^ int(block[14:23], 2))[2:].zfill(9) + block[23:]
    block = m3[:14] + m2
    m2 = bin((int(block[14:23], 2) & int(bin(2029229568)[2:].zfill(32)[5:14], 2)) ^ int(block[5:14], 2))[2:].zfill(9) + block[14:]
    block = m3[:5] + m2
    m2 = bin((int(block[9:14], 2) & int(bin(2029229568)[2:].zfill(32)[:5], 2)) ^ int(block[:5], 2))[2:].zfill(5) + block[5:]
    print(m2)
    print(long_to_bytes(int(m2, 2)))
    # step1 decode
    block = m2
    m1 = block[:13] + bin(int(block[:13], 2) ^ int(block[13:26], 2))[2:].zfill(13)
    block = m1 + block[26:]
    m1 = block[:26] + bin(int(block[13:19], 2) ^ int(block[26:], 2))[2:].zfill(6)
    print(m1)
    print(long_to_bytes(int(m1, 2)))
    plain += m1
print(long_to_bytes(int(plain, 2)))
print(long_to_bytes(int(plain, 2)).hex())

The result is:

flag

An unimaginable path

As mentioned in wp, there is also a solution to get the result through repeated encryption
Here is the code of Python 3:

from Crypto.Util.number import *

def convert(m):
    m = m ^ m >> 13
    m = m ^ m << 9 & 2029229568
    m = m ^ m << 17 & 2245263360
    m = m ^ m >> 19
    return m

def transform(message):
    assert len(message) % 4 == 0
    new_message = b''
    for i in range(len(message) // 4):
        block = message[i * 4: i * 4 + 4]
        block = bytes_to_long(block)
        block = convert(block)
        block = long_to_bytes(block, 4)
        new_message += block
    return new_message

transformed_flag = '641460a9e3953b1aaa21f3a2'
cipher = bytes.fromhex('641460a9e3953b1aaa21f3a2')
# assert (transform(bytes.fromhex('84b45f89af22ce7e67275bdc')).hex() == transformed_flag)
c = cipher
s = set()
while True:
    c = transform(c.zfill(len(cipher)))
    print(c.hex())
    s.add(c.hex())
    print(len(s))
    if c.hex() == transformed_flag:
        break

As for why, it is because the encryption algorithm used in this problem is Mersenne twister , is a pseudo-random number generation algorithm. An updated and more commonly used algorithm of this algorithm is MT19937, 32-bit word length, corresponding to the problem
Moreover, the random number generated by the algorithm is periodic, which is not difficult to understand why the plaintext can be obtained by encrypting the ciphertext all the time, because the ciphertext is still obtained after a cycle, so the last one is plaintext
The result of the above decryption code is:

It is not difficult to find that its cycle is 61319

For more information about the shape of MT19937 pseudo-random number generation algorithm, please refer to the previous generation of madmonkey Analysis of mt19937 pseudo-random number generation algorithm
It introduces the same solution as the first method, but uses modular code like wp to package the decryption steps into functions
Here is the decryption code referring to the code therein:

from Crypto.Util.number import *

# right shift inverse
def inverse_right(res, shift, bits=32):
    tmp = res
    for i in range(bits // shift):
        tmp = res ^ tmp >> shift
    return tmp


# right shift with mask inverse
def inverse_right_mask(res, shift, mask, bits=32):
    tmp = res
    for i in range(bits // shift):
        tmp = res ^ tmp >> shift & mask
    return tmp

# left shift inverse
def inverse_left(res, shift, bits=32):
    tmp = res
    for i in range(bits // shift):
        tmp = res ^ tmp << shift
    return tmp


# left shift with mask inverse
def inverse_left_mask(res, shift, mask, bits=32):
    tmp = res
    for i in range(bits // shift):
        tmp = res ^ tmp << shift & mask
    return tmp


def extract_number(y):
    y = y ^ y >> 11
    y = y ^ y << 7 & 2636928640
    y = y ^ y << 15 & 4022730752
    y = y ^ y >> 18
    return y & 0xffffffff

def recover(y):
    y = inverse_right(y, 19)
    y = inverse_left_mask(y, 17, 2245263360)
    y = inverse_left_mask(y, 9, 2029229568)
    y = inverse_right(y, 13)
    return y & 0xffffffff

transformed_flag = '641460a9e3953b1aaa21f3a2'
cipher = bytes.fromhex('641460a9e3953b1aaa21f3a2')
new_message = b''
for i in range(len(cipher) // 4):
    block = cipher[i * 4: i * 4 + 4]
    block = bytes_to_long(block)
    block = recover(block)
    block = long_to_bytes(block, 4)
    new_message += block
print(new_message.hex())

The results are the same:

Another method is the black box method, which regards the binary coding of ciphertext and plaintext as two vectors a , b a,b a. B. according to the encryption method, there is a linear relationship between the two vectors, that is, there is a square matrix M M M. Make a = M b a=Mb a=Mb
The specific linear relationship is as follows:

I don't understand this method very much. I'll talk about it later (slip away)

epilogue

I wrote a little yesterday (July 24), but I didn't finish it
Because there is a bug in the code of method 2, I can't solve it. Thank Phoenix for helping me change it
Today (July 25), we strive to add another issue
(you asked me why I went on July 23? I made up for it all day [doge])
Hope to continue to insist

Topics: Encryption Crypto