Redmask 2020 - Crypto & Reversing

Di penghujung tahun 2020 saya diberi amanah untuk menjadi salah satu problem setter event Capture The Flag (CTF) yang diselenggarakan oleh Indonesia Cyber Security Independence Resilience Team (CSIRT.ID), yaitu Redmask CTF. Redmask CTF adalah kompetisi keamanan siber nasional yang diselenggarakan secara online babak penyisihan maupun final. Redmask CTF merupakan Jeopardy-style, dimana terdiri dari 5 kategori utama (Reverse, Pwn, Crypto, Web, Forensic) dan 1 kategori tambahan (OSINT) yang hanya tersedia di babak penyisihan.

Sebelumnya terima kasih kepada CSIRT.ID selaku penyelenggara Redmask CTF, mas Anton Sahbana dan mas Hamdan Abdul Aziz selaku event organizer, dan juga @cacadosman, @hanasuru dan @circleous selaku problem setter. Tanpa adanya mereka kompetisi Redmask CTF tidak akan berjalan lancar.

Sebagai salah satu dari empat problem setter, saya diberi tanggung jawab untuk membuat soal kategori Crypto. Berikut beberapa soal dan pembahasan yang saya buat pada event Redmask CTF 2020. Enjoy!

Penyisihan

  • Baby Remainder (Tags: Crypto, Math)
  • Drakjoge (Tags: Crypto, AES)
  • Red Prime (Tags: Crypto, RSA)

Final

  • halftimepad (Tags: Crypto, OTP)
  • Mini LFSR (Tags: Crypto, RNG)
  • poggers (Tags: Reversing)

Baby Remainder

Description

You know what to do with this baby, right?

Author: deomkicer

Challenge

Peserta diberikan dua attachment, yaitu chall.py dan output.txt.

#!/usr/bin/python
from Crypto.Util.number import bytes_to_long, getPrime, isPrime
from Crypto.Util.Padding import pad
import random

FLAG = open('flag.txt').read().strip()

def gcd(x, y):
    while y:
        x, y = y, x % y
    return x

def nextPrime(x):
    y = x | 1
    while not isPrime(y) or x == y:
        y += 2
    return y

def func1(m):
    r = 0
    n = getPrime(24)
    for _ in range(20):
        c = m % n
        r = (r + c) << 24
        n = nextPrime(n)
    return r + n

def func2(m):
    e = 65537
    s = int(1.5 * m.bit_length())
    p = getPrime(s)
    q = getPrime(s)
    n = p * q
    phi = (p-1) * (q-1)
    assert gcd(e, phi) == 1
    c = pow(m, e, n)
    return [c, n]

def main():
    m = bytes_to_long(pad(FLAG, 16))
    x = func1(m)
    c, n = func2(m)
    print '[*] x: {}'.format(x)
    print '[*] c: {}'.format(c)
    print '[*] n: {}'.format(n)

if __name__ == "__main__":
    main()
[*] x: 23094165431786100355526044253709728814798463389708809102582728735375919100958575587180933434416065193831984219858333697884459439237225076085631323146599
[*] c: 35703828195014314779130572173838405792203827843838337164138324349054018626165405608631668424125354824088581206690470493715670441904715732670268298902314992522325166381192347787631033487842179185626477526402060448402994095952498184032625011762001368283090667915206705299930832531126368939887359502769489279148908735016512609818108700326025593402920304007229114235376763826366011429815102687106002897099462599442901323115537245513737287502192642632120652995972085
[*] n: 69322062056110811910771256698054585206504587085352309734114706285067271080556482705613601934594927074503445446788760755236171656631535180458709243715042135691514436000849977156915131384111573721148858175923910499712701585271174643759183003665224191766815397787666172266983527504656043546864175488365999898549975705381211336797642712384196936177534859747966267679353351832867589273653392437775139285515467259488716919224671730822202554164269755489504674616199041

Solution

Bisa dikatakan tidak ada vulnerability pada fungsi func2 selain mengenkripsi nilai $ m $, hanya saja kedua prime yang di-generate memiliki panjang bit yang berasal dari 1.5 kali panjang bit $ m $.

s = int(1.5 * m.bit_length())
p = getPrime(s)
q = getPrime(s)
n = p * q

Pada func1, nilai $ m $ di-modulo sebanyak 20 kali dengan prime yang berbeda-beda, namun 20 nilai prime tersebut tidak tersedia pada output.txt. Hal ini tidak menjadi masalah karena kita diberikan prime terakhir dari 20 prime tersebut, sehingga untuk mencari 19 prime lainnya, kita tinggal implementasi fungsi prevPrime.

def prevPrime(x):
    y = x | 1
    while not isPrime(y) or x == y:
        y -= 2
    return y

