Em tên là Thái Vĩnh Đạt (tên discord là tvdat20004), hôm 22/7 vừa rồi em có tham gia miniCTF do clb Wanna.W1n tổ chức và em được hạng 3. Em làm được 2 bài thuộc mảng Crypto và sau đây em xin trình bày write-up cho 2 bài đó ạ.

flipping login

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

#!/usr/bin/env python3 import json import os import signal from base64 import b64decode, b64encode from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from secret import flag key = os.urandom(32) def menu_choice() -> str: print( """Valid choices: 1. Login 2. Register 3. Quit""" ) return input(">> ").strip() def handler(_signum, _frame): print("Time out!") exit(0) def encrypt(msg): iv = os.urandom(16) cipher = AES.new(key, AES.MODE_CBC, iv=iv) return iv+cipher.encrypt(pad(msg, 16)) def decrypt(iv, msg): cipher = AES.new(key, AES.MODE_CBC, iv=iv) return unpad(cipher.decrypt(msg), 16) def login(): enc_token = b64decode(input("Login token: ").strip()) token = decrypt(enc_token[:16], enc_token[16:]) token = json.loads(token) return token['name'], token['admin'] def register(): name = input("Nickname: ").strip() token = { "admin": False, "name": name } token = json.dumps(token).encode() enc_token = b64encode(encrypt(token)).decode() print("Here is your login token: " + enc_token) def main(): signal.signal(signal.SIGALRM, handler) signal.alarm(60) name = 'N.A' is_admin = False print("Welcome to my under-development program. Who are you?") while True: choice = menu_choice() match choice: case '1': name, is_admin = login() break case '2': register() print('') case '3': print("Bye bye!") break case _: print("??????????????") raise RuntimeError("I found a hacker") if is_admin: print(f"Hello {name}! Here is your secret: " + flag) else: print(f"Hello {name}! Sorry, only admin can read my secret!") try: main() except Exception as E: print(str(E))
  • Tóm tắt đoạn code:
    • Server sẽ cho ta 3 lựa chọn: Login(1), Register(2), Quit(3).
    • Khi chọn Register, server cho phép ta nhập name, sau đó nó sẽ tạo một login_token. login_token chính là base64 của kết quả sau khi mã hóa AES_CBC của một json:{ "admin": False, "name": name }
    • Tùy chọn login cho phép ta nhập vào một login_token. Server sẽ encode base64, sau đó decrypt AES_CBC và loads json token. Để nhận được flag thì token phải có key admin ứng với giá trị True.
  • Vậy điểu mình cần làm để lấy được flag là phải gởi token như yêu cầu ở trên. Với tên đề bài là Flipping login cộng với việc đã làm bài flipping cookie trên Cryptohack thì em nghĩ ngay đến kĩ thuật bit flipping attack. Trong quá trình làm bài em có tham khảo ở link này
  • Nhìn chung thì kĩ thuật này cho phép ta modify một vài byte trên chuỗi ciphertext (thực ra là iv + ciphertext) để thu được ciphertext mới và sau khi decrypt ta thu được plaintext như mong muốn. Ở đây mình cần modify sao cho từ plaintext là { "admin": false, "name": name } thành plaintext { "admin": true, "name": name }, tức là chuyển chuỗi false thành true. Mình sẽ tiến hành flip từng byte như sau:
    • Ví dụ để chuyển 'f' thành 't', em sẽ xor bit thứ 10 (vị trí của 'f' trong plaintext) với (ord(f)^ord(t)).
    • Lặp lại quá trình đồi với các bit tiếp theo cho đến khi flip hết chuỗi 'false' thành 'true '.
  • Python implementation:
import base64 import json from pwn import * r = remote('45.122.249.68', 20022) def login(token : str): data = r.recvuntil(b'>> ') r.sendline(b'1') data = r.recvuntil(b'Login token: ') r.sendline(token.encode()) flag = r.recvline() print(flag) def register() -> str: data = r.recvuntil(b'>> ') r.sendline(b'2') data = r.recvuntil(b'Nickname: ') r.sendline(b'tvdat20004') data = r.recvuntil(b'token: ') token = r.recvuntil(b'\n').decode().strip() return token def flipping_bits(ciphertext, pos, plaintext : bytes, new_plaintext : bytes): assert len(plaintext) == len(new_plaintext) ciphertext = list(ciphertext) for i in range(pos, pos + len(plaintext)): ciphertext[i] = ciphertext[i] ^ (plaintext[i-pos] ^ new_plaintext[i-pos]) iv = ciphertext[:16] ct = ciphertext[16:] return base64.b64encode(bytes(iv)+bytes(ct)) token = base64.b64decode(register()) ans = flipping_bits(token,10,b" true",b"false") login(ans.decode()) r.interactive()

  • Flag: W1{CBC_bit_flipping_attack_can_flip_your_system_https://tsublogs.wordpress.com/2015/07/18/bit-flipping-attack-on-cbc-mode}

Weird Primes

  • chall.py, output.txt
  • Hint: 'Bruteforcing each digit since number of possible bytes for each prime is small'.
  • Sau khi đọc xong hint và một hồi suy nghĩ, em cảm thấy nó "khá giống" với parital bit leak dạng "random known bit of p and q" mà em đã làm trước đây (chi tiết về nó em để ở đây, 4.3.1, trang 21). Tuy thoạt nhìn thì thấy 2 bài có vẻ rất khác, nhưng ý tưởng brute-force lại hoàn toàn giống nhau, đều sử dụng "branch and prune algorithm".
  • Cụ thể, em sẽ brute-force từng byte(do p và q được tạo từ chuỗi byte gồm 128 số) của p và q theo thứ tự từ LSB đến MSB.
  • Mô tả thuật toán:
    • Xét cặp LSB của p và q, có tất cả 10x10 cặp có thể có. Bruteforce hết không gian mẫu đó cho đến khi gặp p.q mod 256 = n mod 256 (p,q đang chỉ có 1 byte LSB)
    • Xét tiếp byte sau đó, gọi là dp và dq, cũng có 10x10 cặp có thể có. Bruteforce hết không gian mẫu đó cho đến khi gặp p.q mod (256**2)= n mod (256**2)(khi này p,q có 2 byte LSB)
    • Lặp lại quá trình cho đến khi tìm được 128 byte của p và q.
  • Python implementation:
n = 1193924133214231889546351753346545092386694958282484590742357919398336185127955297065849705214714735049853117873865465248652047796382636343398101815207127117475834303652582611557916482763041203527984476566898846821511189884575355212170099724076171155888215024629846800791116844007098877699954543318380357268011069937195402719153407503698044969734971221799699731859308452701292314029495280470102667890355711974564903038932606840517852702768264359240118381178825407856623508271302891317946429909210917646516863004718071228523457611307993744722272634542518670009533519107837470331125580871788354030347986972475782995433 e = 65537 enc_flag = '0255ad928673b69d2c1998cafaadda7028d339dd82483c9d01363231a01def067bcea9d2cc378e2b9054feadf31412b1f52e9b9061fc1ba8863cfffff840979e3e44743409e3d5a19ab2bcb2f570776b028ea5f5422962d103e2ea2e59c9c479e5a46c514e2196dd7d3f1c30d588879ef029e0fd41d278c6715998ed746817a29f9d09df775368c038cd616b1d17b0bb318fa4501a6375a98497ceda9a827e0f25a55790bb0a259305dc696e140a6f14a901148113ca5ae3a78195d2d06005a036a19478e7cf1d30d38791479169092745bbfc325d73c162e602e4918dab99221f2f64a50c4b349124d7ca8fb56d8358614f20dd2d445debfe711a6d3b441bb5' from Crypto.Util.number import * from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA p = '' q = '' for i in range(128): sign = False for dp in '0123456789': for dq in '0123456789': tmp_p = dp + p tmp_q = dq + q if bytes_to_long(tmp_p.encode()) * bytes_to_long(tmp_q.encode()) % pow(256,i+1) == n % pow(256,i+1): p = tmp_p q = tmp_q sign = True break if sign: break print(p) print(q) p = bytes_to_long(p.encode()) q = bytes_to_long(q.encode()) assert isPrime(p) and isPrime(q) and p*q == n d = pow(e,-1,(p-1)*(q-1)) key = RSA.construct((n, e, d, p, q)) cipher = PKCS1_OAEP.new(key) flag = cipher.decrypt(bytes.fromhex(enc_flag)) print(flag)

  • Flag: W1{branch_and_prune_is_sometime_very_useful!!!!}