이 시국에 DEF CON CTF 가기

정말 오랜만에 블로그 글을 쓰네요. 이번에 DEF CON CTF를 위해 라스 베가스에 다녀오게 되었습니다. 라스 베가스를 다녀오면서 어떤 일들이 있었는지 기록으로 남겨두고자 글을 작성하게 되었습니다.

DEF CON CTF 본선 진출하기

우선 DEF CON CTF 본선을 가기 위해서는 두 가지 방법이 있습니다. 하나는 DEF CON CTF의 Pre-qualifier로 지정된 대회들 중 하나에서 1등을 차지하는 것이고, 하나는 예선 대회에서 일정 등수 이상 차지하는 것입니다.

저희 팀, Perfect ⚔ Guesser는 팀 perfect blue와 팀 Super Guesser의 연합으로 탄생한 팀입니다. (저는 양쪽 모두의 멤버이구요.) Pre-qualifier로 지정된 대회는 다음 4개가 있었습니다.

  • HITCON CTF 2020
  • hxp CTF 2020
  • PlaidCTF 2021
  • Pwn2Win CTF 2021

작년에는 HITCON CTF 2020에서 Perfect ⚔ Guesser로 1등을 차지했고, PlaidCTF 2021에서는 perfect blue로 1등을 차지해 본선에 성공적으로 진출할 수 있었습니다. 덕분에 예선 대회를 굳이 치루지 않고 편안하게 준비하는 것이 가능했습니다.

라스 베가스로 갈 준비

DEF CON CTF를 위한 대비에 저는 크게 참여하지는 못했습니다. 패킷 분석, 자동 공격 등의 툴링에서 수고해준 팀원 분들에게 감사의 말씀을 드립니다.

아시다시피 이 시국에는 해외로 가는 것이 쉽지 않습니다. 미국에 가기 위해서는 다음과 같은 준비가 필요했습니다.

  • 미국으로 출국 전 72시간 내에 받은 COVID-19 테스트 결과 (백신 상관 없음)
  • ESTA

… 놀랍게도 이것이 전부입니다! 미국 내에서는 자가 격리가 권장사항일 뿐 필수가 아니고, 여전히 VISA 없이 ESTA를 통해서 여행을 하는 것이 가능합니다. 다만 개인 목적으로 검사를 받는 경우 비용이 꽤 비쌉니다. 그 중에서도 인천공항 검사소가 제일 싼 것으로 보였구요. 적어도 대전에서 찾아봤는데 대체로 20만원을 넘어갔었습니다.

다행히도 인천공항 검사소에 당일 오전 7시로 예약이 가능했고, 당일날 음성 판정을 받고 무사 출국할 수 있었습니다.

가는 길

비행기 표는 Delta로 끊었는데요, 대체로 라스 베가스를 갈 때는 제일 싸기 때문에 델타를 끊게 되는 것 같습니다. 가는 길 오는 길 모두 시애틀을 경유했습니다.

경유하는 표를 끊을 때 주의할 점이 있다면 갈 때는 최소한 환승 시간을 2~3시간 정도로 넉넉하게 잡아야 한다는 것입니다. 입국 심사를 거치게 되고, 국제선에서 국내선으로 갈아타면서 짐을 다시 수속시켜야 하기 때문입니다. 이 과정에서 어떤 일이 벌어질지 모르기 때문에 넉넉하게 잡는 것을 추천합니다. (저는 과거에 입국 심사에서 secondary inspection room로 불려본 적이 있습니다. 이 경우 1~2시간을 더 기다리게 될 수도 있습니다.)

가는 길에서 계속 옆자리에 자는 걸 방해하는 사람이 있어서 꽤 괴로웠습니다. 저는 오른쪽의 통로 열이었고, 그 사람은 오른쪽 창문 옆이었는데, 계속 팔로 툭툭 치거나, 창문을 열었다 닫았거나 하는 사람이었습니다. 말을 해도 듣지 않더라고요. 덕분에 거의 잠을 설친 상태로 라스 베가스에 도착했습니다.

재밌었던 점은 그 사람이 secondary room으로 불려가더군요. 쌤통입니다.

라스 베가스에서

라스 베가스에는 perfect blue 팀 사람들만 모였습니다. 이 시국에 해외에서 미국으로 오는 게 쉽지 않긴 하죠… 우선 팀이 hackerone에서 스폰을 받아서, hackerone에서 플라밍고 호텔에 스위트 룸을 잡아줬습니다. 주로 대회는 스위트 룸에서 이뤄졌습니다.

멋진 스위트 룸 감사합니다, hackerone!

대회 전