Dengan demikian, kita sudah memiliki 20 pasang remainder dan modulus. Selanjutnya, recover nilai $ m $ menggunakan algoritme Chinese Remainder Theorem (CRT). Kita dapat menggunakan fungsi solve_crt yang tersedia di library libnum.

Namun sayangnya hasil yang didapat masih berupa junk, hal ini dikarenakan nilai $ m $ memiliki panjang ±512-bit, sementara dari algoritme CRT yang dihasilkan memiliki panjang ±480-bit (karena kita hanya memiliki 20 pasang 24-bit remainder dan 24-bit modulus). Idenya kita dapat menggunakan dua cara untuk menghasilkan nilai CRT yang mendekati ±512-bit:

  • Kalikan hasil CRT dengan kelipatan Least Common Multiple (LCM) seluruh modulus, dan
  • Tambahkan satu pasang remainder dan modulus

Dari mana kita dapat menemukan satu pasang remainder dan modulus? Mari kita lihat kode program kembali.

m = bytes_to_long(pad(FLAG, 16))

Flag di-padding menggunakan PKCS#7, sehingga kita bisa bruteforce 16 kemungkinan padding bytes dan menambahkan pasangan remainder dan modulus tersebut ke algoritme CRT.

Untuk menghitung LCM dari seluruh modulus, kita tinggal kalikan seluruh modulus tersebut karena seluruh modulus merupakan prime. Seberapa besar kelipatan $ k $ yang nantinya perlu kita bruteforce? Harusnya tidaklah besar karena panjang bit $ m $ yang telah dihasilkan 21 pasang remainder dan modulus sudah mendekati ±512-bit.

Implementasi

Saya gunakan nilai $ c $ dan $ n $ RSA sebagai pengecekan apakah nilai $ m $ yang didapat merupakan flag.

from deom import *

output = open('../peserta/output.txt').read()
output = output.replace('[*] ', '')
output = output.replace(': ', '=')
exec(output)

C = c
N = n
print 'Flag bit-length: %d' % (int(N.bit_length() / 3.0))

def prevPrime(x):
    y = x | 1
    while not isPrime(y) or x == y:
        y -= 2
    return y

def prod(ns):
    bign = 1
    for n in ns:
        bign *= n
    return bign

rem_blocks = []
prime = prevPrime(x & 0xFFFFFF)
x >>= 24
while x:
    rem_blocks.append(x & 0xFFFFFF)
    x >>= 24
rem_blocks = rem_blocks

mod_blocks = [prime]
while len(rem_blocks) != len(mod_blocks):
    prime = prevPrime(prime)
    mod_blocks.append(prime)
mod_blocks = mod_blocks

for i in range(16):
    tmp = '}' + (chr(i + 1) * (i + 1))
    v0 = s2n(tmp)
    v1 = 256 ** len(tmp)
    cs = rem_blocks + [v0]
    ns = mod_blocks + [v1]
    res = solve_crt(cs, ns)
    prodn = prod(ns)
    for k in range(1024):
        tes = k*prodn + res
        if pow(tes, 65537, N) == C:
            print i, k, unpad(n2s(tes), 16)
            break
Flag bit-length: 510
3 350 redmask{____why_are_y0u_padding___why_are_you_paddin999____}

Flag

redmask{____why_are_y0u_padding___why_are_you_paddin999____}

Drakjoge

Description

Kamu adalah seorang komedian yang ingin mendaftar ke salah satu komunitas komedi yang cukup terkenal, yaitu Indonesian Drakjoge. Namun pada saat mendaftar, kamu dihadapkan dengan seorang Bang Haxor, salah satu sesepuh di komunitas Indonesian Drakjoge. Bang Haxor tidak bisa menerima kamu ke komunitas jika kamu tidak mengerti apa yang ia katakan. Ayo buktikan kalau kamu layak berada di komunitas Indonesian Drakjoge!

nc 202.148.27.84 30001

Author: deomkicer

Challenge

Peserta diberikan sebuah attachment server.py dan servis ke 202.148.27.84 30001.

#!/usr/bin/python
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from libnum import b2s, s2b
import struct
import sys

class Unbuffered(object):
    def __init__(self, stream):
        self.stream = stream
    def write(self, data):
        self.stream.write(data)
        self.stream.flush()
    def writelines(self, datas):
        self.stream.writelines(datas)
        self.stream.flush()
    def __getattr__(self, attr):
        return getattr(self.stream, attr)

class Cyclic:
    def __init__(self):
        self.mask = 0xFFFFFFFF
        self.poly = 0xEDB88320
        self.table = []
        for byte in range(256):
            tmp = 0
            for _ in range(8):
                if (byte ^ tmp) & 1:
                    tmp = (tmp >> 1) ^ self.poly
                else:
                    tmp >>= 1
                byte >>= 1
            self.table.append(tmp)
    def calc(self, string):
        value = self.mask
        for c in string:
            value = self.table[(ord(c) ^ value) & 0xFF] ^ (value >> 8)
        return struct.pack('>I', (-1 - value) & self.mask)

