서버 이전

오랫동안 함께한 서버를 버리고 옮기게 되었습니다.

여러가지 사유가 있지만, 가장 큰 이유는 무지한 상태에서 서버를 사용한 나머지 다양한 업보가 쌓였다는 것입니다. (virtualenv 없이 Python을 사용하다보니 무언가 꼬이고… root 계정은 살아있고…)

이번에는 좀 더 현명하게 사용하기 위해 노력해야겠습니다. 파이팅 ㅎㅎ

Coppersmith’s method

긴 글은 아니고, Coppersmith’s method에 대해서 공부하고 싶으신 분들을 위한 요약 글입니다.

기본

기본적인 Coppersmith’s method를 이해하는데 도움이 되는 글은 역시 Alexander May의 “New RSA Vulnerabilities Using Lattice Reduction Methods”라는 글이 아닐 수 없네요.

Coppersmith’s method의 근본이 되는 Lattice와 LLL theorem에 대해서 간략하게 설명하며, Coppersmith’s method의 univariate case와 multivariate case, 그리고 그 응용들에 대해서 자세히 설명합니다.


또한 sage를 통해서 간단한 경우에 대해서 Coppersmith’s method를 어떻게 사용할 수 있는지를 알아보는 일본어 글도 찾아볼 수 있습니다. (SageMathを使ってCoppersmith’s Attackをやってみる)

다만 해당 글의 코드는 엄밀하지 못하기 때문에, 아래 PPT도 한 번 읽어보시는 것을 추천합니다.


작년 겨울에 위의 문서들을 바탕으로 PLUS 동아리에서 세미나를 할 때 사용한 PPT를 첨부합니다. (퀄리티가 좋지 못한 점 양해 부탁드려요 ㅎㅎ)

심화

Boneh-Durfee Attack

Boneh-Durfee Attack의 기본에 대해서 이해하고 싶으시다면 ebmoon님의 “RSA Attack Using LLL – Understanding Boneh-Durfee Attack” 문서를 참고하시는 것을 추천합니다.

Sage로 작성한 Boneh-Durfee Attack 코드는 RSA-and-LLL-attacks repository에서 확인하실 수 있습니다.

On Factoring Given Any Bits

한 때 트위터에서 소개했던 코드입니다. (링크)

“Solving Linear Equations Modulo Divisors: On Factoring Given Any Bits” 논문을 바탕으로 작성한 것으로, random한 위치의 bit를 모르는 경우 뿐만 아니라 multivariate linear equation을 푸는 코드입니다.

그 외

Partial information으로부터 key를 복구하는 방법에 대해서 다양하게 다룬 “How to recover cryptographic keys from partial information”라는 PPT도 참고하기 좋습니다.

[zer0pts CTF 2020] dirty laundry

Do you wanna air my dirty laundry?


How to solve

Recovering PRNG

The challenge uses 256-bit LFSR PRNG, of which seed is the lower 256-bit of 1024-bit prime number.

Let’s see pailler_enc unction.

def paillier_enc(m, p, noise):
    p = next_prime(p + noise)
    q = getStrongPrime(512)
    n = p * q
    g = (1 + prng.rand() * n) % n**2
    c = pow(g, m, n**2) * pow(prng.rand(), n, n**2) % n**2
    return n, g, c

The value p is 1024-bit, so n should be 1536-bit. This means that g = (1 + prng.rand() * n) % n**2 is just same as g = 1 + prng.rand() * n, beacuse prng.rand() is only 256-bit.

So, you can get some prng.rand() values from gs, and then recover the PRNG by reversing the PRNG steps.

class PRNG256(object):
    def __init__(self, seed):
        self.mask = (1 << 256) - 1
        self.seed = seed & self.mask

    def _pick(self):
        b = ((self.seed>>0)^(self.seed>>2)^(self.seed>>5)^(self.seed>>10)^1)&1
        self.seed = ((self.seed>>1)|(b<<255)) & self.mask
        return b

    def _rev(self):
        b = ((self.seed>>255)^(self.seed>>1)^(self.seed>>4)^(self.seed>>9)^1)&1
        self.seed = ((self.seed<<1)|b) & self.mask

    def rev(self):
        for i in range(256):
            self._rev()

    def rand(self):
        x = 0
        for i in range(256):
            x = (x << 1) | self._pick()
        return x

with open('output.txt', 'r') as f:
    ln = len("[+] pubkey: ")
    lines = f.readlines()
    pubkeys = eval(lines[1][ln:])
    shares = eval(lines[2][ln:])

g1 = (pubkeys[0][1] - 1) // pubkeys[0][0]
g1_rev = int(format(g1, '0256b')[::-1], 2)

prng2 = PRNG256(g1_rev)