대회 전에는 Paris의 상층에 있는 레스토랑에서 팀 식사를 가졌습니다. 매우 맛있는 식사였습니다. 이 모든 것이 팀이 다른 대회에서 받은 상금으로 이뤄졌다는 놀라운 사실! 식사 후 영수증을 보니 대충 1300달러 정도 찍혀있었던 것은 안 비밀입니다.

그리고 팀 사진도 찍었습니다.

Paris의 레스토랑에서 팀 사진

맨 왼쪽이 저인데, 비행기에서 잠을 못 자서 상당히 초췌한 모습입니다.

대회 중

대회 중에는 잠을 못 잔 여파와 시차 때문에 제대로 집중하지 못했습니다. 또 대회 중에는 귀국하기 위해서 COVID-19 검사를 받아야 했습니다. 그나마 ooopf, ooows-p92021, shooow-your-shell에서 매우 조그마한 성과를 얻을 수 있었고, ooows-broadcooom은 풀려고 시도했지만 졸려서 제대로 패치하지는 못했습니다. 아마 한국에 있었다면 풀로 집중할 수 있었을지도 모르겠습니다.

특히 shooow-your-shell은 인력이 많이 필요하다고 생각해서 급하게 PLUS 친구들을 불렀는데, 많은 PLUS 친구들이 늦은 시간에도 도와줘서 어느 정도 대비를 할 수 있었습니다.

대회 도중의 스위트 룸 모습
아침을 먹으며 대회로 고통받을 준비를 하는 모습

분명 수많은 사람이 채팅에 있었음에도 불구하고 많은 사람들이 대회를 같이 뛰지 않았습니다. 이 부분에 대해서 불만이 많았어서, 한국에 와서 대략적으로 열심히 뛴 정도를 정리해보니 다음과 같았습니다.

대충 기여도

하지만 perfect blue 팀원들이 패킷 분석과 자동 공격에서 많은 힘을 써주어서, 무사히 6등으로 CTF를 마칠 수 있었습니다.

이로부터 얻은 교훈은 역시 잠을 일찍 자야한다는 것과, 좋은 팀원들과 대회를 뛰어야 한다는 것입니다.

대회 후

대회가 끝난 후 결과가 발표되고, 6등인 것을 확인했습니다. 그 뒤 hackerone으로부터 사진을 찍는 시간이 있었습니다.

꽤 나쁘지 않게 나왔습니다. 제 살은 좀 빼야할 거 같네요.

그 뒤 0rganizers에서 맡은 after party에 참가했습니다. 많은 사람들과 만날 수 있어서 즐거운 시간이었습니다.

주로 서로 어떤 팀에서 왔는지, 대회 문제가 어땠는지, 각자 CTF는 어떻게 시작하게 되었는지 등등을 이야기하는 좋은 자리였습니다.

대충 after party에서 따봉하는 사진

귀국 후

다행히 귀국하는 길에는 잠을 방해하는 사람이 없었습니다. 무사히 귀국한 후, 다음과 같은 과정을 거쳤습니다.

  1. 검역 과정에서 미국에서 받은 코로나 검사지 제출
  2. 자가격리자를 위한 어플리케이션 설치
  3. 해외입국자 전용 리무진/KTX/버스를 타고 귀가, 자가격리 시작
  4. 입국 다음날 보건소에서 코로나 검사, 자가격리자용 물품 수령

특히 리무진과 KTX는 무료가 아니었지만 따로 더 비싼 비용을 내지 않았고, 대전역에서 귀가하는 버스의 경우 무료로 제공되었습니다. 저의 경우 개인적인 목적을 위해서 출국한 것인데도 불구하고 이런 데서 국가가 비용을 부담한다는 점에서 다소 미안함을 느꼈습니다.

정리하며

perfect blue 친구들을 만날 수 있어 즐거운 시간이었고, 오랜만에 사람들과 모여 CTF를 할 수 있었단 점에서 재미를 느꼈습니다.

또 이 시국에 라스 베가스를 가는 것을 허락해주신 김용대 교수님과 윤인수 교수님께 감사의 말씀을 드립니다.

이제 자가 격리 기간 동안 오직 연구 뿐입니다. 연구 파이팅~~~

2020 회고

대학원

대학원에 들어갔습니다. 좋은 랩에는 들어갔지만 좋은 주제를 찾지 못해서 초반에 많이 방황했었습니다. 다행히도 좋은 랩에 들어간 덕에 교수님과 랩의 여러 사람들이 케어를 많이 해주셨고 현재는 좋은 주제로 연구를 진행하고 있습니다.