sys.stdout = Unbuffered(sys.stdout)
FLAG = open('flag.txt').read().strip()
KEY = AES.get_random_bytes(16)
CC = Cyclic()

def intro():
    print '''
................................................................
........................----::::::::::---.......................
.............-/+syhdddmmmmmmddddddddmmmmmmddhhyyso+:............
.............yhhhhdddmmmmmmddddddddddmmmmmdddddhhhhh+...........
............-yhhhhdddmmmmmddddddddddddddmmmddddhhhhhs...........
............:yhhhhhhhddddmdddddddddddddddddhhhhhhhhhy...........
............/yyo------:/+shdddddddddddhso++::--:+yyyy...........
............/yyoyyyyys+:...:ohdddddho-..-/osyhyyo+yyy...........
............/syyyhhhhhhhys/-.-hdddy..-+syhhhhhhhyyyyy...........
............/ssyyyyyyyyyyyssosddddhoosyyyyyyyyyyyyyss...........
............/ysooo+////::::/oyhmmmhyso+//////++ooosss...........
............/ssyyyysoo+++osyhhhmmdhhys+//:://+syhyyso...........
............-yyhhddmmddddddddhhdmdhhhddddddddddddhhy+...........
.............ssyyhddddddddddhhhdmdhhhdddddddddhhhyys:...........
.......  ....oosyyhhddddddhhhhhdmdhhhhhddddhhhyyssso............
..... . .    -soosyyyyyyyyhhhhhddmhhhhhsssyyyyysooo+......   ...
..... .   . ..:sos::syyhddhssyyhhhyyssyhhhyso/:/+os-....      .
.....     ...../sss--shhhhhhy:://:-/yyhhhhhh+./syy+.... .
.....         ..+ssyo..://+/...-+....+oo+/:.-osyyo-....
.....         ...+sssyy+:-....-sss-......:+yysyys/.....
.....         ....+sysssyyyhhhyyyyyyhhhhyssssyss+......
.....        ....../syyyyyssss+oooooossssssyyss/.......
.......       ......-oyhyyhhhhy. .:hhhyyssysso-........
....           ..   ../syyyhhds...-hhhyyyyso:.
                       .+yyyhhs. .-hhhyyso:.
                         .+syyh. .ohyys+-.
                           .:+o/ -so/-.
       ........                ..... .               ........
  ________   ....         __         __         ...........
  \______ \____________  |  | __    |__| ____   ____   ____
   |    |  \_  __ \__  \ |  |/ /    |  |/  _ \ / ___\_/ __ \
   |    .   \  | \// __ \|    <     |  (  <_> ) /_/  >  ___/
  /_______  /__|  (____  /__|_ \/\__|  |\____/\___  / \___  >
          \/   ....    \/     \/\______|     /_____/      \/ v1.0
'''

def main():
    while True:
        try:
            inp = b2s(raw_input('Kamu: ').strip())
            chs = CC.calc(inp + FLAG)
            ptx = pad(inp + chs + FLAG, 16)
            cip = AES.new(KEY, AES.MODE_ECB)
            ctx = cip.encrypt(ptx)
            print 'Bang Haxor: %s\n' % (s2b(ctx))
        except:
            break

if __name__ == "__main__":
    intro()
    main()

Solution

Yang perlu diketahui bahwa Cyclic.calc akan menghasilkan 4-byte, dimana itu merupakan algoritme CRC32 checksum.

ptx = pad(inp + chs + FLAG, 16)
cip = AES.new(KEY, AES.MODE_ECB)
ctx = cip.encrypt(ptx)

Variabel ptx di-encrypt menggunakan AES.MODE_ECB. Variabel inp dapat dikontrol, namun chs dan FLAG masing-masing tidak dapat kita kontrol dan tidak diketahui. Variabel chs ini memproteksi agar Chosen-plaintext attack tidak bisa dilakukan dari depan, namun kita tetap dapat melakukan attack dari belakang, dengan catatan harus mengetahui panjang flag terlebih dahulu. Panjang flag bisa diketahui dengan memanfaatkan banyaknya karakter input yang kita masukkan, lalu bandingkan berapa banyak block yang dihasilkan oleh Bang Haxor. Berikut kodenya.

Implementation

from deom import *
import string

# p = process('./server.py')
p = remote('localhost', 30001)

def kirim(x):
  p.sendlineafter('Kamu: ', s2b(x))
  p.recvuntil('Bang Haxor: ')
  res = p.recvline().strip()
  return b2s(res).encode('hex')

