ふるつき

v(*'='*)v かに

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")