그런데도 해결되지 않는 것이 있다면 무기력증이나 귀찮음 같습니다. 연구를 진행하다보면 특정 포인트에서 더 할 의욕이 안 나는 경우가 잦은데, 비단 이것을 ‘내가 연구 주제에 흥미가 없는 것이 아닐까?’ 하고 생각하다보면 그건 또 아니기에 참 아리송한 일이 아닐 수 없습니다.

내년 목표는 ‘꼭 연구를 완성시키자’입니다. 아마 내년 3월 전에는 눈에 보이는 무언가를 만들 수 있을 거라고 생각합니다.

우울증

작년부터인가 재작년부터인가 겪고는 있었다고 생각했지만 올해가 정말 증상이 심해졌습니다. 불면증과 무기력증, 만성 피로까지 겹쳐져서 상당히 고생을 많이 했고 결국 정신과에 가서 치료와 상담을 병행하게 되었습니다. 올해 초에 새 환경에서 적응하느라 좀 심해진 것이 아닌가 싶기도 합니다.

다행히도 현재는 약을 먹으면서 호전이 되고 있지만, 약을 가끔 빼먹으면 불면증에 잠을 제대로 못 자고 있기에 나으려면 갈 길이 먼 것 같습니다.

CTF

포항공대를 졸업하면서 자연스럽게 포항공대 보안 동아리인 PLUS 현역이 아니게 되었습니다. 그래도 종종 PLUS와 같이 CTF를 뛰긴 했지만, 개인적으로 만족스럽게 CTF를 뛰지는 못했습니다. koreanbadass로 DEF CON CTF Finalist가 되기는 했지만 역시 만족스러운 결과는 아니었다고 생각하고요.

그 이후로 여러 대회를 거치면서 한국에서 CTF 팀을 찾는 것에 염증을 느끼게 되었고, 여러모로 징징 글도 썼습니다.

해당 트윗 이후 먼저 Super Guesser 의 sqrtrev님으로부터 연락이 와서 시험삼아 같이 CTF를 뛰어보았는데 만족스럽게 뛸 수 있었고, 그 후 perfect blue 의 knapstack님으로부터도 연락을 받아 2개의 팀에 들어가게 되었습니다.

그 결과 멋진 팀원들도 만나고, HITCON CTF 1등을 거머쥐면서 DEF CON CTF 시드도 획득할 수 있었습니다. 개인적으로 HITCON CTF 1등은 지금까지 CTF를 하면서 가장 큰 성과라고 생각합니다.

아, 올해 사이버작전경연대회와 Bingo CTF, pbctf에서도 문제를 출제했었습니다. 문제 출제는 처음이었는데 좋은 경험이었다고 생각합니다. 내년도 출제할 수 있었으면 좋겠네요.

그 외

올해 rkm0959님을 만나서 같이 암호학과 관련된 활동을 많이 할 수 있었습니다. 바킹독님과 함께 NSUCRYPTO도 나가서 은상도 타고, Super Guesser에서 같이 암호와 관련된 문제들을 풀 수 있어서 정말 다행이라고 생각합니다.

우울증과 관련해서 저에게 도움을 준 사람들이 많이 있습니다. 우울증과 관련해서 상담해준 승엽이형에게 고맙다는 말 전하고 싶고, 자주 말 걸어준 모두에게 감사하다는 말씀을 드립니다.

요즘 들어 게임에 크게 흥미가 떨어진 상태였는데 최근에는 마이마이를 다시 해보고 있습니다.

PT를 받고 있습니다. 운동 정말 힘든 거 같습니다. 제 저질 체력에 눈물이 납니다.

내년에는 좀 더 나은 사람이 될 수 있었으면 좋겠습니다. 좋아하는 공부를 좀 더 열심히 할 수 있었으면 좋겠다고 생각합니다. 내년의 목표는 여러가지가 있습니다.

  • 연구 열심히 하기
  • 운동 평균만큼은 할 수 있게 되기
  • 일본어 공부
  • 위스키 공부
  • 암호학 공부 (Elliptic Curves: Number Theory and Cryptography 취미 삼아 읽기?)
  • 리버싱 공부 (어떤 스택을 더 쌓을 수 있을지…)
  • 포너블 공부?
  • 일본 여행 가기 (가능하면 ㅠ)
  • 코로나 끝나가면 사람들 좀 만나기

다 이룰 수 있었으면 좋겠네요. 그러기에는 시간이 별로 없는 거 같은데…

서버 이전

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

여러가지 사유가 있지만, 가장 큰 이유는 무지한 상태에서 서버를 사용한 나머지 다양한 업보가 쌓였다는 것입니다. (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!!!}.

« Older posts

© 2021 RBTree.insert()

Theme by Anders NorenUp ↑