def panjang_flag():
  for i in range(1, 16+1):
    res = kirim('z' * i)
    print i, len(res)

def potong(x):
  res = []
  for i in range(0, len(x), 32):
    res.append(x[i:i + 32])
  return res

panjang_flag()
LEN_FLAG = 128 / 2 - 13 - 4
JUNK = -(LEN_FLAG + 4) % 16
flag = ''
ctr = 0

while True:
  for i in range(16):
    payload = 'z'*(JUNK + i + 1)
    correct_block = kirim(payload)
    correct_block = potong(correct_block)[-ctr-1]
    for c in string.printable:
      payload = c + flag + chr(16 - i - 1) * (16 - i - 1)
      test_block = kirim(payload)
      test_block = potong(test_block)[0]
      if test_block == correct_block:
        flag = c + flag
        print flag
        if flag.startswith('redmask{'):
          p.close()
          exit()
        if len(flag) % 16 == 0:
          ctr = 0
        break
  ctr += 1
$ python solve.py
[+] Starting local process './server.py': pid 23273
1 128
2 128
3 128
4 128
5 128
6 128
7 128
8 128
9 128
10 128
11 128
12 128
13 160
14 160
15 160
16 160
}
a}
ya}
_ya}
e_ya}
re_ya}
are_ya}
_are_ya}
y_are_ya}
ny_are_ya}
wny_are_ya}
owny_are_ya}
lowny_are_ya}
clowny_are_ya}
_clowny_are_ya}
w_clowny_are_ya}
ow_clowny_are_ya}
how_clowny_are_ya}
_how_clowny_are_ya}
__how_clowny_are_ya}
e__how_clowny_are_ya}
ge__how_clowny_are_ya}
oge__how_clowny_are_ya}
joge__how_clowny_are_ya}
kjoge__how_clowny_are_ya}
akjoge__how_clowny_are_ya}
rakjoge__how_clowny_are_ya}
drakjoge__how_clowny_are_ya}
_drakjoge__how_clowny_are_ya}
o_drakjoge__how_clowny_are_ya}
to_drakjoge__how_clowny_are_ya}
_to_drakjoge__how_clowny_are_ya}
3_to_drakjoge__how_clowny_are_ya}
m3_to_drakjoge__how_clowny_are_ya}
om3_to_drakjoge__how_clowny_are_ya}
com3_to_drakjoge__how_clowny_are_ya}
lcom3_to_drakjoge__how_clowny_are_ya}
elcom3_to_drakjoge__how_clowny_are_ya}
Welcom3_to_drakjoge__how_clowny_are_ya}
{Welcom3_to_drakjoge__how_clowny_are_ya}
k{Welcom3_to_drakjoge__how_clowny_are_ya}
sk{Welcom3_to_drakjoge__how_clowny_are_ya}
ask{Welcom3_to_drakjoge__how_clowny_are_ya}
mask{Welcom3_to_drakjoge__how_clowny_are_ya}
dmask{Welcom3_to_drakjoge__how_clowny_are_ya}
edmask{Welcom3_to_drakjoge__how_clowny_are_ya}
redmask{Welcom3_to_drakjoge__how_clowny_are_ya}
[*] Stopped process './server.py' (pid 23273)

Flag

redmask{Welcom3_to_drakjoge__how_clowny_are_ya}

Red Prime

Description

Kalo ada cara ribet kenapa harus gampang?

Author: deomkicer

Challenge

Peserta diberikan dua attachment chall.py dan output.txt.

#!/usr/bin/python
from Crypto.Util.number import *
from fractions import Fraction
from flag import flag

def calc(a, b, c):
    s = float(a if b else -a)
    while True:
        s *= -c
        a += s
        if abs(s) < .0001:
            return int(a) + 1

def genPrime(nbit):
    while True:
        x = getRandomNBitInteger(nbit)
        p = calc(x, False, Fraction(11, 17))
        q = calc(x, True, Fraction(13, 19))
        if isPrime(p) and isPrime(q):
            return p, q

def encrypt(m, n):
    m = bytes_to_long(m)
    assert m < n
    return pow(m, 65537, n)

nbit = 512
p, q = genPrime(nbit)
n = p * q
c = encrypt(flag, n)

print 'c =', c
print 'n =', n
c = 36329427815660857273975769241128439913876212605062992262994190763574776272663976674103443716897608306230018751864075024103462484630058628345543656344514348327259129134588398512790627389534011046346572424412645320049318379077699227840195565804368461545343159195445136776937608078872952802876228889700607871824
n = 90426937012775177429000599791153537960097523093679151685293953736111122844607505842911804538666518238999277005201661454277975570212424800306742495208980816771772338412911631058816972038342529285531308198920534491617390252583741071016207659940881379473007727445261857366529091574347745852612560736676904697857