prng2.rev()
prng2.rev()

Recovering msg from ciphertexts

Now we know every prng.rand() values. Then, how can we recover m from c = pow(g, m, n**2) * pow(prng.rand(), n, n**2) % n**2?

First, we know pow(prng.rand(), n, n**2), so we can get pow(g, m, n**2).

About pow(g, m, n**2), let’s use the lemma:

(1+n)^x \equiv 1+nx (\textrm{mod}\ n^2)

and L function from the Paillier cryptosystem. (Check out Background section of the wikipedia article.)

Let’s name the prng.rand() of g = 1 + prng.rand() * n as noise. Then,

g \equiv 1 + n \cdot noise \equiv (1 + n)^{noise} (\textrm{mod}\ n^2)

g^m \equiv (1 + n)^{noise \cdot m} \equiv 1 + n \cdot noise \cdot m (\textrm{mod}\ n^2)

Therefore, if the value of pow(g, m, n**2) is x, we can get m by

L(x) \cdot noise^{-1}\ \textrm{mod}\ n.

# ...
noises = [ [ prng2.rand() for j in range(3) ] for i in range(5) ]

assert all([ noises[i][1] == (pubkeys[i][1] - 1) // pubkeys[i][0] for i in range(5) ])

from Crypto.Util.number import inverse
msgs = []
for i in range(5):
    n = pubkeys[i][0]
    gm = shares[i][1] * pow(inverse(noises[i][2], n**2), n, n**2) % (n**2)
    assert (gm - 1) % n == 0
    m = ((gm - 1) // n) * inverse(noises[i][1], n) % n
    # Actually the input m of paillier_enc is f(x) + noise.
    # Removing noise here
    m -= noises[i][0]
    msgs.append(m)

Recovering PRIME and flag

Let’s see the make_shares funciton.

def make_shares(secret, k, shares, prime=PRIME):
    PR, x = PolynomialRing(GF(prime), name='x').objgen()
    f = PR([secret] + [ZZ.random_element(prime) for _ in range(k-1)])
    xy = []
    pubkey = []
    for x in range(1, shares+1):
        noise = prng.rand()
        n, g, y = paillier_enc(f(x) + noise, prime, noise)
        pubkey.append([n, g])
        xy.append([x, y])
    return pubkey, xy

We can write f = PR([secret] + [ZZ.random_element(prime) for _ in range(k-1)]) as ax^2 + bx + secret with unknown a, b.

We know the values f(1), f(2), f(3), f(4), f(5), but we still don’t know PRIME value.

To recover PRIME, follow these steps:

f(1) = a + b + secret (\textrm{mod}\ PRIME)
f(2) = 4a + 2b + secret (\textrm{mod}\ PRIME)
f(3) = 9a + 3b + secret (\textrm{mod}\ PRIME)
f(4) = 16a + 4b + secret (\textrm{mod}\ PRIME)
f(5) = 25a + 5b + secret (\textrm{mod}\ PRIME)

f(2) – f(1) = 3a + b (\textrm{mod}\ PRIME)
f(3) – f(2) = 5a + b (\textrm{mod}\ PRIME)
f(4) – f(3) = 7a + b (\textrm{mod}\ PRIME)
f(5) – f(4) = 9a + b (\textrm{mod}\ PRIME)

f(3) – 2f(2) + f(1) = 2a (\textrm{mod}\ PRIME)
f(4) – 2f(3) + f(2) = 2a (\textrm{mod}\ PRIME)
f(5) – 2f(4) + f(3) = 2a (\textrm{mod}\ PRIME)

Those three values should be same in mod PRIME. Let’s see the values.

# ...
diff1 = [msgs[i + 1] - msgs[i] for i in range(4)]
diff2 = [diff1[i + 1] - diff1[i] for i in range(3)]

print(diff2)
[15811421083594511222443737739252635264048880525922585588301936609228995378847100219077025996330637679748201495232713488175501419093889756582141854326794585932757033416010075789093873549925885865427721464977303837867972880212881542442858600772700056257381545416159374874599314719160184896130245018676138377339, 15811421083594511222443737739252635264048880525922585588301936609228995378847100219077025996330637679748201495232713488175501419093889756582141854326794585932757033416010075789093873549925885865427721464977303837867972880212881542442858600772700056257381545416159374874599314719160184896130245018676138377339, -124774146038784351164660695638844469856057176985103369924500484527626445042767085680881218533371013137755309524956122804303169360720687143840617652657883917066736244248204558778941905728536041360624781514852127873619283658133441911094549580928196987208500641692108582717297358074519511405222407995323945125710]

The third value is different from the others. It means the difference between the third and the others is the multiple of PRIME, because it should be same in mod PRIME.

The difference is:

140585567122378862387104433378097105120106057511025955512802421136855440421614185899958244529701650817503511020188836292478670779814576900422759506984678502999493277664214634568035779278461927226052502979829431711487256538346323453537408181700897043465882187108267957591896672793679696301352653014000083503049

and it’s actually a prime number. Therefore, it must be PRIME.

Now we know the value of PRIME, we can decrypt the flag.

Here is the full solver.

from Crypto.Util.number import long_to_bytes, inverse

class PRNG256(object):
    def __init__(self, seed):
        self.mask = (1 << 256) - 1
        self.seed = seed & self.mask

    def _pick(self):
        b = ((self.seed>>0)^(self.seed>>2)^(self.seed>>5)^(self.seed>>10)^1)&1
        self.seed = ((self.seed>>1)|(b<<255)) & self.mask
        return b

    def _rev(self):
        b = ((self.seed>>255)^(self.seed>>1)^(self.seed>>4)^(self.seed>>9)^1)&1
        self.seed = ((self.seed<<1)|b) & self.mask

    def rev(self):
        for i in range(256):
            self._rev()

    def rand(self):
        x = 0
        for i in range(256):
            x = (x << 1) | self._pick()
        return x

with open('output.txt', 'r') as f:
    ln = len("[+] pubkey: ")
    lines = f.readlines()
    pubkeys = eval(lines[1][ln:])
    shares = eval(lines[2][ln:])

g1 = (pubkeys[0][1] - 1) // pubkeys[0][0]
g1_rev = int(format(g1, '0256b')[::-1], 2)

prng2 = PRNG256(g1_rev)

prng2.rev()
prng2.rev()

noises = [ [ prng2.rand() for j in range(3) ] for i in range(5) ]

assert all([ noises[i][1] == (pubkeys[i][1] - 1) // pubkeys[i][0] for i in range(5) ])

msgs = []
for i in range(5):
    n = pubkeys[i][0]
    gm = shares[i][1] * pow(inverse(noises[i][2], n**2), n, n**2) % (n**2)
    assert (gm - 1) % n == 0
    m = ((gm - 1) // n) * inverse(noises[i][1], n) % n
    m -= noises[i][0]
    msgs.append(m)

diff1 = [msgs[i + 1] - msgs[i] for i in range(4)]
diff2 = [diff1[i + 1] - diff1[i] for i in range(3)]

p = diff2[1] - diff2[2] # It is prime!

a = diff2[0] * inverse(2, p) % p
b = (diff1[0] - 3 * a) % p
secret = (msgs[0] - a - b) % p
print(long_to_bytes(secret))

The flag is zer0pts{excellent_w0rk!y0u_are_a_master_0f_crypt0!!!}.

[zer0pts CTF 2020] nibelung

nc 18.179.178.246 3002
(This was all of the description)


Overview

From the organizer’s write-up(Link):

FiniteGeneralLinearGroup is just a matrix class over a finite field.
We can encrypt and decrypt arbitrary messages.
Encryption is defined as: Y = UXU^{-1}.
Decryption is defined as: X = U^{-1}YU.
The size of matrix (from the server) is 6×6.

How to solve

You cannot decrypt the flag directly, because it only accepts the matrix with values in the range 0~255.

Let’s think about (i, j)-th entry of the UXU^{-1}.

(UXU^{-1})_{i, j} = \sum_{i’ = 1}^6 \sum_{j’=1}^6 U_{i, i’} X_{i’, j’} U^{-1}_{j’, j}

If you send the matrix X which has 1 in the (i, j)-th entry and 0 in the others, you can get U_{k, i} U^{-1}_{j, l} values for every (k, l).

Notice that (UXU^{-1})_{i, j} is a linear combination of X_{i’, j’} and U_{i, i’} U^{-1}_{j’, j}. Because we now know U_{i, i’} U^{-1}_{j’, j} values, we can construct the linear system with 36 variables and 36 equations. We can solve the system with sage.

Here’s the extractor code for getting U_{i, i’} U^{-1}_{j’, j} values.

from pwn import *
import base64

f = open('data', 'w')

r = remote('13.231.224.102', 3002)
r.recvuntil('Encrypted Flag:\n')

eF = r.recvuntil('p = ')[:-5]
p = r.recvuntil('=')[:-1]

eF = eF.replace('\n', ',') + '\n'
f.write(p + eF)

for i in range(36):
    target = b'\x00' * i + b'\x01' + b'\x00' * (35 - i)
    target = base64.b64encode(target)
    r.sendline('1')
    r.sendline(target)
    r.recvuntil('Encrypted:\n')
    v = r.recvuntil('=')[:-2]
    v = v.replace('\n', ',') + '\n'
    f.write(v)

f.close()

Here’s the sage solver for the linear system.

from itertools import product as iproduct

# [i][j][i_][j_] = U[i][j] * U**-1[i_][j_]
UUm1_coef = [ [ [ [None for i in range(6)] for j in range(6) ] for k in range(6) ] for l in range(6) ]

with open('data', 'r') as f:
    p = int(f.readline()[:-1])
    eF = eval(f.readline()[:-1])

    for i, j in iproduct(range(6), repeat=2):
        mat = eval(f.readline()[:-1])
        for i_, j_ in iproduct(range(6), repeat=2):
            UUm1_coef[i_][i][j][j_] = mat[i_][j_]

mat = [ [0 for i in range(36)] for j in range(36) ]
vec = [ eF[i // 6][i % 6] for i in range(36) ]

for i, j, i_, j_ in iproduct(range(6), repeat=4):
    mat[i * 6 + j][i_ * 6 + j_] += UUm1_coef[i][i_][j_][j]
    mat[i * 6 + j][i_ * 6 + j_] %= p

R = IntegerModRing(p)
M = Matrix(R, mat)
b = vector(R, vec)
ans = M.solve_right(b)

flag = ''
for i in range(36):
    flag += chr(ans[i])
print(flag)

The flag is zer0pts{r1ng_h0m0m0rph1sm_1s_c00l}.

[zer0pts CTF 2020] diysig

I made a cipher-signature system by myself.

nc 18.179.178.246 3001

How to Solve

In the challenge, you can (1) encrypt and sign, (2) verify encrypted message, and (3) get public key. The public key of the server is always fixed.

If you see the verify function of server.py, you can find out that it will let you know the hash of the decrypted message.

        if h == H:
            sock.send(b"Signature OK!\n")
        else:
            sock.send(s2b("Invalid Signature: {:08x} != {:08x}\n".format(h, H)))

Next, let’s see the _hash function of diysig.py.

    def _hash(self, m):
        """ DIY Hash Function """
        H = 0xcafebabe
        M = m
        # Stage 1
        while M > 0:
            H = (((H << 5) + H) + (M & 0xFFFFFFFF)) & 0xFFFFFFFF
            M >>= 32
        # Stage 2
        M = H
        while M > 0:
            H = ((M & 0xFF) + (H << 6) + (H << 16) - H) & 0xFFFFFFFF
            M >>= 8
        # Stage 3
        H = H | 1 if m & 1 else H & 0xfffffffe
        return H

As you can see, Stage 3 leaks LSB of the message. This means you can apply the RSA LSB oracle attack in this challenge.

The solver is here.

from pwn import *
from Crypto.Util.number import inverse

# From the server
enc = 0x3cfa0e6ea76e899f86f9a8b50fd6e76731ca5528d59f074491ef7a6271513b2f202f4777f48a349944746e97b9e8a4521a52c86ef20e9ea354c0261ed7d73fc4ce5002c45e7b0481bb8cbe6ce1f9ef8228351dd7daa13ccc1e3febd11e8df1a99303fd2a2f789772f64cbdb847d6544393e53eee20f3076d6cdb484094ceb5c1
n = 0x6d70b5a586fcc4135f0c590e470c8d6758ce47ce88263ff4d4cf49163457c71e944e9da2b20c2ccb0936360f12c07df7e7e80cd1f38f2c449aad8adaa5c6e3d51f15878f456ceee4f61547302960d9d6a5bdfad136ed0eb7691358d36ae93aeb300c260e512faefe5cc0f41c546b959082b4714f05339621b225608da849c30f
e = 0x10001

div = pow(inverse(2, n), e, n)

flag = ''
m, sur = 0, 0
for i in range(1024):
    r = remote('18.179.178.246', 3001)
    r.recvuntil('> ')
    r.sendline('2')
    r.recvuntil('ENC : ')
    r.sendline(hex(enc)[2:])
    r.recvuntil('SIG : ')
    r.sendline('00000000')
    r.recvuntil('!= ')
    sig = r.recv(8)
    r.close()

    sig = int(sig, 16) % 2
    if sig % 2 == 0:
        m |= (sur % 2) << (i % 8)
        sur = (sur + (sur % 2)) // 2
    else:
        m |= (1 - sur % 2) << (i % 8)
        sur = (sur + (1 - sur % 2) + n) // 2
    enc = enc * div % n

    if i % 8 == 7:
        if m == 0:
            break
        flag += chr(m)
        m = 0

print(flag[::-1])

The flag is zer0pts{n3v3r_r3v34l_7h3_LSB}.

« Older posts

© 2020 RBTree.insert()

Theme by Anders NorenUp ↑