ふるつき

v(*'='*)v かに

UIUCTF 2020 writeup

I participated in this year's UIUCTF as a member of zer0pts. We got 5th place! The competition was well-designed and had a lot of good challenges. The other, there were a few of a guess or boring challenges and unfair operations. I hope it will be fixed in the next competition.

f:id:Furutsuki:20200720142023p:plain

[web] nookstop

There was a quite simple web page. It seems easy to get a flag. Just launch a click event for the flag button by this script: $("#flag").click(). It set a cookie clicked_flag_button=true and reloaded the page.

<html>
    <head>
   <script src="/static/cookie.js"></script>
   <script src="/static/jquery.min.js"></script>
   </head>
<body>

<!-- im so sorry -->
<div id="main">

    <pre id="output" ></pre>

    <br><br>
    <div id="buttons">
        <button type="button" id="status">Network Status</button> 
        <div>
            <button type="button" id="flag" style="margin-left: 300px">Flag</button> 
        </div>
    </div>

    
        <pre>Not that easy c: but here's a crumb: uiuctf{w</pre>
        

        

</div>

<script>

async function print(m, ms) {
   $("#output").append("\n");
   for (var i=0; i < m.length; i++)
   {
       var sel = $("#output");
       sel.append(m[i]);
       await sleep(ms);
   }
   $("#output").append("\n");
}

var msg1 = "Welcome to the Nook Stop, a magical multimedia terminal from Nook Inc.\nPlease select one of the following services:\n1) Network status\n2) Flag";
var msg2 = "Ah, sorry!! Our banking systems run on FORTRAN and they're a bit \non the slow side today.............................................\nyou know how this game's netcode can be sometimes, ahahahahaha!\nAlthought, come to think of it, they did say they were rolling out \nsome kind of new system the other day.......anyway, seems like that flag button isn't working right now."

$(document).ready(function(){

   $("#flag").click(function(){
       console.log("TODO: シークレット_バックエンド_サービスを可能にする");
       document.cookie = "clicked_flag_button=true";
       location.reload();
   });

   f = $("#flag");

   f.mousemove(function(){
       var r = Math.random()*100;
       if (r < 25)
           f.css("margin-left", "+=100px");
       else if (r >= 25 && r < 50)
           f.css("margin-left", "-=30px");
       else if (r >= 50 && r < 75)
           f.css("margin-top", "+=50px");
       else if (r >= 75 && r < 97)
           f.css("margin-top", "-=50px");
       else
           f.remove();
   });


   $("#status").click(function(){
       print(msg2,100);
   });

   print(msg1,10);
});

</script>



</body>
</html>

Then the following line is appended to HTML.

Not that easy c: but here's a crumb: uiuctf{w

To get full of the flag, we should set additionally cookie secret_backend_service=true according to the log of the console. Then got a flag! uiuctf{wait_its_all_cookies?}

[crypto] isabelles_file_encryption

The following script was a challenge. Also blackmail_encrypted was given.

#!/usr/bin/python

# password-protect your files with this super powerful encryption!
def super_secret_encryption(file_name, password):
  with open(file_name, "rb") as f:
    plaintext = f.read()
  
  assert(len(password) == 8) # I heard 8 character long passwords are super strong!
  assert(password.decode("utf-8").isalpha()) # The numbers on my keyboard don't work...
  assert(b"Isabelle" in plaintext) # Only encrypt files Isabelle has been mentioned in
  add_spice = lambda b: 0xff & ((b << 1) | (b >> 7))
  ciphertext = bytearray(add_spice(c) ^ password[i % len(password)] for i, c in enumerate(plaintext))

  with open(file_name + "_encrypted", "wb") as f:
    f.write(ciphertext)

# use this to decrypt the file with the same password!
def super_secret_decryption(file_name, password):
  with open(file_name + "_encrypted", "rb") as f:
    ciphertext = f.read()
  
  remove_spice = lambda b: 0xff & ((b >> 1) | (b << 7))
  plaintext = bytearray(remove_spice(c ^ password[i % len(password)]) for i, c in enumerate(ciphertext))

  with open(file_name + "_decrypted", "wb") as f:
    f.write(plaintext)

with open("password", "rb") as f: # I got too lazy typing it in each time
    password = f.read()
    # Make sure to encrypt the text in the middle!!!
    super_secret_encryption("blackmail", password)
    super_secret_decryption("blackmail", password)

It must be included in the plaintext that the spice added (= bit rotated) Isabelle and the key must be alphabetical 8 bytes. So using like the following script, we can enumerate list of (some rotation of) the key. And easily decrypt the blackmail by the given super_secret_decryption function.

from ptrlib import xor


add_spice = lambda b: 0xff & ((b << 1) | (b >> 7))
plaintext = bytes(add_spice(b) for b in b"Isabelle")

ciphertext = open("blackmail_encrypted", "rb").read()
for i in range(0, len(ciphertext) - 8):
    key = xor(plaintext, ciphertext[i:])
    try:
        if key.decode().isalpha():
            print(repr(key))
    except UnicodeDecodeError:
        pass

The key was iSaBelLE and the flag was in the mail: uiuctf{winner_winner_raccoon_dinner}

[crypto] coelacanth_vault

coelacanth_vault.py

#!/usr/bin/python
import random
from Crypto.Util import number
from functools import reduce

TOTAL = 15
THRESHOLD = 10
MAX_COELACANTH = 9

NUM_LOCKS = 5
NUM_TRIES = 250

# substitute for math.prod
prod = lambda n: reduce(lambda x, y: x*y, n)

def create_key(t, n, size=8):
    while True:
        seq = sorted([number.getPrime(size) for _ in range(TOTAL)])
        if len(set(seq)) != len(seq):
            continue

        alpha = prod(seq[:t])
        beta = prod(seq[-t + 1:])
        if alpha > beta:
            secret = random.randint(beta, alpha)
            shares = [(secret % num, num) for num in seq]
            return secret, shares