Solution

Fokus challenge ini berada pada fungsi calc.

def calc(a, b, c):
    s = float(a if b else -a)
    while True:
        s *= -c
        a += s
        if abs(s) < .0001:
            return int(a) + 1

Variabel $ c $ merupakan pecahan yang berada pada rentang nilai $ 0 < c < 1 $, dan jika dilihat dari fungsi calc, nilai $ s $ dikalikan $ -c $ secara terus-menerus hingga nilai $ s $ benar-benar mendekati nol. Dengan demikian, kita bisa menyadari ini sebagai jumlah deret geometri tak hingga, dengan rasio sebesar $ -c $. Dengan menggunakan rumus jumlah deret geometri tak hingga $ \frac{a}{1 - r} $, kita bisa menemukan formula yang dihasilkan fungsi calc terhadap nilai $ x $.

p = calc(x, False, Fraction(11, 17))
q = calc(x, True, Fraction(13, 19))
p = 39 * x / 28
q = 19 * x / 32
n = p * q
n = (39 * x / 28) * (19 * x / 32)
n = (741 * x * x / 896)
896 * n = 741 * x * x
896 * n / 741 = x * x
x = sqrt(896 * n / 741)

Sebenernya nilai $ x $ yang didapat dari persamaan ini tidak begitu akurat, tapi salah satu prime $ q $ masih bisa didapat dari nilai $ x $ tersebut. Untuk mengembalikan prime $ p $, kita bisa membagi $ n $ terhadap $ q $.

Implementation

from deom import *

exec(open('output.txt').read())

def calc(a, b, c):
    s = float(a if b else -a)
    while True:
        s *= -c
        a += s
        if abs(s) < .0001:
            return int(a) + 1

def genPrime(x):
    p = calc(x, False, Fraction(11, 17))
    q = calc(x, True, Fraction(13, 19))
    return p, q

x = iroot(896 * n / 741, 2)[0]
print 'x =', x

_, q = genPrime(x)
p = n / q
print 'p =', p
print 'q =', q

d = inverse(65537, (p - 1) * (q - 1))
m = pow(c, d, n)
print n2s(m)
x = 10456679839079558435741293278799951804696465146133756844686290313160766675050958143205793164740013321816920439873929284909687775742842280579847027557233139
p = 14564661204432239532713999155601891754421924746339293744623528128911381783311842948286857271889672256015768465228080468579260508202704806823788292375314433
q = 6208653654453488918561497666988432859118874764084736512542892043121192330662698577624399240484750055090263661210623544244224366737822586320004311446192129
redmask{an0ther_w4yyy_to_5um_RSA_up_45d1e8}

Unintended solution dari peserta cukup mengejutkan saya sebagai problem setter. Kita juga bisa bruteforce nilai $ x $ dengan pendekatan binary search karena nilai $ x $ dan $ n $ berbanding lurus. Sadge.

Flag

redmask{an0ther_w4yyy_to_5um_RSA_up_45d1e8}

halftimepad

Description

I’ll generate a new random key each time you connect. There’s no way for you to decrypt my flag!

nc 103.55.38.18 30001

Author: deomkicer

Challenge

Peserta diberikan sebuah attachment server.py dan servis ke 103.55.38.18 30001.

#!/usr/bin/python
from binascii import hexlify
from re import findall
from os import urandom
import sys

class Unbuffered(object):
    def __init__(self, stream):
        self.stream = stream
    def write(self, data):
        self.stream.write(data)
        self.stream.flush()
    def writelines(self, datas):
        self.stream.writelines(datas)
        self.stream.flush()
    def __getattr__(self, attr):
        return getattr(self.stream, attr)

sys.stdout = Unbuffered(sys.stdout)
madpad = lambda a, b: ''.join([chr(ord(i) ^ ord(j) ^ 0x69) for i, j in zip(a, b)])
genkey = lambda n: hexlify(urandom(n / 2))

if __name__ == '__main__':
    flag = open('flag.txt').read().strip()
    m = findall(r'^redmask{(\w+)}$', flag)[0]
    print hexlify(madpad(m, genkey(len(m))))

Solution

Kode program singkat, padat dan jelas. Variabel $ m $ merupakan flag tanpa format flag redmask{...}, lalu flag di-xor dengan random hex character. Karena hex character hanya ada 16 kemungkinan (0123456789abcdef), maka kita tinggal kumpulkan beberapa ciphertext yang didapat dari servis, lalu amati kemunculan flag xor dengan salah satu karakter 0123456789abcdef. Solusi tercepat yang dapat problem setter temukan yaitu kita dapat recover flag secara menyeluruh dengan mengumpulkan kurang dari 100 ciphertext. Berikut kodenya.

Implementation

