<h1 style="text-align:center; color:#cb42f5"> PicoCTF Walkthrough: Part 1 </h1> Đây là lời giải cho một số thử thách của **picoCTF** mà mình cảm thấy thú vị và hữu ích. Hầu hết trong số chúng sẽ liên quan đến việc viết chương trình. ## Pixelated ![image](https://hackmd.io/_uploads/SJ1VxyU5gg.png) Thử thách này giống một thử thách của forensics hơn là thử thách cryptography Về cơ bản, chúng ta cần phải kết hợp hai hình ảnh được cho để khôi phục lại hình ảnh gốc. Dưới đây sẽ là script lời giải được tạo ra dưới sự trợ giúp của LLM (**chatGPT**) :::spoiler <b style="color:tomato">combine_images.py</b> ```python=1 from PIL import Image def combine_images(image1_path, image2_path, output_path): # Open the images image1 = Image.open(image1_path) image2 = Image.open(image2_path) # Ensure both images have the same size if image1.size != image2.size: raise ValueError("Images must have the same size") # Combine the images using a simple averaging technique combined_image = Image.blend(image1, image2, alpha=0.5) # Save the combined image combined_image.save(output_path) if __name__ == "__main__": image1_path = "scrambled1.png" image2_path = "scrambled2.png" output_path = "combined_image.png" combine_images(image1_path, image2_path, output_path) ``` ::: Và chúng ta đã có được một hình ảnh xám. ![image](https://hackmd.io/_uploads/BJleGk8cxl.png) Tất cả những gì chúng ta cần làm bây giờ là tải bức ảnh lên <a href="https://29a.ch/photo-forensics/#forensic-magnifier">website này</a> và thử các lựa chọn cho đến khi ta nhìn rõ được flag trong bức ảnh thôi. :::success **Flag:** <b style="color:#3262a8">picoCTF{d562333d}</b> ::: ## Easy Peasy ![image](https://hackmd.io/_uploads/rk41MlL5ee.png) :::spoiler <b>otp.py</b> ```python= #!/usr/bin/python3 -u import os.path KEY_FILE = "key" KEY_LEN = 50000 FLAG_FILE = "flag" def startup(key_location): flag = open(FLAG_FILE).read() kf = open(KEY_FILE, "rb").read() start = key_location stop = key_location + len(flag) key = kf[start:stop] key_location = stop result = list(map(lambda p, k: "{:02x}".format(ord(p) ^ k), flag, key)) print("This is the encrypted flag!\n{}\n".format("".join(result))) return key_location def encrypt(key_location): ui = input("What data would you like to encrypt? ").rstrip() if len(ui) == 0 or len(ui) > KEY_LEN: return -1 start = key_location stop = key_location + len(ui) kf = open(KEY_FILE, "rb").read() if stop >= KEY_LEN: stop = stop % KEY_LEN key = kf[start:] + kf[:stop] else: key = kf[start:stop] key_location = stop result = list(map(lambda p, k: "{:02x}".format(ord(p) ^ k), ui, key)) print("Here ya go!\n{}\n".format("".join(result))) return key_location print("******************Welcome to our OTP implementation!******************") c = startup(0) while c >= 0: c = encrypt(c) ``` ::: Đây là một thử thách được viết vào sau thời kỳ hồng hoang một chút nên nhìn cách viết đề nó hơi lạ. Bạn sẽ được gửi dữ liệu có độ dài không được vượt quá độ dài khóa trong mỗi lần gửi, và chú ý đoạn mã này của hàm **encrypt** như sau: ```python= if stop >= KEY_LEN: stop = stop % KEY_LEN key = kf[start:] + kf[:stop] else: key = kf[start:stop] key_location = stop ``` Có nghĩa là nếu tổng độ dài trong các lần gửi mà vượt quá độ dài khóa thì ta sẽ quay lại đầu khóa và tiếp tục mã hóa. Như vậy thì ta có thể tận dụng điều này để khôi phục lại đoạn khóa dùng để mã hóa flag. Dưới đây là script cho bài này: :::spoiler <b style="color:tomato">solution.py</b> ```python= from pwn import * from Crypto.Util.strxor import strxor KEY_LEN = 50000 HOST = "mercury.picoctf.net"; PORT = 58913 connection = remote(HOST, PORT) connection.recvuntil(b'This is the encrypted flag!\n') encrypted_flag = connection.recvline().strip().decode() encrypted_flag = bytes.fromhex(encrypted_flag) length = len(encrypted_flag) connection.recvuntil(b"What data would you like to encrypt? ") data_send = b"0"*(KEY_LEN - length) connection.sendline(data_send) connection.recvuntil(b"What data would you like to encrypt? ") data_send = b"0"*length connection.sendline(data_send) connection.recvuntil(b'Here ya go!\n') encrypted_data = connection.recvline().strip().decode() encrypted_data = bytes.fromhex(encrypted_data) connection.close() data_send = data_send[-length:] key = strxor(encrypted_data, data_send) inside_flag = strxor(encrypted_flag, key).decode() flag = "picoCTF{" + inside_flag + '}' print("[!] Got flag:", flag) # [!] Got flag: picoCTF{35ecb423b3b43472c35cc2f41011c6d2} ``` ::: :::success **Flag:** <b style="color:#3262a8">picoCTF{35ecb423b3b43472c35cc2f41011c6d2}</b> ::: >[!Note] Nói thêm về bài này >Ban đầu mình gửi thẳng 50000 ký tự rồi lấy 64 ký tự cuối để khôi phục flag, nhưng không hiểu vì một lý do nào đó mà lại ra sai đáp án nên mình mới thả dislike bài này. :-1: ## No Padding, No Problem ![image](https://hackmd.io/_uploads/By9CxZLcel.png) Bài này nhìn qua là ta có thể thấy đây là tấn công liên quan đến **Oracle** của RSA. Chúng ta sẽ được cho khóa công khai $(n, e, c)$ và server sẽ tiến hành giải mã theo yêu cầu của chúng ta. Tuy nhiên nếu chúng ta nhập vào $\text{ciphertext}$ mà đề cho để giải mã thì server sẽ từ chối giải mã cho mình. Vì vậy chúng ta phải tìm cách để đánh lừa server, ta có cách như sau: - Chọn một số nguyên ngẫu nhiên $a \in (0 ,n)$ sao cho $a\cdot m < n$. - Tính $\text{new_cipher} = a^{e} \cdot c ~~ mod ~ n$. - Gửi $\text{new_cipher}$ cho server để giải mã, ta thu về được $\text{plaintext}$. - Ta tìm được $\text{flag} = \frac{\text{plaintext}}{a}$. Việc chứng minh điều này sẽ được coi như là một bài tập dành cho những người đọc bài viết này. :penguin: Dưới đây sẽ là script lời giải: :::spoiler <b style="color:tomato">solution.py</b> ```python= from pwn import * from Crypto.Util.number import long_to_bytes HOST = "mercury.picoctf.net"; PORT = 33780 connection = remote(HOST, PORT) connection.recvuntil(b"n: ") n = int(connection.recvline().strip().decode()) connection.recvuntil(b"e: ") e = int(connection.recvline().strip().decode()) connection.recvuntil(b"ciphertext: ") ciphertext = int(connection.recvline().strip().decode()) new_cipher = (pow(2, e, n)*ciphertext) % n connection.recvuntil(b"Give me ciphertext to decrypt: ") connection.sendline(f"{new_cipher}".encode()) connection.recvuntil(b"Here you go: ") plaintext = int(connection.recvline().strip().decode()) int_flag = plaintext//2 flag = long_to_bytes(int_flag).decode() connection.success(f"Got flag: {flag}") # [+] Got flag: picoCTF{m4yb3_Th0se_m3s54g3s_4r3_difurrent_0801973} connection.close() ``` ::: :::success **Flag:** <b style="color:#3262a8">picoCTF{m4yb3_Th0se_m3s54g3s_4r3_difurrent_0801973}</b> ::: ## New Caesar ![image](https://hackmd.io/_uploads/SyObspI5xl.png) :::spoiler <b>new_caesar.py</b> ```python= import string LOWERCASE_OFFSET = ord("a") ALPHABET = string.ascii_lowercase[:16] def b16_encode(plain): enc = "" for c in plain: binary = "{0:08b}".format(ord(c)) enc += ALPHABET[int(binary[:4], 2)] enc += ALPHABET[int(binary[4:], 2)] return enc def shift(c, k): t1 = ord(c) - LOWERCASE_OFFSET t2 = ord(k) - LOWERCASE_OFFSET return ALPHABET[(t1 + t2) % len(ALPHABET)] flag = "redacted" key = "redacted" assert all([k in ALPHABET for k in key]) assert len(key) == 1 b16 = b16_encode(flag) enc = "" for i, c in enumerate(b16): enc += shift(c, key[i % len(key)]) print(enc) ``` ::: Ta để ý hai dòng này: ```python= assert all([k in ALPHABET for k in key]) assert len(key) == 1 ``` Điều này chứng tỏ khóa chỉ có 16 giá trị có thể có, và trên hết hàm mã hóa đề bài có thể đảo ngược nên ta chỉ cần viết hàm giải mã và thử hết không gian khóa là ra. :::spoiler <b style="color:tomato">solution.py</b> ```python= import string LOWERCASE_OFFSET = ord("a") ALPHABET = string.ascii_lowercase[:16] def decrypt_message(encryted, key): b16_flag = "" t2 = ord(key) - LOWERCASE_OFFSET for char in encryted: index = ALPHABET.index(char) t1_1 = index - t2 t1_2 = index + 16 - t2 if(t1_1 < 0): t1 = t1_2 else: t1 = t1_1 b16_flag += chr(t1 + LOWERCASE_OFFSET) flag = "" for i in range(0, len(b16_flag), 2): left_half = ALPHABET.index(b16_flag[i]) right_half = ALPHABET.index(b16_flag[i+1]) binary = format(left_half, "04b") + format(right_half, "04b") flag += chr(int(binary, 2)) return flag encrypted_flag = "lkmjkemjmkiekeijiiigljlhilihliikiliginliljimiklligljiflhiniiiniiihlhilimlhijil" for key in ALPHABET: print(decrypt_message(encrypted_flag, key)) # ºÉ¤Éʤ¹·¸¸¹»¹··· # ©¸¸¹sxwu¨¦zv§yzu|§¨{yªu¨t¦|w|wv¦z{¦xz # §§¨bgfdiehidkjhdckfkfeijgi # qQqVUSXTWXSZYWSRZUZUTXYVX # v`@`EDBusGCtFGBItuHFwBuAsIDIDCsGHsEG # et_tu?_431db62c5618cd75f1d0b83832b67b46 # TcNcd.N#" SQ%!R$% 'RS&$U S/Q'"'"!Q%&Q#% # CR=RS=B@AABDB@@@ # 2A,AB # ???00131 # !01ûÿý .òþ/ñòýô/ óñ"ý ü.ôÿôÿþ.òó.ðò # / # / ê # ïîìáíàáìãâàìëãîãîíáâïá # ùÙùÞÝÛ # ÑßÛÚ # ÒÝÒÝÜ # ÐÑ # ÞÐ # ÈèÍÌÊýûÏËüÎÏÊÁüýÀÎÿÊýÉûÁÌÁÌËûÏÀûÍÏ # íü×üý·×¼»¹ì꾺뽾¹°ë쿽î¹ì¸ê°»°»ºê¾¿ê¼¾ # ÜëÆëì¦Æ«ª¨ÛÙ­©Ú¬­¨¯ÚÛ®¬Ý¨Û§Ù¯ª¯ª©Ù­®Ù«­ # ËÚµÚÛµÊÈÉÉÊÌÊÈÈÈ ``` ::: ![image](https://hackmd.io/_uploads/r1SX2aUcex.png) Vậy là ta đã có 16 flag tiềm năng, 1 trong số chúng chắc chắn là flag (cụ thể là `et_tu?_431db62c5618cd75f1d0b83832b67b46` vì nhìn nó khả nghi vãi). :::success **Flag:** <b style="color:#3262a8">picoCTF{et_tu?_431db62c5618cd75f1d0b83832b67b46}</b> ::: >[!Tip] >Hàm format trong python có vẻ tiện lợi khi chuyển về nhị phân so với hàm bin vì mình có thể tùy chọn độ dài của số ở dạng nhị phân. ## spelling-quiz ![image](https://hackmd.io/_uploads/BkkYHRUqlx.png) :::spoiler <b>encrypt.py</b> ```python3= import random import os files = [ os.path.join(path, file) for path, dirs, files in os.walk('.') for file in files if file.split('.')[-1] == 'txt' ] alphabet = list('abcdefghijklmnopqrstuvwxyz') random.shuffle(shuffled := alphabet[:]) dictionary = dict(zip(alphabet, shuffled)) for filename in files: text = open(filename, 'r').read() encrypted = ''.join([ dictionary[c] if c in dictionary else c for c in text ]) open(filename, 'w').write(encrypted) ``` ::: :::spoiler <b>flag.txt</b> ```tex! brcfxba_vfr_mid_hosbrm_iprc_exa_hoav_vwcrm ``` ::: :::spoiler <b>study-guide.txt</b> ```tex! gocnfwnwtr sxlyrxaic dcrrtfrxcv uxbvwavcq lwvicwtiwm pwtmwnxvicq avingciisa ylwtmrcawx mwaxdcrrxuwlwvq yciflwnf mwaxsrtwvq iovabxcabqwtd bcrwtnlwtxvwit srlxtwkwtd bcriurmwrtv nflicxlyicsxswmr titrlrnvcilqvwn xanrcvxwtxulr vficxniblxavwra bwttxnlrv bxbrcerwdfva wtnxctxvwit titborcwlwvq otbcrywtrm ucxaalwgr vcxtabiawvwpr dlqnrcil wmilxvcwkrc fqbrcixcvwx brcwsrmollxcq crtmwvwit sitavcwnwmr rzvcxabrnvcxl sitosrtvxllq nfilrfrsxvwt iprccrxlwas mwttrclraa nxcbrllos uxccolrr rzvciprcvrmtraa trnraaxc rpwtnwtd brcabrnvwpwas blxasilqkxuwlwvq ... (dài quá nên mình không tiện viết ở đây) ``` ::: Nhìn phức tạp vậy thôi chứ bài này chỉ cần lên một website có tên <a href="https://quipqiup.com/">quipqiup</a> để giải mã là ra. Ta sẽ nhập đoạn mã trong file `flag.txt` ở trên cùng và nhập các từ trong file `study-guide.txt` ở bên dưới (nhớ chỉ cần nhập vài chục từ là đủ rồi, nhập quá nhiều sẽ khiến cho website bị đơ). ![image](https://hackmd.io/_uploads/BJ6kw08cxe.png) Sau khi nhập và chạy thì ta sẽ thu được các kết quả bên dưới: ![image](https://hackmd.io/_uploads/SJzODCLcle.png) :::success **Flag:** <b style="color:#3262a8">picoCTF{perhaps_the_dog_jumped_over_was_just_tired}</b> ::: ## Custom encryption ![image](https://hackmd.io/_uploads/ByCRkkDqxe.png) :::spoiler <b>custom_encryption.py</b> ```python= from random import randint import sys def generator(g, x, p): return pow(g, x) % p def encrypt(plaintext, key): cipher = [] for char in plaintext: cipher.append(((ord(char) * key*311))) return cipher def is_prime(p): v = 0 for i in range(2, p + 1): if p % i == 0: v = v + 1 if v > 1: return False else: return True def dynamic_xor_encrypt(plaintext, text_key): cipher_text = "" key_length = len(text_key) for i, char in enumerate(plaintext[::-1]): key_char = text_key[i % key_length] encrypted_char = chr(ord(char) ^ ord(key_char)) cipher_text += encrypted_char return cipher_text def test(plain_text, text_key): p = 97 g = 31 if not is_prime(p) and not is_prime(g): print("Enter prime numbers") return a = randint(p-10, p) b = randint(g-10, g) print(f"a = {a}") print(f"b = {b}") u = generator(g, a, p) v = generator(g, b, p) key = generator(v, a, p) b_key = generator(u, b, p) shared_key = None if key == b_key: shared_key = key else: print("Invalid key") return semi_cipher = dynamic_xor_encrypt(plain_text, text_key) cipher = encrypt(semi_cipher, shared_key) print(f'cipher is: {cipher}') if __name__ == "__main__": message = sys.argv[1] test(message, "trudeau") ``` ::: :::spoiler <b>enc_flag</b> ```tex! a = 88 b = 26 cipher is: [97965, 185045, 740180, 946995, 1012305, 21770, 827260, 751065, 718410, 457170, 0, 903455, 228585, 54425, 740180, 0, 239470, 936110, 10885, 674870, 261240, 293895, 65310, 65310, 185045, 65310, 283010, 555135, 348320, 533365, 283010, 76195, 130620, 185045] ``` ::: Bài này nhìn trông phức tạp vậy chứ thực chất cũng chỉ là thử kỹ năng dịch ngược của chúng ta mà thôi. Chúng ta sẽ thấy hàm này sử dụng các phép toán XOR và phép nhân trên miền nguyên $\mathbb{Z}$, vốn có thể dịch ngược được. Dưới đây sẽ là script lời giải: :::spoiler <b style="color:tomato">solution.py</b> ```python= text_key = "trudeau" def dynamic_xor_encrypt(ciphertext, text_key): plaintext = "" key_length = len(text_key) for i, char in enumerate(ciphertext): key_char = text_key[i % key_length] encrypted_char = chr(ord(char) ^ ord(key_char)) plaintext += encrypted_char return plaintext def decrypt_message(ciphertext, text_key): a = 88; b = 26; p = 97; g = 31 shared_key = pow(g, a*b, p) semi_plaintext = "" for number in ciphertext: semi_plaintext += chr(number//(shared_key*311)) plaintext = dynamic_xor_encrypt(semi_plaintext, text_key) return plaintext[::-1] text_key = "trudeau" ciphertext = [97965, 185045, 740180, 946995, 1012305, 21770, 827260, 751065, 718410, 457170, 0, 903455, 228585, 54425, 740180, 0, 239470, 936110, 10885, 674870, 261240, 293895, 65310, 65310, 185045, 65310, 283010, 555135, 348320, 533365, 283010, 76195, 130620, 185045] flag = decrypt_message(ciphertext, text_key) print("[!] Got flag:", flag) # [!] Got flag: picoCTF{custom_d2cr0pt6d_019c831c} ``` ::: :::success **Flag:** <b style="color:#3262a8">picoCTF{custom_d2cr0pt6d_019c831c}</b> ::: ## miniRSA ![image](https://hackmd.io/_uploads/rkCyEWvqle.png) :::spoiler <b>ciphertext</b> ```tex! N: 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673 e: 3 ciphertext (c): 2205316413931134031074603746928247799030155221252519872650080519263755075355825243327515211479747536697517688468095325517209911688684309894900992899707504087647575997847717180766377832435022794675332132906451858990782325436498952049751141 ``` ::: Ban đầu nhìn thấy $e$ nhỏ làm mình tưởng bài này ngon lành dễ ăn lắm chứ (dễ ăn thật nếu mình cẩn thận suy nghĩ một chút). Mình có sử dụng thư viện **gmpy2** của python và làm như sau: ```python= from gmpy2 import iroot from Crypto.Util.number import long_to_bytes N = 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673 e = 3 c = 2205316413931134031074603746928247799030155221252519872650080519263755075355825243327515211479747536697517688468095325517209911688684309894900992899707504087647575997847717180766377832435022794675332132906451858990782325436498952049751141 k = 1 while(True): expression = c + k*N m, exact = iroot(expression, e) if exact: flag = long_to_bytes(m).decode() break print("[!] Got flag:", flag) ``` Mình nhận ra điều bất thường khi $k$ chạy đến giá trị vài triệu, sau khi kiểm tra và suy nghĩ thì mình nhận ra chân lý như sau: Giá trị của $n$ rất lớn tận 2048 bit, điều này dẫn tới khả năng $m^{e} < n$, tức là $k = 0$. Vì vậy mình đã có sự chỉnh sửa nhỏ và đã ra kết quả. Dưới đây là các script lời giải của mình, ở trên là lời giải mình nghĩ ra còn ở dưới là lời giải mà mình thấy khá hay nên sưu tầm lại. :::spoiler <b style="color:tomato">solution1.py</b> ```python= from gmpy2 import iroot from Crypto.Util.number import long_to_bytes N = 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673 e = 3 c = 2205316413931134031074603746928247799030155221252519872650080519263755075355825243327515211479747536697517688468095325517209911688684309894900992899707504087647575997847717180766377832435022794675332132906451858990782325436498952049751141 k = 0 while(True): expression = c + k*N m, exact = iroot(expression, e) if exact: flag = long_to_bytes(m).decode() break print("[!] Got flag:", flag) # [!] Got flag: picoCTF{n33d_a_lArg3r_e_d0cd6eae} ``` ::: :::spoiler <b style="color:tomato">solution2.py</b> ```python= from Crypto.Util.number import long_to_bytes def binary_search_message(e, c): left = 1; right = c while(right - left > 1): middle = (right + left)//2 if(middle**e > c): right = middle else: left = middle return left if (left**e == c) else right N = 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673 e = 3 c = 2205316413931134031074603746928247799030155221252519872650080519263755075355825243327515211479747536697517688468095325517209911688684309894900992899707504087647575997847717180766377832435022794675332132906451858990782325436498952049751141 flag = long_to_bytes(binary_search_message(e, c)) print("[!] Got flag:", flag.decode()) # [!] Got flag: picoCTF{n33d_a_lArg3r_e_d0cd6eae} ``` ::: :::success **Flag:** <b style="color:#3262a8">picoCTF{n33d_a_lArg3r_e_d0cd6eae}</b> ::: ## b00tl3gRSA3 ![image](https://hackmd.io/_uploads/B1pcbzDqlg.png) Bài này khi kết nối với server thì sẽ được trả về 3 số $n, e, c$, ngoài ra thì không trả về gì thêm. Để ý gợi ý họ có nói rằng có nhiều hơn số thừa số nguyên tố ngoài $p$ và $q$, vì vậy mình sẽ dùng thuật toán phân tích thừa số nguyên tố bằng đường cong Elliptic của Lenstra (<a href="https://en.wikipedia.org/wiki/Lenstra_elliptic-curve_factorization">elliptic-curve factorization method</a>). May mắn là **Sagemath** đã có sẵn thuật toán này nên mình chỉ việc gọi thôi. Dưới đây là script lời giải: :::spoiler <b style="color:tomato">solution.py</b> ```python= from pwn import * from sage.all import Integer, prod from Crypto.Util.number import long_to_bytes connection = remote("jupiter.challenges.picoctf.org", 51575) c = int(connection.recvline().strip().decode()[2:]) n = int(connection.recvline().strip().decode()[2:]) e = int(connection.recvline().strip().decode()[2:]) factor_list = Integer(n).factor(algorithm="ecm") phi = prod([p - 1 for p,e in factor_list]) d = pow(e, -1, phi) m = pow(c, d, n) flag = long_to_bytes(m).decode() connection.success(f"Got flag: {flag}") connection.close() # [+] Got flag: picoCTF{too_many_fact0rs_0731311} ``` ::: :::success **Flag:** <b style="color:#3262a8">picoCTF{too_many_fact0rs_0731311}</b> ::: ## Sum-O-Primes ![image](https://hackmd.io/_uploads/HJ4Q1UP9le.png) :::spoiler <b>gen.py</b> ```python= #!/usr/bin/python from binascii import hexlify from gmpy2 import mpz_urandomb, next_prime, random_state import math import os import sys if sys.version_info < (3, 9): import gmpy2 math.gcd = gmpy2.gcd math.lcm = gmpy2.lcm FLAG = open('flag.txt').read().strip() FLAG = int(hexlify(FLAG.encode()), 16) SEED = int(hexlify(os.urandom(32)).decode(), 16) STATE = random_state(SEED) def get_prime(bits): return next_prime(mpz_urandomb(STATE, bits) | (1 << (bits - 1))) p = get_prime(1024) q = get_prime(1024) x = p + q n = p * q e = 65537 m = math.lcm(p - 1, q - 1) d = pow(e, -1, m) c = pow(FLAG, e, n) print(f'x = {x:x}') print(f'n = {n:x}') print(f'c = {c:x}') ``` ::: :::spoiler <b>output.txt</b> ```tex! x = 1626a189dcb38ca6b8e9ee26623ab5c3c6cd7e4c7ff6726f4b03831ca48c617a056827c5763458d0aa7172650072b892649cc73f943f156b795ff5dd2fc9a53b140cf9c3ee2cbb8181d17bb0275f404b4090766f798ad156db7e71000e93db65f3e1bc7406532d0f509fbecf095ef215b4ad51f5e8ac765861e5f93808948bf72 n = 720d66204ec312d7f1bc688495d4585ec58520170b86ed3488c3f9c76407b7e9e466b82a282ba90d484698160f2e27f413b07cf8805d560abdffa977547d5fec3190a1ce284dfc8e92193f2f70590bf9c6e6d0ab449e35ef43ed20232b7f8686696125cde1f950230fbc6858392a3715c1b8a4947748b7fadd5cc921716ad5e0129c91ea88fceee140fb1c594606186afacb69143ef8f7b3b1aa2cc3206395c60e71ec0555dd15838d8a8395e8ccf9a4e4c4199ae0ab3f8af7ebc6605edc5ddd480be2d6c41e38618eba5822a1e566080877268802750de71e890ac865ebf87fdc290d9151e407dff4c97390c9e7388fd538e2716515cea2240f55963c2e0c21 c = 554b90eb12fbece709d7bf23ab91f9b52d71cd77fbf42f65d68623c2055d99956b9bcf2eaf14771fa5781fae86624e44b452a0f68768849faba1b9695ce353a17238a3e7040ee7aede68b35bf4b51daf0982653910b280ac98aad9a5b3c49d226e10b2e8660effc2cb2a553039bde527e42f1795bc078af6ed2928505be6df1ebe993f2ed8c10477dd5cc9f899d1e69b6512b71c732472dde521f5393c76b2f9fbed668560d4e50ca177dd14b923414549d688b20fab94dba7cad7b5a729941c772dc4a1db79b0e6a111d2d2e8998b4e2a272dc940a9dd4cf856faa5a2ee0cb6f36f0ce6edbb421697e517a4d589cc5a880eecf6fbf65e5f6a1a437b06e5ff9a ``` ::: Đề bài cho chúng ta $e$, $c$ và đặc biệt là hai số $n = p \cdot q$ và $x = p + q$. Theo định lý <a href="https://en.wikipedia.org/wiki/Vieta%27s_formulas">Viète</a> đảo ta có $p$ và $q$ là nghiệm của phương trình $t^{2} - xt + n = 0$. Sau khi giải phương trình tìm lại được $p$, $q$ thì ta sẽ tiến hành giải mã RSA như bình thường. Chú ý rằng $d = e^{-1} ~\operatorname{mod} m$ với $m = \operatorname{lcm}(a,b)$, công thức này hơi khác so với RSA tiêu chuẩn khi nó dùng hàm <a href="https://en.wikipedia.org/wiki/Carmichael_function"><b>Carmichael</b></a> thay vì phi hàm <a href="https://en.wikipedia.org/wiki/Euler%27s_totient_function"><b>Euler</b></a>. :::spoiler <b style="color:tomato">solution.py</b> ```python= from sage.all import var, solve, inverse_mod, lcm from Crypto.Util.number import long_to_bytes x = "1626a189dcb38ca6b8e9ee26623ab5c3c6cd7e4c7ff6726f4b03831ca48c617a056827c5763458d0aa7172650072b892649cc73f943f156b795ff5dd2fc9a53b140cf9c3ee2cbb8181d17bb0275f404b4090766f798ad156db7e71000e93db65f3e1bc7406532d0f509fbecf095ef215b4ad51f5e8ac765861e5f93808948bf72" n = "720d66204ec312d7f1bc688495d4585ec58520170b86ed3488c3f9c76407b7e9e466b82a282ba90d484698160f2e27f413b07cf8805d560abdffa977547d5fec3190a1ce284dfc8e92193f2f70590bf9c6e6d0ab449e35ef43ed20232b7f8686696125cde1f950230fbc6858392a3715c1b8a4947748b7fadd5cc921716ad5e0129c91ea88fceee140fb1c594606186afacb69143ef8f7b3b1aa2cc3206395c60e71ec0555dd15838d8a8395e8ccf9a4e4c4199ae0ab3f8af7ebc6605edc5ddd480be2d6c41e38618eba5822a1e566080877268802750de71e890ac865ebf87fdc290d9151e407dff4c97390c9e7388fd538e2716515cea2240f55963c2e0c21" c = "554b90eb12fbece709d7bf23ab91f9b52d71cd77fbf42f65d68623c2055d99956b9bcf2eaf14771fa5781fae86624e44b452a0f68768849faba1b9695ce353a17238a3e7040ee7aede68b35bf4b51daf0982653910b280ac98aad9a5b3c49d226e10b2e8660effc2cb2a553039bde527e42f1795bc078af6ed2928505be6df1ebe993f2ed8c10477dd5cc9f899d1e69b6512b71c732472dde521f5393c76b2f9fbed668560d4e50ca177dd14b923414549d688b20fab94dba7cad7b5a729941c772dc4a1db79b0e6a111d2d2e8998b4e2a272dc940a9dd4cf856faa5a2ee0cb6f36f0ce6edbb421697e517a4d589cc5a880eecf6fbf65e5f6a1a437b06e5ff9a" e = 65537 x = int(x, 16) n = int(n, 16) c = int(c, 16) t = var('t') solution = solve(t**2 - x*t + n, t) p = solution[0].rhs() q = solution[1].rhs() d = inverse_mod(e, lcm(p - 1, q - 1)) m = pow(c, d, n) flag = long_to_bytes(m).decode() print("[!] Got flag:", flag) # [!] Got flag: picoCTF{pl33z_n0_g1v3_c0ngru3nc3_0f_5qu4r35_92fe3557} ``` ::: :::success **Flag:** <b style="color:#3262a8">picoCTF{pl33z_n0_g1v3_c0ngru3nc3_0f_5qu4r35_92fe3557}</b> ::: ## New Vignere ![image](https://hackmd.io/_uploads/HyYm3VDqle.png) :::spoiler <b>new_vignere.py</b> ```python= import string LOWERCASE_OFFSET = ord("a") ALPHABET = string.ascii_lowercase[:16] def b16_encode(plain): enc = "" for c in plain: binary = "{0:08b}".format(ord(c)) enc += ALPHABET[int(binary[:4], 2)] enc += ALPHABET[int(binary[4:], 2)] return enc def shift(c, k): t1 = ord(c) - LOWERCASE_OFFSET t2 = ord(k) - LOWERCASE_OFFSET return ALPHABET[(t1 + t2) % len(ALPHABET)] flag = "redacted" assert all([c in "abcdef0123456789" for c in flag]) key = "redacted" assert all([k in ALPHABET for k in key]) and len(key) < 15 b16 = b16_encode(flag) enc = "" for i, c in enumerate(b16): enc += shift(c, key[i % len(key)]) print(enc) ``` ::: Bài này giống như một bản nâng cấp của [New Caesar](#New-Caesar) khi ta chỉ được cho biết một vài thông tin mơ hồ như sau: ```python= flag = "redacted" assert all([c in "abcdef0123456789" for c in flag]) key = "redacted" assert all([k in ALPHABET for k in key]) and len(key) < 15 ``` Bài này chúng ta sẽ tiến hành <a href="https://en.wikipedia.org/wiki/Kasiski_examination">kiểm tra Kasiski</a> để có thể dự đoán độ dài khóa. Nói thêm về kiểm tra Kasiski, ta có các bước chính sau: 1. <b>Tìm chuỗi lặp:</b> Bạn cần tìm tất cả các chuỗi ký tự lặp lại có độ dài được khuyến khích là ít nhất 3 chữ cái trong văn bản mã (vì càng nhiều chữ cái thì độ chính xác càng cao). 2. <b>Tính khoảng cách:</b> Đo khoảng cách giữa các lần xuất hiện của mỗi chuỗi lặp đó. 3. <b>Tìm ước số chung lớn nhất (UCLN):</b> Tính UCLN của tất cả các khoảng cách đã tìm được. UCLN này có khả năng cao là độ dài của khóa. Ví dụ, nếu một chuỗi "ABC" xuất hiện hai lần trong văn bản mã và khoảng cách giữa chúng là $20$, và một chuỗi khác "XYZ" cũng lặp lại với khoảng cách là $30$, thì $\gcd(20, 30) = 10$ Điều này gợi ý rằng độ dài khóa có thể là $10$. Để tìm hiểu thêm về bản chất đằng sau và tính đúng đắn của phương pháp này thì bạn có thể lên mạng hoặc dùng LLM để giải thích. Quay trở lại thử thách, ta thấy chuỗi`epdfglkfnbjbhbpicohidjgkhfnejeecmjfnejddgmhpndmchbmifnepdhdmhbah` có chuỗi con `fn` và khoảng cách giữa các chuỗi con `fn` này lần lượt là $9$ và $18$. Như vậy rất có thể độ dài của khóa sẽ là ước của $\gcd(9, 18) = 9$. Ta cũng để ý rằng các ký tự trong flag luôn là ký tự hexadecimal nên khi mã hóa Base16 flag thì ta được một chuỗi mà các ký tự ở vị trí chẵn luôn là `h` hoặc `g`. > [!Note] Giải thích > Vì flag chỉ chứa các ký tự hexadecial $(0-9, a-f)$, nên khi chuyển sang mã nhị phân 8-bit, 4 bit đầu tiên sẽ có quy luật rất rõ ràng: > - Các chữ số $(0-9)$ khi chuyển sang mã **ASCII** có 4 bit đầu là `0011`. Giá trị này tương ứng với ký tự thứ 3 trong **ALPHABET** là `d`. > - Các chữ cái $(a-f)$ khi chuyển sang mã **ASCII** có 4 bit đầu là `0110`. Giá trị này tương ứng với ký tự thứ 6 trong **ALPHABET** là `g`. > Điều này có nghĩa là chuỗi trung gian sau khi qua **b16_encode** (nhưng trước khi bị mã hóa **Vigenère**) sẽ có một quy luật: các ký tự ở vị trí chẵn (0, 2, 4,...) phải là `d` hoặc `g`. Đây chính là "lỗ hổng" để giải mã. Vì không gian của mỗi ký tự trong khóa rất nhỏ, chỉ có $16$ khả năng, nên ta sẽ tận dụng điều này và các tính chất đã phân tích ở trên để tiến hành brute-force như sau: 1. <b>Lặp qua từng vị trí của khóa:</b> Tạo một vòng lặp chạy từ `key_pos = 0` đến `len(key)`. 2. <b>Với mỗi vị trí, thử mọi khả năng:</b> Bên trong vòng lặp trên, tạo một vòng lặp nữa để thử từng ký tự trong **ALPHABET** (`a` đến `p`) làm ứng cử viên cho `key[key_pos]`. Gọi đây là `candidate_char`. 3. <b>Kiểm tra ứng cử viên:</b> Giờ, chúng ta cần kiểm tra xem candidate_char có phải là ký tự đúng cho `key[key_pos]` hay không. Cách làm như sau: a. Lấy tất cả các ký tự trong bản mã (**CIPHERTEXT**) mà `key[key_pos]` đã mã hóa. Đó là các ký tự ở vị trí `key_pos`, `key_pos + len(key)`, `key_pos + 2*len(key)`, ... b. Với mỗi ký tự bản mã này, hãy **giải mã thử** nó bằng `candidate_char` (dùng hàm `unshift`). c. Sau khi giải mã thử, chúng ta có một ký tự trung gian giả định. Bây giờ, hãy áp dụng "Quy luật `d` hoặc `g`". d. **Chỉ khi** vị trí của ký tự bản mã là **chẵn** (ví dụ: 0, 2, 18, 20,...), thì ký tự trung gian giả định mà ta vừa tìm được phải là `d` hoặc `g`. e. Nếu có bất kỳ một trường hợp nào vi phạm quy luật này, `candidate_char` chắc chắn sai. Chúng ta ngay lập tức loại bỏ nó và chuyển sang thử ký tự tiếp theo trong **ALPHABET**. 4. <b>Tìm ký tự đúng:</b> Nếu một `candidate_char` vượt qua tất cả các bài kiểm tra ở bước 3 cho tất cả các vị trí liên quan, thì đó chính là ký tự đúng cho `key[key_pos]`. Lưu nó lại và chuyển sang tìm ký tự cho vị trí tiếp theo của khóa (`key_pos + 1`). Nếu bạn nào thấy văn phong giống AI thì đúng rồi đó, mình đã dựa vào AI để viết đoạn phân tích và giải thích trên (vì mình lười :penguin:). Nhân tiện thì dưới đây sẽ là script giải bài cho thử thách này: :::spoiler <b style="color:tomato">solution.py</b> ```python= import string LOWERCASE_OFFSET = ord("a") ALPHABET = string.ascii_lowercase[:16] # Đảo ngược của hàm shift def unshift(c: str, k: str) -> str: t1 = ord(c) - LOWERCASE_OFFSET t2 = ord(k) - LOWERCASE_OFFSET decrypted_index = (t1 - t2) % len(ALPHABET) return ALPHABET[decrypted_index] # Đảo ngược hàm mã hóa b16 def b16_decode(cipher: str) -> str: plain = "" for c1, c2 in zip(cipher[0::2], cipher[1::2]): n1 = "{0:04b}".format(ALPHABET.index(c1)) n2 = "{0:04b}".format(ALPHABET.index(c2)) binary = int(n1 + n2, 2) plain += chr(binary) return plain # Đảo ngược mã hóa đề bài def decrypt(enc: str, key: str) -> str: dec = "" for i, c in enumerate(enc): dec += unshift(c, key[i % len(key)]) return b16_decode(dec) # Sau kiểm tra Kasiski, ta tìm được độ dài khóa tiềm năng là 1, 3 và 9 CIPHERTEXT = "epdfglkfnbjbhbpicohidjgkhfnejeecmjfnejddgmhpndmchbmifnepdhdmhbah" POTENTIAL_KEY_LENGTH = [1, 3, 9] # Chúng ta bắt đầu brute-force thông minh để tìm những ký tự khóa khả quan for KEY_LENGTH in POTENTIAL_KEY_LENGTH: possible_key_chars = [[] for _ in range(KEY_LENGTH)] for key_position in range(KEY_LENGTH): for key_char in ALPHABET: is_valid_key_char = True for cipher_postion in range(key_position, len(CIPHERTEXT), KEY_LENGTH): decrypted_char = unshift(CIPHERTEXT[cipher_postion], key_char) if(cipher_postion % 2 == 0): if(decrypted_char not in ['d', 'g']): is_valid_key_char = False break if is_valid_key_char: possible_key_chars[key_position].append(key_char) is_possible_list = True for key_char in possible_key_chars: if(key_char == []): is_possible_list = False break if(is_possible_list == False): continue # Đoạn này dùng quay lui (backtracking) để có thể lấy ra tất cả các khóa khả thi def backtrack(i: int, n: int, current: string, possible_key_chars: list, possible_key_list: list): if(i == n): possible_key_list.append(current) return for char in possible_key_chars[i]: current += char backtrack(i+1, n, current, possible_key_chars, possible_key_list) current = current[:-1] # Python lỏ này không xịn như C++, dùng string::pop_back() là xong possible_key_list = [] backtrack(0, len(possible_key_chars), "", possible_key_chars, possible_key_list) # Muốn in ra màu mè ở đoạn này một chút mà cũng khó nhỉ 🥺 print("All possible flags and keys:") for index, possible_key in enumerate(possible_key_list, start=1): possible_flag = decrypt(CIPHERTEXT, possible_key) print(f"{index:>{1}}. Key: {possible_key}") print(" "*(1 + 5) + "Flag: picoCTF{" + f"{possible_flag}" + '}') # All possible flags and keys: # 1. Key: bgabajepk # Flag: picoCTF{94bf01ad4b8a63425c32c02ba4c9632f} # 2. Key: bgnbajepk # Flag: picoCTF{9dbf04ad4bha63725c3bc02ea4c9f32f} ``` ::: :::success **Flag:** <b style="color:#3262a8">picoCTF{94bf01ad4b8a63425c32c02ba4c9632f}</b> ::: >[!Important] Lưu ý >Ở flag thứ hai có ký tự `h` kỳ lạ không thuộc ký tự hexadecimal nên ta có thể loại luôn flag thứ hai nhé! ## Summary Đây là những bài mà mình cảm thấy khá thú vị trong **picoCTF**. Các câu trung bình thì thường không thuần khiết cryptography (cụ thể là lai tạp với forensics) và cũng không khó lắm. Các câu ở mức độ khó thì giống với cryptography và độ khó cũng tăng lên đáng kể làm việc giải nó cũng trở nên thú vị hơn. Hi vọng bài viết này sẽ giúp ích được phần nào đó cho các bạn đọc. Xin chào và hẹn gặp lại ở các phần tiếp theo :wave:.