def construct_key(shares):
    glue = lambda A, n, s=1, t=0, N=0: (n < 2 and t % N or glue(n, A % n, t, s - A//n * t, N or n), -1)[n < 1]
    mod = prod([m for s, m in shares])
    secret = sum([s * glue(mod//m, m) * mod//m for s, m in shares]) % mod
    return secret

if __name__ == "__main__":
    print("Hi, and welcome to the virtual Animal Crossing: New Horizon coelacanth vault!")
    print("There are {} different cryptolocks that must be unlocked in order to open the vault.".format(NUM_LOCKS))
    print("You get one portion of each code for each coelacanth you have caught, and will need to use them to reconstruct the key for each lock.")
    print("Unfortunately, it appears you have not caught enough coelacanths, and thus will need to find another way into the vault.")
    print("Be warned, these locks have protection against brute force; too many wrong attempts and you will have to start over!")
    print("")

    num_shares = abs(int(input("How many coelacanth have you caught? ")))
    if num_shares > MAX_COELACANTH:
        print("Don't be silly! You definitely haven't caught more than {} coelacanth!".format(MAX_COELACANTH))
        print("Come back when you decide to stop lying.")
        quit()

    for lock_num in range(NUM_LOCKS):
        print("Generating key for lock {}, please wait...".format(lock_num))
        secret, shares = create_key(THRESHOLD, TOTAL)
        r_secret = construct_key(random.sample(shares, THRESHOLD))
        assert(secret == r_secret)
        print("Generated!")
        
        print("Here are your key portions:")
        print(random.sample(shares, num_shares))

        for num_attempts in range(NUM_TRIES):
            n_secret = abs(int(input("Please input the key: ")))
            if secret == n_secret:
                print("Lock {} unlocked with {} failed attempts!".format(lock_num, num_attempts))
                break
            else:
                print("Key incorrect. You have {} tries remaining for this lock.".format(NUM_TRIES - num_attempts))        
        else:
            print("BRUTE FORCE DETECTED. LOCKS SHUT DOWN.")
            print("Get out. You don't deserve what's in this vault.")
            quit()
    
    print("Opening vault...")
    with open("flag.txt", "r") as f:
        print(f.read())

It makes some of the shares by getting the remainder by the prime modulus of the secret. So it is recoverable by the Chinese Remainder Theorem. It seems impossible for the lack of shares, however, the 8-bit primes is at most 251 and we have 250 times to challenge. Thus we can pick a one of the large (and not in the given share) prime and bruteforce to hit the correct remainder.

The solution script:

from ptrlib import Socket, crt

NUM_LOCKS = 5
NUM_TRIES = 250

sock = Socket("chal.uiuc.tf", 2004)
sock.sendlineafter("caught? ", "9")

primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251][::-1]

for i in range(NUM_LOCKS):
    print(f"stage {i+1}")
    shares = eval(sock.recvlineafter("portions:\n").decode())

    for p in primes:
        flag = True
        for s in shares:
            if s[1] == p:
                flag = False
                break
        if flag:
            prime = p
            break

    for x in range(251):
        v, _ = crt(shares + [(x, prime)])
        sock.sendlineafter("key: ", str(v))
        if b"unlocked" in sock.recvline():
            break
sock.interactive()

the flag was uiuctf{small_oysters_expire_quick}

[rev] Redd's Art

We're given the 64bit ELF binary. The point was sub_A5A. It does xor bytes in address 973 by the key: the result of sub_91A which was sum of string uiuctf{v3Ry_r341_@rTT}. However the modification of .text causes a segmentation fault. So I wrote the modification script

data = [0x4B,0x56,0x97,0xFB,0x4D,0x56,0x9D,0xF2,0x36,0x56,0xD9,0x5B,0xF6,0x17,0x1E,0x1E,0x1E,0x56,0x95,0x5B,0xF6,0x11,0xA8,0x1E,0x56,0x11,0xA0,0xDE,0x56,0x1F,0x5B,0xF6,0x56,0x95,0x5B,0xF6,0x11,0xA8,0x1E,0x96,0x5B,0xC5,0xD9,0x5B,0xC2,0x1E,0x1E,0x1E,0x1E,0xF5,0x2C,0x56,0x95,0x0B,0x65,0x08,0x3E,0x1E,0x95,0x5B,0xC2,0x56,0x86,0x56,0x1F,0xCE,0x11,0xA8,0x1E,0x97,0xDC,0x11,0xA8,0x5B,0xC5,0x93,0x12,0x1C,0x56,0x95,0x0B,0x7E,0x08,0x3E,0x1E,0x95,0x5B,0xC2,0x56,0x86,0x56,0x1F,0xCE,0x97,0xD4,0x96,0x0E,0x9D,0x5B,0xC2,0x1F,0x95,0x5B,0xC2,0x56,0x7D,0xC6,0x56,0x95,0x1B,0x5D,0x08,0x3E,0x1E,0x56,0x97,0xD9,0xF6,0xAD,0xE3,0xE1,0xE1,0x56,0x27,0xDD,0x6C,0xAA,0xA6,0x1E,0x1E,0x1E,0x1E,0xF6,0x00,0xE1,0xE1,0xE1,0x97,0x5B,0xFA,0xD9,0x5B,0xFE,0x1E,0x1E,0x1E,0x1E,0xF5,0x2E,0x56,0x95,0x0B,0x07,0x08,0x3E,0x1E,0x95,0x5B,0xFE,0x56,0x86,0x56,0x1F,0xCE,0x11,0xA8,0x16,0x95,0x5B,0xFA,0x97,0xD8,0x56,0x95,0x0B,0x1C,0x08,0x3E,0x1E,0x95,0x5B,0xFE,0x56,0x86,0x56,0x1F,0xCE,0x2F,0xEF,0x97,0xD4,0x96,0x0E,0x9D,0x5B,0xFE,0x1F,0x95,0x5B,0xFE,0x56,0x7D,0xC6,0x56,0x95,0x1B,0xFD,0x0B,0x3E,0x1E,0x56,0x97,0xD9,0xF6,0x4D,0xE3,0xE1,0xE1,0x56,0x27,0xDD,0x6C,0xA8,0x8E,0x56,0x9D,0xDA,0x36,0x45,0x43]

key = sum(ord(c) for c in "uiuctf{v3Ry_r341_@rTT}")
xored = [(d^key)&0xff for d in data]


data = bytearray(open("ReddsArt", "rb").read())

data = data[:0x973] + bytes(xored) + data[0x973 + len(xored):]
open("modified", "wb").write(data)

Then It tuned out that the sub_973 does add some constant x to the each byte of string at 0x202028, and after that, xor them by the key which calculated by sub_91A.

The unknown x was only 1 byte. So I brute-forced all candidates.

key = sum(ord(c) for c in "uiuctf{v3Ry_r341_@rTT}")
xs = [ord(c) for c in "hthzgubI>*ww7>z+Ha,m>W,7z+hmG`"]

for k in range(256):
    ys = "".join(chr((((k+x) % 256) ^ key) & 0xff) for x in xs)
    print(repr(ys))

the flag was: uiuctf{R_3dd$_c0Uz1n_D1$c0unT}

Defenit CTF 2020 writeup

I played Defenit CTF 2020 as a member of zer0pts. This was an amazing competition. I enjoyed it a lot.

f:id:Furutsuki:20200607180823p:plain

Challenge attachments and solution scripts are available from here

[Crypto] Double Message

We're given the double.sage and the output.txt.

from Crypto.PublicKey import RSA
from hashlib import md5
import binascii

nBitSize = 2048
e = 3
Flag = b'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'   # censored

key = RSA.generate(nBitSize)

M1 = Flag + md5(Flag).digest()
M2 = Flag + md5(b'One more time!' + Flag).digest()

M1 = int(binascii.hexlify(M1),16)
M2 = int(binascii.hexlify(M2),16)

C1 = Integer(pow(M1,e,key.n))
C2 = Integer(pow(M2,e,key.n))

with open('out.txt','w') as f:
    f.write('n = ' + hex(key.n)+'\n')
    f.write('C1 = '+ hex(C1)+'\n')
    f.write('C2 = ' + hex(C2)+'\n')

Apparently, it seems to be Coppersmith's Short Pad Attack + Franklin-Reiter's Related Message Attack. I borrowed the script from this writeup .

e = 3

PRxy.<x,y> = PolynomialRing(Zmod(n))
PRx.<xn> = PolynomialRing(Zmod(n))
PRZZ.<xz,yz> = PolynomialRing(Zmod(n))
 
g1 = x**e - C1
g2 = (x + y)**e - C2
 
q1 = g1.change_ring(PRZZ)
q2 = g2.change_ring(PRZZ)
 
h = q2.resultant(q1)
# need to switch to univariate polynomial ring
# because .small_roots is implemented only for univariate
h = h.univariate_polynomial() # x is hopefully eliminated
h = h.change_ring(PRx).subs(y=xn)
h = h.monic()
 
roots = h.small_roots(X=2**127, beta=0.3)
assert roots, "Failed1"

diff = roots[0]

x = PRx.gen() # otherwise write xn
g1 = x**e - C1
g2 = (x + diff)**e - C2
 
# gcd
while g2:
    g1, g2 = g2, g1 % g2
 
g = g1.monic()
assert g.degree() == 1, "Failed 2"
 
# g = xn - msg
msg = -g[0]
print(msg)

[Crypto] Hash ChungDol

server.py

#!/usr/bin/python3
from Crypto.Random.random import getrandbits
from base64 import b64decode, b64encode
from Crypto.Util.number import long_to_bytes, bytes_to_long
from secret import flag

BITS = 220

def shift(n,i):
    n1=(n<<i)%0x800
    n2=(n>>(11-i))
    return n1^n2


def PIE(i, A, B, C):
    if i<16:
        return (A&B)|((~A)&C)
    return (A&B)|(B&C)|(C&A)

def pie(j):
    if(j<16): return j
    elif(j==16): return 0
    elif(j==17): return 4
    elif(j==18): return 8
    elif(j==19): return 12
    elif(j==20): return 1
    elif(j==21): return 5
    elif(j==22): return 9
    elif(j==23): return 13
    elif(j==24): return 2
    elif(j==25): return 6
    elif(j==26): return 10
    elif(j==27): return 14
    elif(j==28): return 3
    elif(j==29): return 7
    elif(j==30): return 11
    else: return 15

def HashGenerate(value):
    n = 3
    s = [3]*16 + [7]*16
    Q = [0]*36
    m = [0]*16
    X = []
    for i in range(0,20):
        X.insert(0, value&0x7ff)
        value >>=11
        
    for i in range(-3, 1, 1):
        Q[n+i]=X[i+3]
    for i in range(0,16):
        m[i]=X[i+4]
    for i in range(0, 32):
        Q[n+i+1]=shift(( (Q[n+i-3] + PIE(i,Q[n+i],Q[n+i-1],Q[n+i-2]) + m[pie(i)])%0x800 ), s[i]) 

    Y = []
    for i in range(0,4):
        Y.append((Q[n+i-3] + Q[32+i])%0x800)

    Y[1]=(Y[0]<<33)^(Y[1]<<22)
    Y[0]=Y[2]<<11
    Y[1]^=Y[0]^Y[3]

    return Y[1]

def challenge():
    goal = HashGenerate(getrandbits(BITS))
    print("Hash : %s [0/5]" % hex(goal))
    answer = []
    for i in range(1, 6):
        data = b64decode(input("data : "))
        value = bytes_to_long(data)
        result = HashGenerate(value)
        if (result!= goal) or (value in answer):
            exit(0)
        answer.append(value)
        print("Hash : %s [%d/5]" % (hex(goal), i))
    print("\nGood!\n")


if __name__ == '__main__':
    print("Can you make a hash like this?\n")
    stage = 1
    while stage <= 10:
        print("============================================")
        print("                  Stage %d" % stage)
        print("============================================")
        try: 
            challenge()
            stage+=1
        except:
            print("Nop")
            exit(0)
    print(flag)

Our task is to find 5 value pairs that yield the same hash value. @mitsu proposed a method that makes almost of Q[i] to 0. Using this method, we can fix the least 33bit of the hash value. So I brute-forced the remaining 11bit to find the collision.

In fact, as this algorithm assumes to get the 220-bit long value at most, value a and a | (1 << 220) yields the same result.

solve.py

def crack(h):
    n = 3
    for t in range(1 << 11):
        tt = (t << 33) | (h & ((1 << 33) - 1))
        value = tt  << (220 - 44)

        x = value
        Q = [0]*36
        m = [0]*16

        X = []
        for i in range(0,20):
            X.insert(0, value&0x7ff)
            value >>=11
        for i in range(-3, 1, 1):
            Q[n+i]=X[i+3]
        for i in range(0,16):
            m[i]=X[i+4]

        for i in range(0, 4):
            P = PIE(i, Q[n+i], Q[n+i-1], Q[n+i-2])
            m[i] = 0x800 - P - Q[n+i-3]
            while m[i] < 0:
                m[i] += 0x800
            x |= (m[i] << (165 - 11 * i))

        h2 = HashGenerate(x)
        if h2 == h:
            return x


from ptrlib import *
from Crypto.Random.random import getrandbits
from base64 import b64decode, b64encode
from Crypto.Util.number import long_to_bytes, bytes_to_long

sock = Socket("hash-chungdol.ctf.defenit.kr", 8960)
for _ in range(10):
    h = int(sock.recvlineafter("Hash : ").split(b" ")[0], 16)
    r = crack(h)
    for i in range(1, 6):
        exploit = r | (i << 220)
        sock.sendlineafter("data : ", b64encode(long_to_bytes(exploit)))
        print(sock.recvline())
    print(sock.recvline())
sock.interactive()

[Forensics] What Browse do I use

We're given some huge Evidence files. Its format is EWF/Expert Witness/EnCase image file format. I've never seen the format, however, it can be mounted with ewftool. Then it exploded its size to 30GiB . This file can also be mounted with kpartx in this time.

Now we can see the windows file system. As our purpose is to find out the version of the user's default browser, I checked NTUSER.DAT and saw the value of HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice\Progid. Then it turned out the user used google chrome.

To get the version of chrome was easy, just see in the C:\Program Files (x86)\Google\Chrome.

[rev] child encrypter

We're given the ELF file and ciphertext.txt. After some observation, it turned out that the ELF file works like the following.

from Crypto.Cipher import AES
import os

key = open("key", "rb").read()
plaintext = open("plaintext", "rb").read()
nonce = os.urandom(16)

c.srand(key[0])

xorkey = None
ciphertext = []
for i in range(len(plaintext)):
    if i % 16 == 0:
        nonce = nonce[:15] + bytes([c.rand() % 10])
        xorkey = AES.new(key=key, mode=AES.MODE_ECB).encrypt(nonce)
    ciphertext.append( xorkey[i % 16] ^ plaintext[i] )

with open("cihpertext", "wb") as f:
    f.write(ciphertext)

It is similar to the CTR_MODE but the last byte is determined by rand() % 10. This means that the variation of the key is just 10. So this challenge could be reduced to a multi-time pad challenge.

[rev] Lord Fool Song Remix

The reversing part was done by @ptr-yudai. I received the python emulated version of the binary.

def encrypt(name, slen):
    bits = []
    for c in name + b'\x00' * (9-len(name)):
        for i in range(8):
            bits.append((c >> (7-i)) & 1)
    memory = [
        bits[0:23][::-1] + [0] * (slen - 23),
        bits[23:23+24][::-1] + [0] * (slen - 24),
        bits[23+24:23+24+25][::-1] + [0] * (slen - 25)
    ]
    for i in range(0, slen - 23):
        memory[0][23+i] = memory[0][i] ^ memory[0][i+5]
    for i in range(0, slen - 24):
        memory[1][24+i] = memory[1][i] ^ memory[1][i+4] ^ memory[1][i+1] ^ memory[1][i+3]
    for i in range(0, slen - 25):
        memory[2][25+i] = memory[2][i] ^ memory[2][i+3]

    output = []
    for i in range(slen):
        output.append(
            ((memory[2][i] ^ memory[0][i]) & memory[1][i]) ^ memory[2][i]
        )
    return output

length = 20
name = b"abcd123"
enc = encrypt(name, length*10)

for i in range(length):
    snd = 0
    for j in range(10):
        snd |= enc[i*10 + j]
        snd <<= 1
    print(snd >> 1, end=" ")

print()

just do the boring stuff with z3.

def solve(piyo):
    slen = len(piyo)
    bits = [BitVec("bitvec{}".format(i), 1) for i in range(72)]
    memory = [
        bits[0:23][::-1] + [0] * (slen - 23),
        bits[23:23+24][::-1] + [0] * (slen - 24),
        bits[23+24:23+24+25][::-1] + [0] * (slen - 25)
    ]

    for i in range(0, slen - 23):
        memory[0][23+i] = memory[0][i] ^ memory[0][i+5]

    for i in range(0, slen - 24):
        memory[1][24+i] = memory[1][i] ^ memory[1][i+4] ^ memory[1][i+1] ^ memory[1][i+3]

    for i in range(0, slen - 25):
        memory[2][25+i] = memory[2][i] ^ memory[2][i+3]

    output = []
    solver = Solver()
    for i in range(len(piyo)):
        solver.add( ((memory[2][i] ^ memory[0][i]) & memory[1][i]) ^ memory[2][i] == piyo[i] )

    m = solver.check()
    if m == sat:
        print("sat")
        m = solver.model()
        p = ''
        flag = []
        for b in bits:
            p += str(m[b].as_long())
            if len(p) == 8:
                flag.append(chr(int(p, 2)))
                p = ''
        print("".join(flag))
    else:
        print("unsat")
outputs = []
xs = [int(x) for x in "638 1004 829 320 666 526 593 247 940 790 1023 75 240 42 1012 326 447 261 882 881".split(" ")]
for x in xs:
    outputs += [int(b) for b in "{:010b}".format(x)]

print(len(outputs))
solve(outputs)

[rev] MixMix

the binary shuffle and replace the input with the RC4-like algorithm. I collected shuffle indices and replace values using the following gdb script.

import gdb


gdb.execute("file ./mixmix")
gdb.execute("start < input")
piebase  = int(gdb.execute("piebase", to_string=True).strip().split(" = ")[1], 16)
data = 'mogutakomogumogutakomogudefenitc'
print(data)


gdb.execute("b *0x{:0x}".format(piebase+ 0xECA))
offsets = []
bits = []
for _ in range(256):
    gdb.execute("continue")
    offset = int(gdb.execute("print $rdx", to_string=True).strip().split(" = ")[1])
    offsets.append(offset)
    bit = int(gdb.execute("print $rcx", to_string=True).strip().split(" = ")[1])
    bits.append(bit)

gdb.execute("b *0x{:0x}".format(piebase+ 0x102A))
gdb.execute("b *0x{:0x}".format(piebase+ 0x105B))
swapindex = []
bitvalues = []
for _ in range(256):
    gdb.execute("continue")
    offset = int(gdb.execute("print $rdx", to_string=True).strip().split(" = ")[1])
    swapindex.append(offset)

    gdb.execute("continue")
    offset = int(gdb.execute("print $rdx", to_string=True).strip().split(" = ")[1])
    bitvalues.append(offset)


gdb.execute("b *0x{:0x}".format(piebase+ 0x1123))
mogo_offsets = []
vs = []
for _ in range(256):
    gdb.execute("continue")
    offset = int(gdb.execute("print $rdx", to_string=True).strip().split(" = ")[1])
    mogo_offsets.append(offset)

    gdb.execute("step")
    v = int(gdb.execute("print $rdx", to_string=True).strip().split(" = ")[1])
    vs.append(v)


gdb.execute("b *0x{:0x}".format(piebase+0x0F64))
mogo_read_offsets = []
read_vs = []
for _ in range(256):
    gdb.execute("continue")
    offset = int(gdb.execute("print $rdx", to_string=True).strip().split(" = ")[1])
    mogo_read_offsets.append(offset)

    gdb.execute("step")
    v = int(gdb.execute("print $rdx", to_string=True).strip().split(" = ")[1])
    read_vs.append(v)

gdb.execute("b *0x{:0x}".format(piebase+0x0D8b))
mini_values = []
vx = []
for _ in range(len(data)):
    gdb.execute("continue")
    v = int(gdb.execute("print $rsi", to_string=True).strip().split(" = ")[1])
    mini_values.append(v)

    v2 = int(gdb.execute("print $rcx", to_string=True).strip().split(" = ")[1])
    vx.append(v2)

print(offsets)
print(bits)
print(swapindex)
print(bitvalues)
print(mogo_offsets)
print(vs)
print(mogo_read_offsets)
print(read_vs)
print(mini_values)
print(vx)

After getting the indices, applying the reverse assignments.

shuffle_2 = [164, 144, 464, 928, 96, 856, 580, 268, 556, 180, 244, 392, 468, 200, 544, 936, 776, 316, 524, 932, 412, 172, 688, 676, 444, 572, 796, 76, 652, 692, 380, 408, 916, 356, 84, 360, 188, 68, 312, 388, 340, 88, 816, 44, 512, 264, 20, 184, 52, 0, 372, 520, 168, 740, 236, 568, 252, 260, 644, 552, 852, 548, 292, 420, 72, 1004, 884, 136, 768, 248, 240, 304, 344, 272, 792, 564, 256, 680, 708, 80, 620, 760, 976, 744, 480, 4, 864, 592, 944, 320, 952, 948, 696, 124, 452, 472, 428, 284, 752, 832, 204, 64, 720, 872, 348, 440, 588, 28, 560, 220, 432, 608, 56, 764, 176, 784, 148, 972, 496, 92, 504, 880, 488, 860, 436, 772, 684, 48, 8, 476, 844, 416, 368, 960, 920, 484, 868, 280, 352, 36, 120, 824, 24, 212, 376, 828, 532, 712, 808, 996, 780, 448, 276, 1008, 60, 152, 700, 100, 508, 308, 756, 364, 648, 328, 116, 612, 748, 216, 528, 456, 956, 704, 224, 660, 716, 736, 636, 1016, 132, 604, 128, 788, 576, 672, 196, 904, 668, 848, 840, 888, 724, 896, 300, 584, 540, 912, 980, 876, 424, 404, 624, 208, 596, 992, 836, 640, 232, 160, 500, 516, 396, 332, 336, 12, 800, 656, 140, 112, 296, 156, 228, 812, 968, 32, 288, 908, 536, 732, 460, 1012, 728, 16, 892, 820, 104, 492, 192, 600, 400, 940, 984, 40, 108, 1000, 988, 632, 924, 324, 964, 900, 1020, 628, 616, 804, 384, 664]
bit_shuffle = [72, 84, 4, 156, 136, 56, 148, 44, 168, 180, 24, 220, 200, 152, 212, 140, 20, 40, 68, 8, 76, 52, 64, 60, 240, 248, 100, 252, 232, 0, 244, 32, 120, 104, 88, 112, 92, 116, 80, 124, 196, 208, 164, 192, 172, 216, 160, 204, 144, 28, 184, 132, 188, 16, 176, 128, 12, 108, 228, 48, 236, 96, 224, 36, 328, 340, 260, 412, 392, 312, 404, 300, 424, 436, 280, 476, 456, 408, 468, 396, 276, 296, 324, 264, 332, 308, 320, 316, 496, 504, 356, 508, 488, 256, 500, 288, 376, 360, 344, 368, 348, 372, 336, 380, 452, 464, 420, 448, 428, 472, 416, 460, 400, 284, 440, 388, 444, 272, 432, 384, 268, 364, 484, 304, 492, 352, 480, 292, 584, 596, 516, 668, 648, 568, 660, 556, 680, 692, 536, 732, 712, 664, 724, 652, 532, 552, 580, 520, 588, 564, 576, 572, 752, 760, 612, 764, 744, 512, 756, 544, 632, 616, 600, 624, 604, 628, 592, 636, 708, 720, 676, 704, 684, 728, 672, 716, 656, 540, 696, 644, 700, 528, 688, 640, 524, 620, 740, 560, 748, 608, 736, 548, 840, 852, 772, 924, 904, 824, 916, 812, 936, 948, 792, 988, 968, 920, 980, 908, 788, 808, 836, 776, 844, 820, 832, 828, 1008, 1016, 868, 1020, 1000, 768, 1012, 800, 888, 872, 856, 880, 860, 884, 848, 892, 964, 976, 932, 960, 940, 984, 928, 972, 912, 796, 952, 900, 956, 784, 944, 896, 780, 876, 996, 816, 1004, 864, 992, 804]
bit_pick = [28, 24, 20, 16, 12, 8, 4, 0, 60, 56, 52, 48, 44, 40, 36, 32, 92, 88, 84, 80, 76, 72, 68, 64, 124, 120, 116, 112, 108, 104, 100, 96, 156, 152, 148, 144, 140, 136, 132, 128, 188, 184, 180, 176, 172, 168, 164, 160, 220, 216, 212, 208, 204, 200, 196, 192, 252, 248, 244, 240, 236, 232, 228, 224, 284, 280, 276, 272, 268, 264, 260, 256, 316, 312, 308, 304, 300, 296, 292, 288, 348, 344, 340, 336, 332, 328, 324, 320, 380, 376, 372, 368, 364, 360, 356, 352, 412, 408, 404, 400, 396, 392, 388, 384, 444, 440, 436, 432, 428, 424, 420, 416, 476, 472, 468, 464, 460, 456, 452, 448, 508, 504, 500, 496, 492, 488, 484, 480, 540, 536, 532, 528, 524, 520, 516, 512, 572, 568, 564, 560, 556, 552, 548, 544, 604, 600, 596, 592, 588, 584, 580, 576, 636, 632, 628, 624, 620, 616, 612, 608, 668, 664, 660, 656, 652, 648, 644, 640, 700, 696, 692, 688, 684, 680, 676, 672, 732, 728, 724, 720, 716, 712, 708, 704, 764, 760, 756, 752, 748, 744, 740, 736, 796, 792, 788, 784, 780, 776, 772, 768, 828, 824, 820, 816, 812, 808, 804, 800, 860, 856, 852, 848, 844, 840, 836, 832, 892, 888, 884, 880, 876, 872, 868, 864, 924, 920, 916, 912, 908, 904, 900, 896, 956, 952, 948, 944, 940, 936, 932, 928, 988, 984, 980, 976, 972, 968, 964, 960, 1020, 1016, 1012, 1008, 1004, 1000, 996, 992]
xorbuf = [247, 162, 130, 73, 139, 252, 234, 40, 142, 146, 75, 134, 81, 115, 215, 169, 165, 169, 56, 234, 105, 163, 139, 231, 172, 23, 9, 106, 139, 191, 253, 21]


encrypted = open("flag_mixed", "rb").read()
xoredx = []
xored = ''
for i in range(len(encrypted)):
    x = xorbuf[i] ^ encrypted[i]
    xoredx.append(x)
    xored += "{:08b}".format(x)[::-1]
print(xoredx)

bitbuf = [None for _ in range(256)]
for i, b in enumerate(xored):
    index = bit_pick[i] // 4
    bitbuf[index] = b
print(bitbuf)

shuffled = [None for _ in range(256)]
for i, b in enumerate(bitbuf):
    index = bit_shuffle[i] // 4
    shuffled[index] = b

plainbits = [None for _ in range(256)]
for i, b in enumerate(shuffled):
    index = shuffle_2[i] // 4
    plainbits[index] = b

print(plainbits)


flag = ""
for i in range(0, len(plainbits), 8):
    flag += chr(int("".join(plainbits[i:i+8])[::-1], 2))

print(flag)

[rev] Mom's Touch

import gdb

gdb.execute("b main")
gdb.execute("b *0x8048800")
gdb.execute("run < piyo2")

flag = ''
for _ in range(0x49):
    # ecx1 = int(gdb.execute("print $ecx", to_string=True).split(" ")[2])
    # ecx2 = int(gdb.execute("print $ecx", to_string=True).split(" ")[2])
    # ecx3 = int(gdb.execute("print $ecx", to_string=True).split(" ")[2])
    mask1 = int(gdb.execute("x/w $bp*4 + 0x80492AC", to_string=True).split("\t")[1])
    mask2 = int(gdb.execute("x/w $eax*4 + 0x80492AC", to_string=True).split("\t")[1])
    cmpto = int(gdb.execute("x/w $esi*4 + 0x8049144", to_string=True).split("\t")[1])
    gdb.execute("step")
    gdb.execute("step")
    gdb.execute("step")
    gdb.execute("step")

    gdb.execute("set $ZF = 6")
    gdb.execute("set $eflags |= (1 << $ZF)")

    flag += chr(mask1 ^ mask2 ^ cmpto)
    print(flag)

    gdb.execute("continue")
gdb.execute("quit")

pwn2win 2020 writeup

I played this year's pwn2win CTF as a member of zer0pts. We reached 6th place and got 2842 points. I mostly solved crypto challenges which were fun. and tried to solve some weird misc challenges and ppc challenges.

f:id:Furutsuki:20200601014755p:plain

challenge files and my solution scripts are available from here

[crypto] Androids Encryption

challenge script:

#!/usr/bin/python3 -u
# *-* coding: latin1 -*-
import sys
import base64
from Crypto.Cipher import AES

from secrets import flag, key1, iv1


def to_blocks(txt):
    return [txt[i*BLOCK_SIZE:(i+1)*BLOCK_SIZE] for i in range(len(txt)//BLOCK_SIZE)]


def xor(b1, b2=None):
    if isinstance(b1, list) and b2 is None:
        assert len(set([len(b) for b in b1])) == 1, 'xor() - Invalid input size'
        assert all([isinstance(b, bytes) for b in b1]), 'xor() - Invalid input type'
        x = [len(b) for b in b1][0]*b'\x00'
        for b in b1:
            x = xor(x, b)
        return x
    assert isinstance(b1, bytes) and isinstance(b2, bytes), 'xor() - Invalid input type'
    return bytes([a ^ b for a, b in zip(b1, b2)])


BUFF = 256
BLOCK_SIZE = 16
iv2 = AES.new(key1, AES.MODE_ECB).decrypt(iv1)
key2 = xor(to_blocks(flag))


def encrypt(txt, key, iv):
    global key2, iv2
    assert len(key) == BLOCK_SIZE, f'Invalid key size'
    assert len(iv) == BLOCK_SIZE, 'Invalid IV size'
    assert len(txt) % BLOCK_SIZE == 0, 'Invalid plaintext size'
    bs = len(key)
    blocks = to_blocks(txt)
    ctxt = b''
    aes = AES.new(key, AES.MODE_ECB)
    curr = iv
    for block in blocks:
        ctxt += aes.encrypt(xor(block, curr))
        curr = xor(ctxt[-bs:], block)
    iv2 = AES.new(key2, AES.MODE_ECB).decrypt(iv2)
    key2 = xor(to_blocks(ctxt))
    return str(base64.b64encode(iv+ctxt), encoding='utf8')


def enc_plaintext():
    print('Plaintext: ', end='')
    txt = base64.b64decode(input().rstrip())
    print(encrypt(txt, key1, iv1))


def enc_flag():
    print(encrypt(flag, key2, iv2))


def menu():
    while True:
        print('MENU')
        options = [('Encrypt your secret', enc_plaintext),
                   ('Encrypt my secret', enc_flag),
                   ('Exit', sys.exit)
                   ]
        for i, (op, _) in enumerate(options):
            print(f'{i+1} - {op}')
        print('Choice: ', end='')
        op = input().strip()
        assert op in ['1', '2', '3'], 'Invalid option'
        options[ord(op)-ord('1')][1]()


def main():
    print('Let\'s see if you are good enough in symmetric cryptography!\n')

    try:
        menu()
    except Exception as err:
        sys.exit(f'ERROR: {err}')


if __name__ == '__main__':
    main()

We can see there are two keys: one is a fixed key for encrypting our input, and another one is modified each in encrypt for encrypting the flag. On focusing the last line in the encrypt, we found that key2 is determined by our last ciphertext key2 = xor(to_blocks(ctxt)). As we can get the result of encryption, we can get the key2 too.

So we can get the pair of iv, ciphertext, and its key. Just write the decryption with taking care of encryption mode.

from ptrlib import *
from base64 import b64encode, b64decode
from Crypto.Util.Padding import *
from Crypto.Cipher import AES

BLOCK_SIZE = 16

def to_blocks(txt):
    return [txt[i*BLOCK_SIZE:(i+1)*BLOCK_SIZE] for i in range(len(txt)//BLOCK_SIZE)]

def xor(b1, b2=None):
    if isinstance(b1, list) and b2 is None:
        assert len(set([len(b) for b in b1])) == 1, 'xor() - Invalid input size'
        assert all([isinstance(b, bytes) for b in b1]), 'xor() - Invalid input type'
        x = [len(b) for b in b1][0]*b'\x00'
        for b in b1:
            x = xor(x, b)
        return x
    assert isinstance(b1, bytes) and isinstance(b2, bytes), 'xor() - Invalid input type'
    return bytes([a ^ b for a, b in zip(b1, b2)])

plaintext = pad(b"the quick brown fox jumps over the lazy dog", AES.block_size)

sock = Socket("encryption.pwn2.win", 1337)
sock.sendlineafter("Choice: ", "1")
sock.sendlineafter("Plaintext: ", b64encode(plaintext))
c = b64decode(sock.recvline())
iv, c = c[:16], c[16:]

key = xor(to_blocks(c))

sock.sendlineafter("Choice: ", "2")
c = b64decode(sock.recvline())
iv, c = c[:16], c[16:]

blocks = to_blocks(c)
aes = AES.new(key=key, mode=AES.MODE_ECB)
curr = iv
ctxt = b''
for i in range(len(blocks)):
    ctxt += xor(aes.decrypt(blocks[i]), curr)
    curr = xor(blocks[i] , ctxt[-16:])

print(ctxt)

[crypto] Omni Crypto

enc.py

import gmpy2
import random
from Crypto.Util.number import bytes_to_long

def getPrimes(size):
    half = random.randint(16, size // 2 - 8)
    rand = random.randint(8, half - 1)
    sizes = [rand, half, size - half - rand]

    while True:
        p, q = 0, 0
        for s in sizes:
            p <<= s
            q <<= s
            chunk = random.getrandbits(s)
            p += chunk 
            if s == sizes[1]:
                chunk = random.getrandbits(s)
            q += chunk
        p |= 2**(size - 1) | 2**(size - 2) | 1
        q |= 2**(size - 1) | 2**(size - 2) | 1
        if gmpy2.is_prime(p) and gmpy2.is_prime(q):
            return p, q

e = 65537
p, q = getPrimes(1024)
N = p*q
phi = (p-1)*(q-1)
d = gmpy2.invert(e, phi)
m = bytes_to_long(open("flag.txt", "rb").read())
c = pow(m, e, N)
print("N =", hex(N))
print("c =", hex(c))

It seems to be an RSA challenge whose p and q shares some MSB and LSB. MSB can be extracted from N by a naive square root. LSB can also be extracted from the modular square root of mod [tex: 2n].

def sqrt_power_of_2_mod(a, n):
    """find x^2 = a mod 2^n""" 
    assert a % 8 == 1

    res = []
    for x in [1, 3]:
        for k in range(3, n):
            i = ((x*x - a) // pow(2, k)) % 2
            x = x + i * pow(2, k-1)

        res.append(x)
        res.append(2**n - x)
    return res

Though we cannot find out sizes of MSB and LSB, just pray for MSB + LSB >= half of p and q bits and apply coppersmith.

from sage.all import *
from modsqrt import sqrt_power_of_2_mod
import gmpy2


N = 0xf7e6ddd1f49d9f875904d1280c675e0e03f4a02e2bec6ca62a2819d521441b727d313ec1b5f855e3f2a69516a3fea4e953435cbf7ac64062dd97157b6342912b7667b190fad36d54e378fede2a7a6d4cc801f1bc93159e405dd6d496bf6a11f04fdc04b842a84238cc3f677af67fa307b2b064a06d60f5a3d440c4cfffa4189e843602ba6f440a70668e071a9f18badffb11ed5cdfa6b49413cf4fa88b114f018eb0bba118f19dea2c08f4393b153bcbaf03ec86e2bab8f06e3c45acb6cd8d497062f5fdf19f73084a3a793fa20757178a546de902541dde7ff6f81de61a9e692145a793896a8726da7955dab9fc0668d3cfc55cd7a2d1d8b631f88cf5259ba1
c = 0xf177639388156bd71b533d3934016cc76342efae0739cb317eb9235cdb97ae50b1aa097f16686d0e171dccc4ec2c3747f9fbaba4794ee057964734835400194fc2ffa68a5c6250d49abb68a9e540b3d8dc515682f1cd61f46352efc8cc4a1fe1d975c66b1d53f6b5ff25fbac9fa09ef7a3d7e93e2a53f9c1bc1db30eed92a30586388cfef4d68516a8fdebe5ebf9c7b483718437fcf8693acd3118544249b6e62d30afa7def37aecf4da999c1e2b686ca9caca1b84503b8794273381b51d06d0dfb9c19125ce30e67a8cf72321ca8c50a481e4b96bbbc5b8932e8d5a32fa040c3e29ced4c8bf3541e846f832a7f9406d263a592c0a9bce88be6aed043a9867a7

size = 1024
for lsb_len in range(100, 400, 5):
    print(lsb_len)
    for msb_len in range(100, 400, 5):
        msb, _ = gmpy2.iroot(N, 2)
        shift = int(msb).bit_length() - msb_len
        msb = int(msb) >> shift

        ls = sqrt_power_of_2_mod(N, lsb_len)
        for l in ls:
            lsb = l

            _, x = PolynomialRing(Zmod(N), name='x').objgen()

            f = msb * (2 ** (size - msb_len)) + (x * (2 ** lsb_len)) + lsb
            f = f.monic()
            xs = f.small_roots(X=2**(size - msb_len - lsb_len), beta=0.3)
            for s in xs:
                print( msb * (2 ** (size - msb_len)) + (s * (2 ** lsb_len)) + lsb)

the flag was found around lsb=300.

[crypto/rev] S1 Protocol

The reversing part writeup will be written by @ptr-yudai. The binary works like the following script.

from Crypto.Util.number import *

pattern = 0x1000000000000000000000000000000000000000001000000000000000000000000000000000000000001000000000000000000000000000000000000000001000000000000000000000000000000000000000001000000000000000000000000000000000000000001

while True:
    x = getRandomNBitInteger(21 * 8)
    p = (0b11 << 1022) + (pattern * x << 16) + getRandomNBitInteger(16) | 1
    if isPrime(p):
        break
q = getStrongPrime(1024)
n = p * q

r = getRandomRange(2, n)

flag = long_to_bytes(open("flag.txt", "rb").read())

print(n+1)
print(pow(r, n, n**3))
print(pow(1 + n, flag, (n+1)*(n**3)))

Obviously, the flag is encrypted with modified paillier crypto, and the factor of modulus has some pattern. There was an analogy challenge in the google CTF 2019. Luckily, its solver can be applied as-is. So I don't know how it works.

from sage.all import *


n = 24873956958658402268767683103493059896705299540675048547193795288445376056675974746836994045550208950377561438045299653683756857105762284046700057257044778523863095409390382049154376271272054943837723148622450408029389494774175445756568213417919195725130474727174628849316519649758004047082836095165350139241058167015090602339726250580015660699904551403507821949156006918652616841989957152257987416669296535546243917804424508781976452089018525704521989449529550926409771357879685458344231307480394377651973499196053459100169309910822372188138310059463352985798139208816853247052829533726918240148879918230116779685881


pattern_size = 21*8
prime_size = 1024
x = 2**pattern_size
d0 = 2**pattern_size - 1
w = 2**prime_size
u, v = divmod(n, w)
M = matrix([[x, 0, u * d0 % w],
            [0, x, v * d0 % w],
            [0, 0,         w]])

for vec in M.LLL():
  bx, ax = vec[0], -vec[1]
  p = gcd(ax * w + bx, n)
  if 1 < p < n:
    q = n // p
    break
print(p)
print(q)

[crypto] Lost qKeys

import qiskit
import os
from Crypto.Util.number import bytes_to_long
# from secrets import flag
flag = b"hogepiyo"

class IQuantumComputer:
    def __init__(self, nbits):
        self.nbits = nbits
        self.nbytes = (self.nbits - 1)//8 + 1

    def string_to_bits(self, m):
        if isinstance(m, str):
            m = m.encode()
        return self.decode(self.encode(bytes_to_long(m)))

    def encode(self, m):
        return '{:0{}x}'.format(m, self.nbytes)

    def decode(self, m):
        return '{:0{}b}'.format(int(m, 16), self.nbits)[-self.nbits:]

    def send_qubits(self, msg, dest):
        qbit_config = self.decode(msg)
        q = qiskit.QuantumRegister(9*len(qbit_config))
        circ = qiskit.QuantumCircuit(q)
        for j, c in enumerate(qbit_config):
            if c=='1':
                circ.x(q[9*j])
            circ.cx(q[9*j],q[9*j+3])
            circ.cx(q[9*j],q[9*j+6])
            circ.h(q[9*j])
            circ.h(q[9*j+3])
            circ.h(q[9*j+6])
            circ.cx(q[9*j],q[9*j+1])
            circ.cx(q[9*j],q[9*j+2])
            circ.cx(q[9*j+3],q[9*j+4])
            circ.cx(q[9*j+3],q[9*j+5])
            circ.cx(q[9*j+6],q[9*j+7])
            circ.cx(q[9*j+6],q[9*j+8])

        return quantum_channel(circ, dest)

    def read_qubits(self, circ):
        raise NotImplemented('read_qubits')

    @staticmethod
    def send_message(message, dest):
        classical_channel(message, dest)

    @staticmethod
    def read_message(message):
        raise NotImplemented('read_message')


class LocalQuantumComputer(IQuantumComputer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    @staticmethod
    def read_message(message):
        print(message)


class RemoteQuantumComputer(IQuantumComputer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.key = self.decode(self.encode(bytes_to_long(os.urandom(self.nbytes))))
        self.encoded_flag = self.string_to_bits(flag)

    def encrypt_flag(self, circ):
        self.qbuffer = qiskit.QuantumRegister(self.nbits)
        circ.add_register(self.qbuffer)

        for i, c in enumerate(self.encoded_flag):
            if c=='1':
                circ.x(self.qbuffer[i])

        for i in range(len(self.key)):
            if self.key[i]=='1':
                circ.h(self.qbuffer[i])
        return circ

    def decrypt_flag(self, circ):
        for i in range(self.nbits):
            circ.ch(self.received_qubits[9*i], self.qbuffer[i])

        output = qiskit.ClassicalRegister(self.nbits)
        circ.add_register(output)
        circ.measure(self.qbuffer, output)

        bk = qiskit.Aer.get_backend('qasm_simulator')
        job = qiskit.execute(circ, bk, shots=1)
        res = job.result()
        if not res.success:
            raise MemoryError('You should use a real quantum computer...')
        res = res.get_counts(circ)
        self.res = [self.encode(int(k,2)) for k in res.keys()]

    def read_qubits(self, circ):
        self.received_qubits = circ.qubits
        for i in range(0, len(self.received_qubits), 9):
            circ.cx(self.received_qubits[i], self.received_qubits[i+1])
            circ.cx(self.received_qubits[i], self.received_qubits[i+2])
            circ.ccx(self.received_qubits[i+1], self.received_qubits[i+2], self.received_qubits[i])
            circ.h(self.received_qubits[i])
            circ.cx(self.received_qubits[i+3], self.received_qubits[i+4])
            circ.cx(self.received_qubits[i+3], self.received_qubits[i+5])
            circ.ccx(self.received_qubits[i+4], self.received_qubits[i+5], self.received_qubits[i+3])
            circ.h(self.received_qubits[i+3])
            circ.cx(self.received_qubits[i+6], self.received_qubits[i+7])
            circ.cx(self.received_qubits[i+6], self.received_qubits[i+8])
            circ.ccx(self.received_qubits[i+7], self.received_qubits[i+8], self.received_qubits[i+6])
            circ.h(self.received_qubits[i+6])
            circ.cx(self.received_qubits[i], self.received_qubits[i+3])
            circ.cx(self.received_qubits[i], self.received_qubits[i+6])
            circ.ccx(self.received_qubits[i+3], self.received_qubits[i+6], self.received_qubits[i])

        circ = self.encrypt_flag(circ)
        self.decrypt_flag(circ)


def classical_channel(message, dest):
    dest.read_message(message)


def quantum_channel(qcirc, dest):
    # noise
    dest.read_qubits(qcirc)


def connect(local, remote):
    remote.send_message('passwd:', local)
    pwd = input()
    local.send_qubits(pwd, remote)
    remote.send_message(', '.join(remote.res), local)


def main(nbits):
    local = LocalQuantumComputer(nbits)
    remote = RemoteQuantumComputer(nbits)
    connect(local, remote)


if __name__=='__main__':
    nbits = 8*len(flag)
    main(nbits=nbits)

The server encrypts the flag with quantum computing. But practically, it is no needed to know about quantum computing. After some local experiments and observations, I noticed that the flag is masked by key and our input. So we should input 0 as the password to reduce the bit size of the mask.

Connect to the server many times and get statistics of the masked flag, then the flag can be recovered.