#!/usr/bin/python
from pwn import *

LEN = 20
boxes = [[] for _ in range(LEN)]

ctr = 0

while not all(len(box) == 16 for box in boxes):
    # p = process('./server.py', level='warn')
    p = remote('localhost', 30001, level='warn')
    pantun = p.recvline(0).decode('hex')
    assert len(pantun) == LEN
    for i in range(LEN):
        if pantun[i] not in boxes[i]:
            boxes[i].append(pantun[i])
    p.close()
    ctr += 1

print ctr

flag = ''

for i in range(LEN):
    box = sorted(boxes[i])
    for j in range(256):
        baru = []
        for k in '0123456789abcdef':
            baru.append(chr(j ^ ord(k)))
        baru = sorted(baru)
        if baru == box:
            flag += chr(j ^ 0x69)
            print flag
            break

assert len(flag) == LEN
print 'Flag: redmask{\%s}' % (flag)
$ python solve.py
97
e
ey
eyy
eyy_
eyy_y
eyy_y0
eyy_y0_
eyy_y0_W
eyy_y0_Wh
eyy_y0_Wha
eyy_y0_What
eyy_y0_What_
eyy_y0_What_t
eyy_y0_What_t1
eyy_y0_What_t1m
eyy_y0_What_t1me
eyy_y0_What_t1me_
eyy_y0_What_t1me_p
eyy_y0_What_t1me_p4
eyy_y0_What_t1me_p4d
Flag: redmask{eyy_y0_What_t1me_p4d}

Flag

redmask{eyy_y0_What_t1me_p4d}

Mini LFSR

Description

Plz help me to decrypt my encrypted flag with my encrypted key ://

Author: deomkicer

Challenge

Peserta diberikan dua attachment chall.py dan output.txt.

#!/usr/bin/env python3
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Util.number import getRandomNBitInteger
import re

from flag import flag

class LFSR64:
    def __init__(self, state, taps):
        self.mask = (1 << 64) - 1
        self.state = state
        self.taps = taps
    def encrypt(self, m: bytes) -> bytes:
        r = bytearray()
        for b in m:
            r.append(b ^ self._gene(8) ^ 0x69)
        return bytes(r)
    def _gene(self, n: int) -> int:
        s = 0
        for _ in range(n):
            s ^= self._tick()
            s <<= 1
        s >>= 1
        return s
    def _tick(self) -> int:
        r = self.state
        self.state = (r << 1) & self.mask
        i = (r & self.taps) & self.mask
        s = 0
        while i:
            s ^= (i & 1)
            i >>= 1
        self.state ^= s
        return s

def main():
    ptx = re.findall(r'^redmask{(\w+)}$', flag)[0].encode()
    rsa = RSA.generate(1024)
    oaep = PKCS1_OAEP.new(rsa)
    lfsr = LFSR64(getRandomNBitInteger(64), 0xC936000000000000)
    with open('output.txt', 'w') as f:
        f.write(f"{lfsr.encrypt(ptx[:len(ptx)//2]).hex()}\n")
        f.write(f"{oaep.encrypt(ptx[len(ptx)//2:]).hex()}\n")
        f.write(f"{lfsr.encrypt(rsa.export_key('PEM')).hex()}\n")
        f.close()

if __name__ == '__main__':
    main()
c9bca96b3692b684627d2f3663515f662acae6
5321e066c8dc9fd994ab8466b35b06bc263b4b4789967f843d41ad3991485507c7c3bd5f6dcc90b6963795385a571e1cfd911995fb98f5d6cd5142bc28d7d704c216305c4c8a6672d63ff2fbdbc2ada80c02daff05dcd040e17196f6576c38ca34585d405c851b1dfdb57d34826261c05ece73403b983b636eb6021972186f67
c42df9728dbe3d02212ec05529edaee2957e0b2a2a63dcb4d6db86b5fb8ae51189bf23f7d93dccce6c65aa8083dfe2583caf3c052e814d39977ba7907d975e0bd92700a64ee2899428e470470c6b13be0b1ffc2112a50c07fb7961f3c794fb1f57455ffe017acda265467d4ee14e3b4772222b96c0f1659b3476734b3927393637360468929817e91cfc6c7e02474ceeed42fc06439e3acb0801b5aeda8d1adb9dddd20971d7638889ecb7759602debd7fe4adca6dff528c944c8735281519a61467999329eb65dc3ba23becf78fb0b1f5d9ccccfebdd3a3754652924c89649ad5fd3e2c024b1a41996e04e936b6dcc9aabebac45531c576c9d98942865329ac42549a144fee1aecb4f13a2f685281815e9b5146ec569a8eda03dfe30d34e1dcb6a5be146f1a5c81ad590f312cf307edc597364ee5cd58fd27c11867c106ec0713f0e4d8cff98db8254eae91ebfc65e04be3ca768b02ce3b9f9c263f7e187fa72d743209ce6ab58cbe4c7fde2b300629fa49112cf58a534764daeb52ac19adf8761a496b4fe1148d574cf7ff78091bcf4b1c14d896b73413755babe02c149d18e36426ffc23e3cce3621d69c943686221750ccab7b7ff76e05d52bc0c069217597dd827eb9811378c2aa76e530c0c6d71ee71fc4518e0ec6c8989ad248e2d26861ba00d768a8e9092dfcc2b215009972869e1987a3ea55b50cb8cf5db42185f338867d33917bc3bea048598db08352a803fae832fba7a94cac959b6a83bc265805247f827ed01bb68178ee9ef0c83e9b4fdd6340c553262175a0716dd8479f6c9be37c1b21239ea33982d92f97ba3d5164cfcc98d0e11544e8ce1cf1c6fa4b2f54992dc8caa0c6f0aefd67078f8ac2e19ebcb131ce806e43f71daad51e6f872bbe4c6d5f0aa8cee9a9bab6b228f0075934358d4990015ef1670544621796268d9e59c5affeea19ca2104f39d278d2f964b620975e536784cfc3741a0a3f0afc8f08dfea34c40c8ded494106946d093d6c74f2a0d807085101aa2bbe0d5b822f37a384dd720130bf1c3b17aa38e1e76d873d06cc6b9b545896ad1ade4b4b4c0499dd8daa1ab75d75633b349e08de5aef8aedbd1b881da3bec24dd42f2fe1acc2704203ca669154f6a7f0eec8bc9fca554a4d4941733388821a491d29852a48909080b392e74843b687a415f120e30f1ec03471b1c0e0732a897756642d9eacd6c4bc024532bd0683eaf0bc7100c72abd31acb20f99b5c

Solution

Jika diamati alur dari kode program, potongan flag pertama dienkripsi menggunakan Linear Feedback Shift Register (LFSR), yang pada kasus ini merupakan Weak Random Number Generator (RNG). Sementara potongan flag kedua dienkripsi menggunakan RSA dengan tambahan PKCS#1 OAEP. RSA key tersebut juga dienkripsi menggunakan LFSR dan dilampirkan pada attachment output.txt.

Seperti yang kita ketahui, RSA key memiliki format sebagai berikut.

-----BEGIN RSA PRIVATE KEY-----
<string base64>
-----END RSA PRIVATE KEY-----

Karena LFSR yang digunakan cukup kecil (64-bit), maka kita dapat mengetahui deretan beberapa bit output LFSR dengan melakukan xor terhadap string -----BEGIN RSA PRIVATE KEY----- (sebenarnya beberapa karakter saja cukup). Lakukan invert LFSR menggunakan $ \mathrm{taps} = \mathrm{0xC936000000000000} $ hingga kita menemukan initial state dari LFSR. Generate ulang LFSR menggunakan initial state dan $ \mathrm{taps} $ tersebut, lalu kita bisa decrypt potongan flag pertama dan RSA key. Setelah didapat RSA key, decrypt potongan flag kedua.

Implementation

#!/usr/bin/env python3
from Crypto.Cipher import PKCS1_OAEP
from deom import *

class LFSR64:
    def __init__(self, state, taps):
        self.mask = (1 << 64) - 1
        self.state = state
        self.taps = taps
    def encrypt(self, m: bytes) -> bytes:
        r = bytearray()
        for b in m:
            r.append(b ^ self._gene(8) ^ 0x69)
        return bytes(r)
    def _gene(self, n: int) -> int:
        s = 0
        for _ in range(n):
            s ^= self._tick()
            s <<= 1
        s >>= 1
        return s
    def _tick(self) -> int:
        r = self.state
        self.state = (r << 1) & self.mask
        i = (r & self.taps) & self.mask
        s = 0
        while i:
            s ^= (i & 1)
            i >>= 1
        self.state ^= s
        return s

out = open('../peserta/output.txt').read().strip().split()
c1 = bytes.fromhex(out[0])
c2 = bytes.fromhex(out[1])
c3 = bytes.fromhex(out[2])

randbs = xor(c3, b'-----BEGIN RSA PRIVATE KEY-----', b'\x69')
output = bin(int.from_bytes(randbs, 'big'))[2:]
output = list(map(int, list(output)))[:64]

length = len(output)
state = 0
taps = 0xC936000000000000

for i in range(length):
    state |= output[i] << (length - 1 - i)

for _ in range(length + (len(c1) * 8)):
    bit = state & 1
    tmp = taps & (state >> 1)
    while tmp:
        bit ^= (tmp & 1)
        tmp = tmp >> 1
    state = (state >> 1) | (bit << (length-1))

lfsr = LFSR64(state, taps)
m1 = lfsr.encrypt(c1)
m3 = lfsr.encrypt(c3)
print(m3.decode())

rsa = RSA.import_key(m3)
oaep = PKCS1_OAEP.new(rsa)
m2 = oaep.decrypt(c2)
print('[*] Flag: redmask{\%s}' % (m1+m2).decode())
$ python3 solve.py
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDArTssW/ve2yhMSyKCyOYGBVtkmpsw56bmO4sTgCm3ifYUcgx7
xtRqDxmTBDepfX9bZjPjqoJmswXDX8Xz4OS0xML3NYHLdatDR3s09qylGLxR5BZr
ZihIeD77uOJmYullm+B7Mtxp20M+Qg8G6DsMZ/M9nfirzZ++L0wpCJGtnQIDAQAB
AoGAHwKiip4BWIRgT4CA55jAQMl9mfWyMuRNvdy+d7SBwlWqLUSP0soU/OmSPacI
8wv3l/UdNoVbuH4UAuENYeuDviGAe4qebAwCXJ90vsWpvzob48X0SL4Sqw2sri/c
vRS14FrplEhaB0RaM4/Lr0GDmAgpyIgqOkHW2A/uB0+nEacCQQDWVd/iCl1YkzQx
raM+qBcQvgDr0flciMF5VdMf6xfkaJsE/0q9Mlq0Vq/4QHmMl9j+WOk5SOFSYDAa
O4kkjRr/AkEA5iGJTi8++Fe9wF3JUgmGWmect6h/2MwfAD5340mug1mhJx7CL0wB
J6xTibFSQC3gbQJdLjGAerpgsKTdxZDDYwJAZptpuH6ZvWOLIxUrBz3U/PDY5Av7
Qm89n+aUUb1sDK5/N983WmeWwKqXR1MmXUX8XZcW35OiOptNq+FAgD0E5QJAPvUD
xDDmsDgIwDyoG9phOBBKbnAZcaz9+ioc0EBTDroRfUtL4naPhlP9kpjBIK+sSwYv
ibifJnQgiZLA3RCqcwJBALbtM8mWQ/B+MzlDQogxrcB2PbM0pvlp5WiFU3D/UlH6
yIRj/8+CafiPvaHf/Lk82kuCqxk2Rn/fEn62sA7CD/Y=
-----END RSA PRIVATE KEY-----
[*] Flag: redmask{plzz_use_s3cur3__R_N_G__nxtime_d1bd4ab}

Flag

redmask{plzz_use_s3cur3__R_N_G__nxtime_d1bd4ab}

poggers

Description

Poggers

Author: deomkicer

Challenge

Diberikan dua attachment poggers yang merupakan ELF 64-bit LSB executable dan output.txt.

z28HADMcwc9AYjKmNiAyPLABws/PnxMyLGLCAHB3YmB1B6BTs0J0Vw==

Solution

File executable poggers sendiri merupakan binary golang, terdapat fungsi main_main ketika dilakukannya proses decompile menggunakan IDA. Input user diambil dari os.Args[1]. Untuk mempermudah proses decompile, gunakan binja + IDA + gdb (solusi dari @vidner, finalis yang berhasil mendapatkan first blood).

Ada 3 bagian utama yang menjadi fokus reverse alur program.

  • Fungsi sus hanya ngeshuffle posisi karakter. Reverse sus dengan implementasi python:
def revSus(s):
    v0 = s[len(s) / 2:][::-1]
    v1 = s[:len(s) / 2][::-1]
    return ''.join([i + j for i, j in zip(v0, v1)])
  • Gray code x ^ (x >> 1). Reverse gray code dengan mengunakan library libnum.rev_grey_code.

  • RotateLeft64 -> RotateRight64.

Implementation

#!/usr/bin/python
from base64 import b64decode
from libnum import n2s, rev_grey_code, s2n
from pwn import xor

def revSus(s):
    v0 = s[len(s) / 2:][::-1]
    v1 = s[:len(s) / 2][::-1]
    return ''.join([i + j for i, j in zip(v0, v1)])

def rotateRight64(num, k):
    b = bin(num)[2:].zfill(64)
    return int(b[-(k % 64):] + b[:-(k % 64)], 2)

out = open('output.txt').read().strip()
out = xor(b64decode(out), '\x69')
out = [out[i:i + 8][::-1] for i in range(0, len(out), 8)]
out = [rotateRight64(s2n(i), 13) for i in out]
out = [n2s(rev_grey_code(i)) for i in out]
out = revSus(''.join(out))
print out
$ python solve.py
redmask{No_SAT_solv3rs_no_pr0bl3m}&&&&&&

Flag

redmask{No_SAT_solv3rs_no_pr0bl3m}