PicoCTF write up - Cryptography (密碼學篇 - 上)

在今年的十月,我決定改變一下玩 PicoCTF 的方式,以 Category 分類去玩,這樣子玩有以下幾個優點:

  • 正向回饋 - 相信大部份人都覺得 CTF 入門很難,小白剛玩 CTF 競賽可能連一題都解不出,很快就會失去興趣。所以不如選一個自己擅長/喜歡的類別,培養出能解同類型題的能力,當有能力解出題目,才會有興趣玩下去,產生正循環。
  • 鞏固知識 - 同類型的題,除了能加強該題型的知識外,還有更重要的一點是”工具使用”的練習,學會善用工具,才能在日後解決一些由延伸或變異而產生的新題型。
  • 團隊分工 - CTF 的題目內容廣泛,包山包海,通常情況下由隊伍各自分工去解不同的題。集中技能樹,不只能幫助隊伍,還只能優先在隊伍中找到自己的定位。

所以我選擇從 Cryptography (密碼學篇)開始,除了我自認自己的數學底子不錯以外,Cryptography 的題不會像 Web 一樣有時間上的限制,又不需要先架設環境,即使競賽已經完結,只要知道題目還是可以繼續解題,基本能把它看作是一道永不過時的數學題。

Cryptography

Mod 26 (10 points)

Cryptography can be easy, do you know what ROT13 is? cvpbPGS{arkg_gvzr_V’yy_gel_2_ebhaqf_bs_ebg13_MAZyqFQj}

Hint : This can be solved online if you don’t want to do it by hand!

知識點:ROT13

英文字母共有 26 個,如果分成 2 組,每組就有 13 個,可以兩兩替換。

480px-ROT13_table_with_example svg

不必手算,直接 Google 找 ROT13 decode 的網站就出答案

構成Flag picoCTF{next_time_I'll_try_2_rounds_of_rot13_ZNMldSDw}


Mind your Ps and Qs (20 points)

In RSA, a small e value can be problematic, but what about N? Can you decrypt this? values

Hint : Bits are expensive, I used only a little bit over 100 to save money

values :

Decrypt my super sick RSA:
c: 62324783949134119159408816513334912534343517300880137691662780895409992760262021
n: 1280678415822214057864524798453297819181910621573945477544758171055968245116423923
e: 65537

這是一道 RSA 題

已知:c (密文)、n (由 p, q 兩大質數相乘)、e (密鑰)

求:d (私鑰) 和 m (明文)

這裡可以攻擊的地方,是 n 的數值(82位)不夠大,所以 p 和 q 是可以被計算/分解出來的。

$echo -n "1280678415822214057864524798453297819181910621573945477544758171055968245116423923" | wc -m
$82

關於分解大數,我這邊提供 2 個方法:

方法一、使用 factordb

$pip3 install factordb-pycli #需要先安裝對應的 py 模組
$factordb 1280678415822214057864524798453297819181910621573945477544758171055968245116423923
$1899107986527483535344517113948531328331 674357869540600933870145899564746495319033

方法二、使用線上網站

factordb.com

螢幕截圖 2021-10-05 下午3 40 14

現在有 p 和 q

p = 1899107986527483535344517113948531328331
q = 674357869540600933870145899564746495319033

用 python 寫的腳本便能解出 m (明文)

import libnum

p = 1899107986527483535344517113948531328331
q = 674357869540600933870145899564746495319033
n = p * q
e = 65537
c = 62324783949134119159408816513334912534343517300880137691662780895409992760262021
phi = (p - 1) * (q - 1)

d = libnum.modular.invmod(e, phi)
m = libnum.n2s(pow(c, d, n))
print(m) 

Easy Peasy (40 points)

A one-time pad is unbreakable, but can you manage to recover the flag? (Wrap with picoCTF{}) nc mercury.picoctf.net 64260 otp.py

Hint : Maybe there’s a way to make this a 2x pad.

otp.py :

#!/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)

知識點:互斥或密碼 (XOR cipher)

這是一個比較典型的”代碼審計”,這種題目基本會給 2 個部分

  1. 用 nc 連線的伺服器 (nc mercury.picoctf.net 64260)
  2. 伺服器的原始碼 (otp.py)

通常就是一邊 nc 連線、一邊看 code 去找找看有沒有可鑽的空子。發現這題目沒有預期的簡單

由於這篇是新手向,關於細節的部分我盡可能解釋詳盡一些

1. 使用 nc 連線:

這可謂 CTF 基礎中的基礎,可以說是”一定”會用到的工具。

在 Mac 和 Linux 上:

直接使用 nc 指令

$ nc mercury.picoctf.net 64260
在 Windows 上:

下載 netcat,使用方法是在 cmd 裡執行 nc.exe

以這題目的形式,連線後這樣:

$ nc mercury.picoctf.net 64260
******************Welcome to our OTP implementation!******************
This is the encrypted flag!
51466d4e5f575538195551416e4f5300413f1b5008684d5504384157046e4959

What data would you like to encrypt? test
Here ya go!
442b2f0c

What data would you like to encrypt? 

2. 邏輯運算基礎之 XOR

這裡就關乎到一些基本功和對於密碼學裡加密方法的知識儲備,馬上惡補 XOR 的邏輯運算方法。(為什麼會知道這題是 XOR 加密? 那是因為透過後面的代碼審計得知)

XOR Cipher Trace Table:

Plaintext Key Ciphertext
0 0 0
0 1 1
1 0 1
1 1 0

舉例來說 5 和 6 做 XOR 運算會出得 3,寫作 5 ⊕ 6 = 3

   00000101 (5)
⊕  00000110 (6)
-----------
=  00000011 (3)

也能透過 python 驗證:

$ python 
Python 3.9.7 (default, Sep  3 2021, 12:45:31) 
[Clang 12.0.0 (clang-1200.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 5 ^ 6
3

同理可用作英文字母(透過 ASCII 碼)

   01101001 (i)
⊕  01101110 (n)
-----------
=  00000111 (7)
$ python       
Python 3.9.7 (default, Sep  3 2021, 12:45:31) 
[Clang 12.0.0 (clang-1200.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> ord('i') ^ ord('n')
7

有 2 件事情可以記一下

  • 任何數字/字母與 0 做 XOR,原有數不變,寫作 A ⊕ 0 = A
  • 對同一個 Key 做兩次 XOR 會還原成自身,即 (A ⊕ B) ⊕ B = A

3. 代碼審計:

這部分相信對大多數人(包括我)來說是比較困難的,尤其是對目標語言不熟悉的情況下,閱讀起來像天書一樣,看的一臉懵逼。

這時候心態就顯得很重要,不要怕,因為我們都是一樣XD,差距就不過是閱讀理解速度快慢而已,而這方面是可以累積而來。

Tips: 下面有一些關於代碼審計的技巧

a.最小化程式碼

比如說 我先運行其中一行,看看它是干嘛的

result = list(map(lambda p, k: "{:02x}".format(ord(p) ^ k), ui, key))

發現光只有這一行,是無法運行。

b.逐個函數去 Google 當中的意思

譬如不知道當中的 ord() 是啥意思,就去 Google 看看,就知道它是把 unicode 轉成十進位數字的意思

也能這樣子測試:

$ python
Python 3.9.7 (default, Sep  3 2021, 12:45:31) 
[Clang 12.0.0 (clang-1200.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> ord("a")
97
>>> 

再一個一個 Google 去理解 ^ , format, lambda, map, list 這些符號/函數的意思,當然有一些概念可能無法完全理解,但打 CTF 只需大概知道在做什麼事情便可以。

c.改改看,模擬運行

開一個新的 python 檔案 my_xor.py

ui = "abcd"
key = [0,5,0,0]

result = list(map(lambda p, k: "{:03x} ".format(ord(p) ^ k), ui, key))

print("Here ya go!\n{}\n".format("".join(result)))

改一點東西,驗證和看看差異。

$ python myxor.py
Here ya go!
061 067 063 064 

上面就是”abcd”對應”0500”做了 XOR ,然後以 16 進位(3位長度)顯示出來

d.重複以上方法的耐性

到這邊,如果看懂了原始碼其實正在做 XOR 加密就算及格了,如果仍然沒看懂,就重複以上 a ~ c 的步驟,直到看懂每一行程式碼

可以的話,把整份改成能運行的模樣,當然把長度 KEY_LEN 縮短,加入自己的 flagkf ,注解掉無法執行的部分。

#!/usr/bin/python3 -u
import os.path

KEY_FILE = "key"
#KEY_LEN = 50000
KEY_LEN = 10
FLAG_FILE = "flag"

flag = "Ank"
kf = [6,2,3,1,7,8,4,3,1,3] #Random

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)

執行一下:

$ python otp_my.py
******************Welcome to our OTP implementation!******************
This is the encrypted flag!
47 6c 68 

What data would you like to encrypt? Ank
Here ya go!
40 69 63 

What data would you like to encrypt? Ank!
Here ya go!
45 6d 6a 22 

What data would you like to encrypt? Ank
Here ya go!
47 6c 68 

一開始的 flag 佔了 3 個字元,我再用 Ank 和 Ank! 多佔 7 個字元,kf 總長度 10 被用完了,此時 kf 就會重覆使用,所以最後一次的 Ank 所加密出的密文便會跟一開始一樣。

如果先放 7 個 0 把 kf 用完,再放 10 個 0 便可把密本 kf 都看光

$ python otp_my.py
******************Welcome to our OTP implementation!******************
This is the encrypted flag!
47 6c 68 

What data would you like to encrypt? 0000000
Here ya go!
31 37 38 34 33 31 33 

What data would you like to encrypt? 0000000000
Here ya go!
36 32 33 31 37 38 34 33 31 33 

3. 解答

前期工作準備完成,可以嘗試攻略題目

$ nc mercury.picoctf.net 64260
******************Welcome to our OTP implementation!******************
This is the encrypted flag!
51466d4e5f575538195551416e4f5300413f1b5008684d5504384157046e4959

一開始的密文有 64 位(16 進位),意味明文是 32 位長度

如果要把整個用來作 XOR 加密用的kf 用完,需要塞 50000 - 32 = 49968 個 a

這裡先教用笨辦法手動塞,先塞 986 個 a。再塞 1000 個 a,重複 49 次

到第 50000 次之後,再填入 32 個 a,這時候 kf 便會重複使用。

32 個 a 和 kf前 32 位做 XOR 得到新密文:

03463d190702003d195004133d190356433d1902503d1950563d1900513d1959

如果把 32 個 a 寫作 16 進位:

6161616161616161616161616161616161616161616161616161616161616161

把上面 2 串東西再做一次 XOR 便能得知 kf

>>> new_c = 0x03463d190702003d195004133d190356433d1902503d1950563d1900513d1959
>>> all_a = 0x6161616161616161616161616161616161616161616161616161616161616161
>>> kf = hex(new_c ^ all_a)
>>> print(kf)
0x62275c786663615c783165725c786237225c7863315c7831375c7861305c7838

既然已經得到 XOR 加密時用的密本 kf ,那只要拿原來的密文和 kf 再做一次 XOR ,便可解密

>>> old_c = 0x51466d4e5f575538195551416e4f5300413f1b5008684d5504384157046e4959
>>> kf = 0x62275c786663615c783165725c786237225c7863315c7831375c7861305c7838
>>> flag = hex(old_c ^ kf)
>>> print(flag)
0x3361313639343464616434333237313763636333393435643364393634323161

把 16 flag 進位轉成 ASCII

>>> bytes.fromhex('3361313639343464616434333237313763636333393435643364393634323161').decode('utf-8')
'3a16944dad432717ccc3945d3d96421a'

構成 flag = picoCTF{3a16944dad432717ccc3945d3d96421a}

4. 編寫成 python 自動化解答:

這恐怕是最困難的部分

from pwn import *

r = remote("mercury.picoctf.net", 64260)
r.recvline()
r.recvline()
flag_enc = bytes.fromhex(r.recvline().decode())
fl = len(flag_enc)


def enc(m):
    r.sendlineafter(b"What data would you like to encrypt? ", m)
    r.recvline()
    return bytes.fromhex(r.recvline().decode())


enc("a" * (50000 - fl))
keyxor = enc("a" * fl)


def xor(x, y):
    return bytes(a ^ b for a, b in zip(x, y))


key = xor(keyxor, b"a" * fl)
flag = xor(flag_enc, key)
print(flag.decode())

The Numbers (50 points)

The numbers… what do they mean?

Hints : The flag is in the format PICOCTF{}

numbers: the_numbers

先看圖片,發現了 {} ,跟我們要的 flag 很像,而且 { 前面有 7 個數字,可能剛好是對應 picoctf

所以這題就是 1 = a ; 2 = b ; 3 = c ,以此類推,可以手寫或用 python 轉換

num = input("input>>")
num_list = num.split()
print(num_list)
new_num_list = [int(c) + 96 for c in num_list]
print(new_num_list)
flag = [chr(int(num)) for num in new_num_list]
print(flag)

構成flag PICOCTF{thenumbersmason}


New Caesar (60 points)

We found a brand new type of encryption, can you break the secret code? (Wrap with picoCTF{}) mlnklfnknljflfmhjimkmhjhmljhjomhmmjkjpmmjmjkjpjojgjmjpjojojnjojmmkmlmijimhjmmj new_caesar.py

Hint 1 : How does the cipher work if the alphabet isn’t 26 letters? Hint 2 : Even though the letters are split up, the same paradigms still apply

new_caesar.py:

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)

知識點:凱撒密碼

這雖然是凱撒密碼的題,但並非單純的”字母位移”,這題我認為難度最大的”代碼審計”部分,只要先理解它是如何進行加密,便可以逆向進行解密。

首先小幅度的修改讓 new_caesar.py 能運行起來:

import string

LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]

print("LOWERCASE_OFFSET",LOWERCASE_OFFSET)
print("ALPHABET",ALPHABET)

def b16_encode(plain):
    enc = ""
    for c in plain:
        print("plain,c = ",plain,c)
        binary = "{0:08b}".format(ord(c))
        print("binary = ",binary,chr(int(binary[:8], 2)))
        enc += ALPHABET[int(binary[:4], 2)]
        enc += ALPHABET[int(binary[4:], 2)]
        print("enc ",enc)
        print("---")
    return enc

def shift(c, k):
    t1 = ord(c) - LOWERCASE_OFFSET
    t2 = ord(k) - LOWERCASE_OFFSET
    return ALPHABET[(t1 + t2) % len(ALPHABET)]

flag = "123"
key = "c"
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)

運行並觀察結果:

$ py new_caesar.py 
LOWERCASE_OFFSET 97
ALPHABET abcdefghijklmnop
plain,c =  123 1
binary =  00110001 1
enc  db
---
plain,c =  123 2
binary =  00110010 2
enc  dbdc
---
plain,c =  123 3
binary =  00110011 3
enc  dbdcdd
---
fdfeff

簡單來說,當 flag = “123” , key = “c” (位移 2 位)時,可得密文 fdfeff,而且是一個明文對應二個密文,即 1 就是 fd ,2 就是 fe 的關係。

接下來就是分析加密時的步驟,題目所說的自創加密法就是這 5 個步驟。雖然步驟多但都很容易。

  1. ASCII to Binary(字母轉 2 進位)
  2. 切割
  3. Binary to Decimal (2 進位轉 10 進位)
  4. 對應字母表
  5. 位移

我以明文 1 到密文 fd 來做說明

1. ASCII to Binary

經典的從字元轉為二進位

ASCII Binary
1 => 00110001

2.切割

00110001

中間切開,就有了前後 2 段

0011 | 0001

3.Binary to Decimal

0011 0001
3 1

4.對應字母表

a b c d e f g h i j k l m
0 1 2 3 4 5 6 7 8 9 10 11 12
n o p q r s t u v w x y z
13 14 15 16 17 18 19 20 21 22 23 24 25

所以

3 1
d b

5.位移

由於 key = “c” (位移 2 位)

key 位移前 位移後
a db db
b db ec
c db fd

最終就從原來明文的 “1”,加密成了 “fd”,而明文就是以這 5 個步驟依次進行加密的。

其實前 4 個步驟,與其說是加密過程,不如說是”編碼”過程,單純的進行了混淆。我們唯一不知道的訊息,是第 5 步驟進行時,位移了多少次。

但通過讀原始碼得知,相比起正常的凱撒密碼的 26 種位移,它只有 16 種位移,基本上到這裡,只要把 16 種組合都列舉也能找到 flag。

但還是有知道做了多少位移的方法。

只要對 flag 和 key 做深層次一點改動:

flag = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
key = "a" # 不進行位移

加密後便是:

ebecedeeefegeheiejekelemeneoepfafbfcfdfefffgfhfifjfkgbgcgdgegfggghgigjgkglgmgngogphahbhchdhehfhghhhihjhk

仔細觀察可發現,密文的格式存在某種規律,大部分都是 e, f, g, h 開頭

我們再回頭看看原始的密文

mlnklfnknljflfmhjimkmhjhmljhjomhmmjkjpmmjmjkjpjojgjmjpjojojnjojmmkmlmijimhjmmj

大部分是 j, l, m, n 等開頭

位移值相差 6 ~ 7 左右(最終位移值為 6,key = “g”)

接下來就是寫一個逆向上面加密 5 個步驟的 python 程式即可:

i = 0
flag = ""
c_text = "mlnklfnknljflfmhjimkmhjhmljhjomhmmjkjpmmjmjkjpjojgjmjpjojojnjojmmkmlmijimhjmmj"
for k in range(int(len(c_text)/2)):
    a = c_text[i]
    b = c_text[i+1]
    a = chr(((ord(a) - ord('a') + 10) % 16 + 1) + ord('a') - 1)
    #a = chr(ord(a) - 6)
    #b = chr(ord(b) - 6)
    b = chr(((ord(b) - ord('a') + 10) % 16 + 1) + ord('a') - 1)
    i += 2

    a = ord(a)-ord("a")
    b = ord(b)-ord("a")

    a = "{:04b}".format(int(a))
    b = "{:04b}".format(int(b))
    c = a + b
    #c = "01100101"
    ans = chr(int(c,2))
    flag += ans
    
print(flag)

不過當密文改變了,就要重新推算位移,所以也能像下面這樣,多加一行 foor loop,把 16 種的位移都打印出來:

i = 0
flag = ""
c_text = "apbopjbobpnjpjnmnnnmnlnbamnpnononpnaaaamnlnkapndnkncamnpapncnbannaapncndnlnpna"

for j in range(1,17):
    for k in range(int(len(c_text)/2)):
        a = c_text[i]
        b = c_text[i+1]
        a = chr(((ord(a) - ord('a') + j) % 16 + 1) + ord('a') - 1)
        b = chr(((ord(b) - ord('a') + j) % 16 + 1) + ord('a') - 1)
        i += 2

        a = ord(a)-ord("a")
        b = ord(b)-ord("a")

        a = "{:04b}".format(int(a))
        b = "{:04b}".format(int(b))
        c = a + b
        ans = chr(int(c,2))
        flag += ans

    print(j , "= ",flag)
    i = 0
    flag = ""

Mini RSA (70 points)

What happens if you have a small exponent? There is a twist though, we padded the plaintext so that (M ** e) is just barely larger than N. Let’s decrypt this: ciphertext

Hint 1 : RSA tutorial Hint 2 : How could having too small of an e affect the security of this key? Hint 3 : Make sure you don’t lose precision, the numbers are pretty big (besides the e value) Hint 4 : You shouldn’t have to make too many guesses Hint 5 : pico is in the flag, but not at the beginning

ciphertext:

N: 1615765684321463054078226051959887884233678317734892901740763321135213636796075462401950274602405095138589898087428337758445013281488966866073355710771864671726991918706558071231266976427184673800225254531695928541272546385146495736420261815693810544589811104967829354461491178200126099661909654163542661541699404839644035177445092988952614918424317082380174383819025585076206641993479326576180793544321194357018916215113009742654408597083724508169216182008449693917227497813165444372201517541788989925461711067825681947947471001390843774746442699739386923285801022685451221261010798837646928092277556198145662924691803032880040492762442561497760689933601781401617086600593482127465655390841361154025890679757514060456103104199255917164678161972735858939464790960448345988941481499050248673128656508055285037090026439683847266536283160142071643015434813473463469733112182328678706702116054036618277506997666534567846763938692335069955755244438415377933440029498378955355877502743215305768814857864433151287
e: 3

ciphertext (c): 1220012318588871886132524757898884422174534558055593713309088304910273991073554732659977133980685370899257850121970812405700793710546674062154237544840177616746805668666317481140872605653768484867292138139949076102907399831998827567645230986345455915692863094364797526497302082734955903755050638155202890599808146956044568639690002921620304969196755223769438221859424275683828638207433071955615349052424040706261639770492033970498727183446507482899334169592311953247661557664109356372049286283480939368007035616954029177541731719684026988849403756133033533171081378815289443019437298879607294287249591634702823432448559878065453908423094452047188125358790554039587941488937855941604809869090304206028751113018999782990033393577325766685647733181521675994939066814158759362046052998582186178682593597175186539419118605277037256659707217066953121398700583644564201414551200278389319378027058801216150663695102005048597466358061508725332471930736629781191567057009302022382219283560795941554288119544255055962

知識點:RSA加密、小公鑰/低加密指數攻擊

公鑰與私鑰的產生:

  1. 隨機選擇兩個不同大質數 $p$ 和 $q$ ,計算 $N = p × q$
  2. 根據歐拉函數, 求得 $r=φ(N)=φ(p)φ(q)=(p−1)(q−1)$
  3. 選擇一個小於 $r$ 的整數 $e$ , 使 $e$ 和 $r$ 互質。並求得 $e$ 關於 $r$ 的模反元素, 命名為 $d (ed≡1(modr))$
  4. 將 $p$ 和 $q$ 的記錄銷毀。

此時(N, e)為公鑰, (N, d)為私鑰。

解答:

這是一道經典的 RSA 題目,關於 RSA 的原理我就不多贅述,RSA 加密法直至現時(2021年),仍然作為主流的加密法之一,可見其算法的可靠性和安全性。所以還是值得花時間去消化了解

但不正確的使用場景、重要信息的洩露或者是數值大小設計不當等,都會帶來安全問題,還有其攻擊的方法。

兩篇轉載關於 RSA 不同漏洞的情景: CTF中常见的RSA相关问题总结 CTF—RSA解密学习指南

這裡利用到的漏洞是小公鑰/低加密指數,也就是 e = 2 或 e = 3

公鑰 e 很小,明文 m 也不大的話,於是m ^ e = k * n + c 中的 k 值很小甚至為 0,爆破 k 或直接開三次方即可。

import gmpy2,binascii,libnum,time
n = 1615765684321463054078226051959887884233678317734892901740763321135213636796075462401950274602405095138589898087428337758445013281488966866073355710771864671726991918706558071231266976427184673800225254531695928541272546385146495736420261815693810544589811104967829354461491178200126099661909654163542661541699404839644035177445092988952614918424317082380174383819025585076206641993479326576180793544321194357018916215113009742654408597083724508169216182008449693917227497813165444372201517541788989925461711067825681947947471001390843774746442699739386923285801022685451221261010798837646928092277556198145662924691803032880040492762442561497760689933601781401617086600593482127465655390841361154025890679757514060456103104199255917164678161972735858939464790960448345988941481499050248673128656508055285037090026439683847266536283160142071643015434813473463469733112182328678706702116054036618277506997666534567846763938692335069955755244438415377933440029498378955355877502743215305768814857864433151287
e = 3
c = 1220012318588871886132524757898884422174534558055593713309088304910273991073554732659977133980685370899257850121970812405700793710546674062154237544840177616746805668666317481140872605653768484867292138139949076102907399831998827567645230986345455915692863094364797526497302082734955903755050638155202890599808146956044568639690002921620304969196755223769438221859424275683828638207433071955615349052424040706261639770492033970498727183446507482899334169592311953247661557664109356372049286283480939368007035616954029177541731719684026988849403756133033533171081378815289443019437298879607294287249591634702823432448559878065453908423094452047188125358790554039587941488937855941604809869090304206028751113018999782990033393577325766685647733181521675994939066814158759362046052998582186178682593597175186539419118605277037256659707217066953121398700583644564201414551200278389319378027058801216150663695102005048597466358061508725332471930736629781191567057009302022382219283560795941554288119544255055962
res = 0

for k in range(200000000):
	if gmpy2.iroot(c+n*k,3)[1]==1:
		res=gmpy2.iroot(c+n*k,3)[0]
		print(k,res)
		print(libnum.n2s(int(res)))
		break

Dachshund Attacks (80 points)

What if d is too small? Connect with nc mercury.picoctf.net 37455.

Hint : What do you think about my pet? dachshund.jpg

知識點:低解密指數(d 太小)攻擊Wiener’s attack

首先 netcat 連線看看:

$ nc mercury.picoctf.net 37455
Welcome to my RSA challenge!
e: 81229431932405069076115541286401461443494262822135658283742292897410925764495502689451059315212238547938663797948023035990811381899591212921183487360416649701773568973719409413970364821014306572579550625402194135767284307689116463076867207148137667749321676903262731528419046683614746969287791511567819422311
n: 103070577929726253631344410352692529019028899492417679170030712588726916233260956307828281366719578148149337963676063478754302654908868697186141139935785807607477859007299550759696360710761593462350508280204189428161004900112381330220076890145703597040417081075805402174962296795435711459649783056881789490351
c: 91695657020488911546514984124491767203636048945190356702572340308491541333231892208913410842759055058532538326429698845388689133318492812505642888037888994704290866466325628211196801570268671101701776860750050743516334024242682186605601842248508533894710903871285318149826578248317551453724632008257499100559

有了 e, n, c,第一個反應是 n 能不能分解出 2 個大質數 p 和 q。簡單的試一個是不行(n 約位 309 位數)

再來看一下 hint 裡的 dachshund.jpg

dachshund

單純的一張臘腸狗圖片(英文名:Wiener dog),所以這個提示的意思是 Wiener’s attack (太難聯想了我是後來才知道)

簡單來說,這題的漏洞是發現加密指數 c 是一個很大的值(接近 n),所以便可以採用 Wiener’s attack。即利用 c (公鑰)很大時、d (私鑰)的數值就會相當小的性質。

Wiener’s attack 重點:透過數值很大的 c 和 n,去求出數值較小的 d。

解法一:

1.在 github 下載 rsa-wiener-attack 工具 :

$ git clone https://github.com/pablocelayes/rsa-wiener-attack

2.在同一個資料夾下建立 solve.py

import libnum
from Crypto.PublicKey import RSA
import ContinuedFractions, Arithmetic

def wiener_hack(e, n):
    # firstly git clone https://github.com/pablocelayes/rsa-wiener-attack.git !
    frac = ContinuedFractions.rational_to_contfrac(e, n)
    convergents = ContinuedFractions.convergents_from_contfrac(frac)
    for (k, d) in convergents:
        if k != 0 and (e * d - 1) % k == 0:
            phi = (e * d - 1) // k
            s = n - phi + 1
            discr = s * s - 4 * n
            if (discr >= 0):
                t = Arithmetic.is_perfect_square(discr)
                if t != -1 and (s + t) % 2 == 0:
                    print("Hacked!")
                    return d
    return False

n = 103070577929726253631344410352692529019028899492417679170030712588726916233260956307828281366719578148149337963676063478754302654908868697186141139935785807607477859007299550759696360710761593462350508280204189428161004900112381330220076890145703597040417081075805402174962296795435711459649783056881789490351
e = 81229431932405069076115541286401461443494262822135658283742292897410925764495502689451059315212238547938663797948023035990811381899591212921183487360416649701773568973719409413970364821014306572579550625402194135767284307689116463076867207148137667749321676903262731528419046683614746969287791511567819422311
d = wiener_hack(e, n)
print("d = ", d)

c = 91695657020488911546514984124491767203636048945190356702572340308491541333231892208913410842759055058532538326429698845388689133318492812505642888037888994704290866466325628211196801570268671101701776860750050743516334024242682186605601842248508533894710903871285318149826578248317551453724632008257499100559
m = libnum.n2s(pow(c, d, n))
print("flag = ", m)

3.運行結果:

$ py solve.py 
Hacked!
d =  7396779914536059503104861975179688509330809171082098841462241492465749371303
flag =  b'picoCTF{proving_wiener_3878674}'

解法二:

發現了更簡潔的方法:

import libnum
import owiener

n = 103070577929726253631344410352692529019028899492417679170030712588726916233260956307828281366719578148149337963676063478754302654908868697186141139935785807607477859007299550759696360710761593462350508280204189428161004900112381330220076890145703597040417081075805402174962296795435711459649783056881789490351
e = 81229431932405069076115541286401461443494262822135658283742292897410925764495502689451059315212238547938663797948023035990811381899591212921183487360416649701773568973719409413970364821014306572579550625402194135767284307689116463076867207148137667749321676903262731528419046683614746969287791511567819422311
d = owiener.attack(e, n)
print("d = ", d)

c = 91695657020488911546514984124491767203636048945190356702572340308491541333231892208913410842759055058532538326429698845388689133318492812505642888037888994704290866466325628211196801570268671101701776860750050743516334024242682186605601842248508533894710903871285318149826578248317551453724632008257499100559
m = libnum.n2s(pow(c, d, n))
print("flag = ", m)

運行結果:

$ py solve.py 
d =  7396779914536059503104861975179688509330809171082098841462241492465749371303
flag =  b'picoCTF{proving_wiener_3878674}'

No Padding, No Problem (90 points)

Oracles can be your best friend, they will decrypt anything, except the flag’s ciphertext. How will you break it? Connect with nc mercury.picoctf.net 10333.

Hint : What can you do with a different pair of ciphertext and plaintext? What if it is not so different after all…

知識點:RSA 基礎知識、 選擇密文攻擊(chosen-ciphertext attack)

其實我挺喜歡這一題的,因為這題幾乎不需要任何編程技巧,反而是回歸根本,呈現最基本的 RSA 的加密方法。

這題需要的是對於 RSA 的原理理解,然後加入數學方法便能求解。

一起快速重溫一下 RSA

公鑰與私鑰的產生:

  1. 隨機選擇兩個不同大質數 $p$ 和 $q$ ,計算 $N = p × q$
  2. 根據歐拉函數, 求得 $r=φ(N)=φ(p)φ(q)=(p−1)(q−1)$
  3. 選擇一個小於 $r$ 的整數 $e$ , 使 $e$ 和 $r$ 互質。並求得 $e$ 關於 $r$ 的模反元素, 命名為 $d$。$(ed ≡ 1(mod r))$
  4. 將 $p$ 和 $q$ 的記錄銷毀。

此時(N, e)為公鑰, (N, d)為私鑰。

看看題目 netcat 連線:

$ nc mercury.picoctf.net 10333
Welcome to the Padding Oracle Challenge
This oracle will take anything you give it and decrypt using RSA. It will not accept the ciphertext with the secret message... Good Luck!


n: 80907234819751598450965859006417228903552848320000151229358773723906600010590329945580259683924752773623944150540498179913209605194486672774037644980975512051487394310916270477443161474324640529352233637411212810764137519808445028296132094562441268361065288082594907742535032356974798089272311417731492113731
e: 65537
ciphertext: 25204502408487966010655456273516993151073917896222636517424387606673238913312360141811214573032185873860734765570121366810599506232598424739814451672038465193294913208095990311469990787659770462592938523559643774917896615197883914322584191904706560530420584057209105697638062200626531758852462759158892392770


Give me ciphertext to decrypt: 25204502408487966010655456273516993151073917896222636517424387606673238913312360141811214573032185873860734765570121366810599506232598424739814451672038465193294913208095990311469990787659770462592938523559643774917896615197883914322584191904706560530420584057209105697638062200626531758852462759158892392770
Will not decrypt the ciphertext. Try Again

題目提供了”解密機”功能,也就是計算 $c^d (mod N)$ 的功能,但輸入的不能是原本的密文 $c$ ,所以這裡需要想出一個繞過它的辦法。透過輸入其他的密文 $c’$,依然能讓解密的方法。

1. 繞過的方法很簡單:

$c’ = k^e * c$ ($k$ 為一個自己建立的常數)

沒錯!! 就是這樣就可以

當然,這樣只能求出 $m’$ 因為 $m’ = (c’)^d (mod N)$

2. $m’$ 和 $m$ 的關係

$m’ = (c’)^d (mod N) = (k^e * c)^d (mod N) = k^{ed} * c^d (mod N) = km$

驚不驚喜、意不意外

只要把 $m’$ 除了常數 $k$ 就搞定

不過可以為了方便計算,可以取 $k = -1$ ,因為 $e = 65537$,所以 $c’ = N - c$,即 $m = N - m’$ (這裡參考自 廢文集中區的 picoCTF 2021 WriteUps

3. 開始解題:

先丟 $c’$ ($c’ = N - c$),然後得到 $m’$。

$ nc mercury.picoctf.net 10333
Welcome to the Padding Oracle Challenge
This oracle will take anything you give it and decrypt using RSA. It will not accept the ciphertext with the secret message... Good Luck!


n: 65603914925786331293577648481402202678250626513848986044457642475308642861873586842670388931330750571052948618643253233772154156913731489543684218025558918455114441784061185206094899732431499927247880349342257764693224996052797387557859812110433828120905864586326066969621171202946481099215943392076001264707
e: 65537
ciphertext: 47411806268133900337506328172776367779944539930078863118033158377243126143845017475721857485143521460334453112963781927593421399358539792331690622115334039612366261703359477342503768112265227075951214355328749129549846148891388170319418225557458741927861048192715782815581775206651273002654069905263058174416


Give me ciphertext to decrypt: 18192108657652430956071320308625834898306086583770122926424484098065516718028569366948531446187229110718495505679471306178732757555191697211993595910224878842748180080701707863591131620166272851296665994013508635143378847161409217238441586552975086193044816393610284154039395996295208096561873486812943090291
Here you go: 6560391492578633129357764848140220267825062651384898604445764247530864286187358684267038893133075057105294861864325323377215415691373148954368421802555891845511444

拿到了 $m’$,求出 $m$。 $(m = N - m’)$

import libnum

N = 65603914925786331293577648481402202678250626513848986044457642475308642861873586842670388931330750571052948618643253233772154156913731489543684218025558918455114441784061185206094899732431499927247880349342257764693224996052797387557859812110433828120905864586326066969621171202946481099215943392076001264707
mm = 65603914925786331293577648481402202678250626513848986044457642475308642861873586842670388931330750571052948618643253233772154156913731489543684218025558918455114441784061185206094899732141224897052030309868801146325769110982831638706581735353690107674202550068924707702298402165477229653831516752238352666310 
m = libnum.n2s(N - mm)
print(m)
$ py solve.py 
b'picoCTF{m4yb3_Th0se_m3s54g3s_4r3_difurrent_1772735}'

Easy1 (100 points)

Description The one time pad can be cryptographically secure, but not when you know the key. Can you solve this? We’ve given you the encrypted flag, key, and a table to help UFJKXQZQUNB with the key of SOLVECRYPTO. Can you use this table to solve it?.

Hints 1 : Submit your answer in our flag format. For example, if your answer was ‘hello’, you would submit ‘picoCTF{HELLO}’ as the flag.

Hints 2 :Please use all caps for the message.

table內容:

    A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 
+----------------------------------------------------
A | A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
B | B C D E F G H I J K L M N O P Q R S T U V W X Y Z A
C | C D E F G H I J K L M N O P Q R S T U V W X Y Z A B
D | D E F G H I J K L M N O P Q R S T U V W X Y Z A B C
E | E F G H I J K L M N O P Q R S T U V W X Y Z A B C D
F | F G H I J K L M N O P Q R S T U V W X Y Z A B C D E
G | G H I J K L M N O P Q R S T U V W X Y Z A B C D E F
H | H I J K L M N O P Q R S T U V W X Y Z A B C D E F G
I | I J K L M N O P Q R S T U V W X Y Z A B C D E F G H
J | J K L M N O P Q R S T U V W X Y Z A B C D E F G H I
K | K L M N O P Q R S T U V W X Y Z A B C D E F G H I J
L | L M N O P Q R S T U V W X Y Z A B C D E F G H I J K
M | M N O P Q R S T U V W X Y Z A B C D E F G H I J K L
N | N O P Q R S T U V W X Y Z A B C D E F G H I J K L M
O | O P Q R S T U V W X Y Z A B C D E F G H I J K L M N
P | P Q R S T U V W X Y Z A B C D E F G H I J K L M N O
Q | Q R S T U V W X Y Z A B C D E F G H I J K L M N O P
R | R S T U V W X Y Z A B C D E F G H I J K L M N O P Q
S | S T U V W X Y Z A B C D E F G H I J K L M N O P Q R
T | T U V W X Y Z A B C D E F G H I J K L M N O P Q R S
U | U V W X Y Z A B C D E F G H I J K L M N O P Q R S T
V | V W X Y Z A B C D E F G H I J K L M N O P Q R S T U
W | W X Y Z A B C D E F G H I J K L M N O P Q R S T U V
X | X Y Z A B C D E F G H I J K L M N O P Q R S T U V W
Y | Y Z A B C D E F G H I J K L M N O P Q R S T U V W X
Z | Z A B C D E F G H I J K L M N O P Q R S T U V W X Y

知識點 :

看到上面的表格就要聯想到維吉尼亞密碼 (Vigenere)

密文 = UFJKXQZQUNB

KEY = SOLVECRYPTO

密碼和 KEY 的長度都是一樣 11 個位

可以拆想成明文第一個字母 未知 偏移了 S 位,得出密文 U,通過上面表格,得知明文第一個字母 未知C,以此類推,得出明文

明文 = CRYPTOISFUN

注意這題內容要全大寫,構成 flag picoCTF{CRYPTOISFUN}


13 (100 points)

Description Cryptography can be easy, do you know what ROT13 is? cvpbPGS{abg_gbb_onq_bs_n_ceboyrz}

Hints : This can be solved online if you don’t want to do it by hand!

關鍵詞 : ROT13

英文字母共有 26 個,如果分成 2 組,每組就有 13 個,可以兩兩替換。

480px-ROT13_table_with_example svg

不必手算,直接 Google 找 ROT13 decode 的網站就出答案

構成 flag picoCTF{not_too_bad_of_a_problem}


caesar (100 points)

Decrypt this message.

Hints : caesar cipher tutorial

知識點 :

凱撒密碼 (Caesar cipher)

如果一串字原本是 ABC,偏移量是 3 的話,就會變成 DEF

簡單來說,就是進行每個字母的偏移,

A -> B -> C -> D

B -> C -> D -> E

C -> D -> E -> F

下載後的 ciphertext:

picoCTF{ynkooejcpdanqxeykjrbdofgkq}

我們雖然不知道這串文字到底偏移量是多少位,但我們知道最多就 26 種偏移組合。

把其中字串 ynkooejcpdanqxeykjrbdofgkq 透過網站 https://www.dcode.fr/caesar-cipher 做 caescar Cipher 的解密,它會把 26 種不同的組合都列出,並把最優解 crossingtherubiconvfhsjkou 放在首位

構成 flag picoCTF{crossingtherubiconvfhsjkou}


Pixelated (100 points)

I have these 2 images, can you make a flag out of them? scrambled1.png scrambled2.png

Hint 1 : https://en.wikipedia.org/wiki/Visual_cryptography

Hint 2 : Think of different ways you can “stack” images

scrambled1.png: scrambled1

scrambled2.png: scrambled2

知識點 : 可視密碼

一開始看好像是 2 張沒有任何意義的點陣圖,但提示給了我們維基百科的連結,只要把 2 張圖”重疊”便可得到訊息,效果如下(取自維基百科): Visual_crypto_animation_demo

方法一 :

使用 stegsolve.jar

安裝方法:

wget http://www.caesum.com/handbook/Stegsolve.jar -O stegsolve.jar
chmod +x stegsolve.jar

使用方法:

java -jar stegsolve.jar

2021-10-28_23-32

方法二 :

$ cat solve.py
# import Image
from PIL import Image

# open both photos
i1 = Image.open('scrambled1.png')
i2 = Image.open('scrambled2.png')

# get width and height
width1, height1 = i1.size

# open new image
i3 = Image.new('RGB', (width1, height1))

# load the pixels
pixels = i3.load()

# loop through all pixels
for i in range(width1):
	for j in range(height1):
        # xor the values
		x = i1.getpixel((i,j))[0] ^ i2.getpixel((i,j))[0]
		y = i1.getpixel((i,j))[1] ^ i2.getpixel((i,j))[1]
		z = i1.getpixel((i,j))[2] ^ i2.getpixel((i,j))[2]

        # if all white then convert to black
		if (x,y,z) == (255,255,255):
			(x,y,z) = (0,0,0)

        # put the new pixels in place
		i3.putpixel((i,j), (x,y,z))

# save the image
i3.save("test.png", "PNG")

test.png : test

方法三 :

from PIL import Image
import numpy as np
import os

file_names = ["scrambled1.png", "scrambled2.png"]
img_data = [np.asarray(Image.open(f'{name}')) for name in file_names]

data = img_data[0].copy() + img_data[1].copy()

new_image = Image.fromarray(data)
new_image.save("out.png", "PNG")

out.png: out


spelling-quiz (100 points)

I found the flag, but my brother wrote a program to encrypt all his text files. He has a spelling quiz study guide too, but I don’t know if that helps.

public.zip

public:

encrypt.py

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)

flag.txt

brcfxba_vfr_mid_hosbrm_iprc_exa_hoav_vwcrm

study-guide.txt

1 gocnfwnwtr
2 sxlyrxaic
3 dcrrtfrxcv
4 uxbvwavcq
5 lwvicwtiwm
6 pwtmwnxvicq
7 avingciisa
...略...
...略...
...略...
272538 pxcwnxvrm
272539 rliwdtrc
272540 ibfwaxocoa
272541 urlvlwtr
272542 nfxbxccxk
272543 nwccxvoloa

知識點: 字母頻率

(取自維基百科)

首先看到 3 個不同的檔案,如果我們先看 encrypt.py,大概能夠了解它對一個明文加密。其中這 3 行告訴了我們一些內容:

alphabet = list('abcdefghijklmnopqrstuvwxyz')
random.shuffle(shuffled := alphabet[:])
dictionary = dict(zip(alphabet, shuffled))

其實如果沒看懂也沒關係,我們可以直接用 terminal 執行:

$ python
Python 3.9.7 (default, Oct 10 2021, 15:13:22)
[GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import random
>>> alphabet = list('abcdefghijklmnopqrstuvwxyz')
>>> random.shuffle(shuffled := alphabet[:])
>>> dictionary = dict(zip(alphabet, shuffled))
>>> print(alphabet)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
>>> print(shuffled)
['o', 'r', 's', 'w', 'n', 'i', 'k', 't', 'z', 'f', 'q', 'c', 'y', 'v', 'p', 'l', 'j', 'g', 'm', 'b', 'x', 'e', 'd', 'h', 'u', 'a']
>>> print(dictionary)
{'a': 'o', 'b': 'r', 'c': 's', 'd': 'w', 'e': 'n', 'f': 'i', 'g': 'k', 'h': 't', 'i': 'z', 'j': 'f', 'k': 'q', 'l': 'c', 'm': 'y', 'n': 'v', 'o': 'p', 'p': 'l', 'q': 'j', 'r': 'g', 's': 'm', 't': 'b', 'u': 'x', 'v': 'e', 'w': 'd', 'x': 'h', 'y': 'u', 'z': 'a'}
>>>

從這裡可以看到,26 個英文字母都一一做了轉換,比如說 a -> o ,b -> r 這樣,當然這只是我自己例子裡的轉換,實際上我們仍然不知道題目裡的 26 個字母分別被替換成哪些字母。

再來看 study-guide.txt,可以看出這裡的單詞似乎都是經過同樣的加密(字母替換),但幸運的是這裡面的單詞數量高達誇張的 272543 行,這樣大的數量足夠我們針對字母出現的頻率進行分析。

但早就有大神做出這樣的工具,字頻分析的線上網站 quipqiup

2021-11-10_11-27

把 flag.txt 裡的加密內容丟上去,它能直接把解答給跑出來。

構成 flag picoCTF{perhaps_the_dog_jumped_over_was_just_tired}


title: (密碼學篇) 二 key: 20211005 tags: CTF PicoCTF Writeup Hacker Cryptography mathjax: true mathjax_autoNumber: true


Play Nice (110 points)

Not all ancient ciphers were so bad… The flag is not in standard format. nc mercury.picoctf.net 40742 playfair.py

playfair.py:

#!/usr/bin/python3 -u
import signal

SQUARE_SIZE = 6


def generate_square(alphabet):
	assert len(alphabet) == pow(SQUARE_SIZE, 2)
	matrix = []
	for i, letter in enumerate(alphabet):
		if i % SQUARE_SIZE == 0:
			row = []
		row.append(letter)
		if i % SQUARE_SIZE == (SQUARE_SIZE - 1):
			matrix.append(row)
	return matrix

def get_index(letter, matrix):
	for row in range(SQUARE_SIZE):
		for col in range(SQUARE_SIZE):
			if matrix[row][col] == letter:
				return (row, col)
	print("letter not found in matrix.")
	exit()

def encrypt_pair(pair, matrix):
	p1 = get_index(pair[0], matrix)
	p2 = get_index(pair[1], matrix)

	if p1[0] == p2[0]:
		return matrix[p1[0]][(p1[1] + 1)  % SQUARE_SIZE] + matrix[p2[0]][(p2[1] + 1)  % SQUARE_SIZE]
	elif p1[1] == p2[1]:
		return matrix[(p1[0] + 1)  % SQUARE_SIZE][p1[1]] + matrix[(p2[0] + 1)  % SQUARE_SIZE][p2[1]]
	else:
		return matrix[p1[0]][p2[1]] + matrix[p2[0]][p1[1]]

def encrypt_string(s, matrix):
	result = ""
	if len(s) % 2 == 0:
		plain = s
	else:
		plain = s + "irlgektq8ayfp5zu037nov1m9xbc64shwjd2"[0]
	for i in range(0, len(plain), 2):
		result += encrypt_pair(plain[i:i + 2], matrix)
	return result

alphabet = open("key").read().rstrip()
m = generate_square(alphabet)
msg = open("msg").read().rstrip()
enc_msg = encrypt_string(msg, m)
print("Here is the alphabet: {}\nHere is the encrypted message: {}".format(alphabet, enc_msg))
signal.alarm(18)
resp = input("What is the plaintext message? ").rstrip()
if resp and resp == msg:
	print("Congratulations! Here's the flag: {}".format(open("flag").read()))

# https://en.wikipedia.org/wiki/Playfair_cipher

知識點: 波雷費密碼

這邊簡述一下波雷費密碼(Playfair_cipher) 的加密過程:

(以下內容部份節錄自維基百科)

1. 製作加密用的”表格”

330px-Playfair_Cipher_building_grid_omitted_letters

而表格的製作方式可以按照 Wiki 上的做,或者自己隨機製作。

2. 將所加密的訊息分成每 2 個字為一組

例如 : hello world

分組後 : he ll ow or ld

3. 加密轉換

轉加的方式有 3 種

A. 原本的兩個字在同一橫列

Playfair_Cipher_10_EX_to_XD

B. 原本的兩個字在同一直排

Playfair_Cipher_02_DE_to_OD

C. 原本的兩個字不在同列或同排

Playfair_Cipher_03_TH_to_ZB

解法:

既然知道了加密的 3 種模式,那仔細想想解密的方法,解密也只需要對 A. 原本的兩個字在同一橫列B. 原本的兩個字在同一直排反方向 操作便行

題目已經給出了加密用的”表格” ,雖然是 6 x 6 的,但原理一樣。

payload: 其實也只是把原本的 code 改一點點而已

from pwn import *

SQUARE_SIZE = 6

def generate_square(alphabet):
    assert len(alphabet) == pow(SQUARE_SIZE, 2)
    matrix = []
    for i, letter in enumerate(alphabet):
        if i % SQUARE_SIZE == 0:
            row = []
        row.append(letter)
        if i % SQUARE_SIZE == (SQUARE_SIZE - 1):
            matrix.append(row)
    return matrix

def get_index(letter, matrix):
    for row in range(SQUARE_SIZE):
        for col in range(SQUARE_SIZE):
            if matrix[row][col] == letter:
                return (row, col)
    print("letter not found in matrix.")
    exit()

def decrypt_pair(pair, matrix):
    p1 = get_index(pair[0], matrix)
    p2 = get_index(pair[1], matrix)

    if p1[0] == p2[0]:
        return matrix[p1[0]][(p1[1] - 1)  % SQUARE_SIZE] + matrix[p2[0]][(p2[1] - 1)  % SQUARE_SIZE]
    elif p1[1] == p2[1]:
        return matrix[(p1[0] - 1)  % SQUARE_SIZE][p1[1]] + matrix[(p2[0] - 1)  % SQUARE_SIZE][p2[1]]
    else:
        return matrix[p1[0]][p2[1]] + matrix[p2[0]][p1[1]]

def decrypt_string(s, matrix):
    result = ""
    if len(s) % 2 == 0:
        plain = s
    else:
        plain = s + "irlgektq8ayfp5zu037nov1m9xbc64shwjd2"[0]
    for i in range(0, len(plain), 2):
        tmp = decrypt_pair(plain[i:i + 2], matrix)
        result += tmp
    return result

p = remote("mercury.picoctf.net",40742)
p.recvuntil("Here is the alphabet:")
alphabet = p.recvline().strip().decode('utf-8')
p.recvuntil("Here is the encrypted message:")
enc_msg = p.recvline().strip().decode('utf-8')
matrix = generate_square(alphabet)
msg = decrypt_string(enc_msg, matrix)
p.recvuntil("What is the plaintext message? ")
p.sendline(msg)
p.recvuntil("Congratulations! Here's the flag:")
flag = p.recvline().strip().decode('utf-8')
print("flag =",flag)

# https://en.wikipedia.org/wiki/Playfair_cipher

Double DES (120 points)

I wanted an encryption service that’s more secure than regular DES, but not as slow as 3DES… The flag is not in standard format. nc mercury.picoctf.net 37751 ddes.py

Hint : How large is the keyspace?

ddes.py:

#!/usr/bin/python3 -u
from Crypto.Cipher import DES
import binascii
import itertools
import random
import string


def pad(msg):
    block_len = 8
    over = len(msg) % block_len
    pad = block_len - over
    return (msg + " " * pad).encode()

def generate_key():
    return pad("".join(random.choice(string.digits) for _ in range(6)))


FLAG = open("flag").read().rstrip()
KEY1 = generate_key()
KEY2 = generate_key()


def get_input():
    try:
        res = binascii.unhexlify(input("What data would you like to encrypt? ").rstrip()).decode()
    except:
        res = None
    return res

def double_encrypt(m):
    msg = pad(m)

    cipher1 = DES.new(KEY1, DES.MODE_ECB)
    enc_msg = cipher1.encrypt(msg)
    cipher2 = DES.new(KEY2, DES.MODE_ECB)
    return binascii.hexlify(cipher2.encrypt(enc_msg)).decode()


print("Here is the flag:")
print(double_encrypt(FLAG))

while True:
    inputs = get_input()
    if inputs:
        print(double_encrypt(inputs))
    else:
        print("Invalid input.")

知識點: DES 原理Meet-in-the-middle attack

仔細讀了一下原始碼,就發現 flag 做了 2 層 DES 加密:

即 $C = DES(DES(M))$

題目也提供了我們可以輸入明文 m 和產成密文 c 的方式

因此解法便是 brute force 兩層 DES 加密時所用的 KEY1 和 KEY2

幸運的就是 KEY1 和 KEY2 的長度都只有 6 位 (000000 ~ 999999)

所以暴力搜尋范圍是 $10^6 * 10^6$ (可用 Meet-in-the-middle attack 降低搜尋複雜度)

解法

先 netcat 到伺服器:

$ nc mercury.picoctf.net 37751
Here is the flag:
bbdf04d0323928e65e182c8be847c7c35a901ed5275d4011f364afd0c1be8d447ea52e014babfe25
What data would you like to encrypt? 12341234
4b731a66758abcfe

這樣我們有 3 個數值:

  1. $DES(DES(flag))$ = bbdf04d0323928e65e182c8be847c7c35a901ed5275d4011f364afd0c1be8d447ea52e014babfe25

  2. 已知明文 M = 12341234

  3. 已知密文 C = 4b731a66758abcfe ($DES(DES(M))$)

現在便能用 M 和 C 去暴力搜得 KEY1 和 KEY2,然後再用 KEY1、KEY2 解出 FLAG

import binascii
from Crypto.Cipher import DES
from tqdm import tqdm

padding = "  "
encrypted_flag = binascii.unhexlify("6f745ccee635f76746be185541b9f9c046b8d707f93d0522e2325fb041c59ec7bbbaa818d7c51381")


def pad(msg):
    block_len = 8
    over = len(msg) % block_len
    pad = block_len - over
    return (msg + " " * pad).encode()

custom_known_text = pad(binascii.unhexlify("12341234").decode())
custom_ciphertext = binascii.unhexlify("4b731a66758abcfe")

encrypt_table = {}
for key in tqdm(range(999999), desc="Bruteforcing 1st Key"):
    key = (f"{key:06}" + padding).encode()
    cipher = DES.new(key, DES.MODE_ECB)
    encrypted_custom = cipher.encrypt(custom_known_text)
    encrypt_table[encrypted_custom] = key

decrypt_table = {}
for key in tqdm(range(999999), desc="Bruteforcing 2nd Key"):
    key = (f"{key:06}" + padding).encode()
    cipher = DES.new(key, DES.MODE_ECB)
    decrypted_custom = cipher.decrypt(custom_ciphertext)
    decrypt_table[decrypted_custom] = key

print("Finding Key Intersection...")
encrypt_table_set = set(encrypt_table.keys())
decrypt_table_set = set(decrypt_table.keys())
for encrypt_decrypt_value in encrypt_table_set.intersection(decrypt_table_set):
    encrypt_key = encrypt_table[encrypt_decrypt_value]
    decrypt_key = decrypt_table[encrypt_decrypt_value]
    break
print("1st Key Found: %s" % encrypt_key)
print("2nd Key Found: %s" % decrypt_key)

cipher1 = DES.new(encrypt_key, DES.MODE_ECB)
cipher2 = DES.new(decrypt_key, DES.MODE_ECB)
flag_intermediate = cipher2.decrypt(encrypted_flag)
flag = cipher1.decrypt(flag_intermediate).decode()
print("Flag: %s" % flag)
$ py solve.py
Bruteforcing 1st Key: 100%|████████████████████████████████████████████████████████████████| 999999/999999 [00:14<00:00, 68160.81it/s]
Bruteforcing 2nd Key: 100%|████████████████████████████████████████████████████████████████| 999999/999999 [00:14<00:00, 69801.52it/s]
Finding Key Intersection...
1st Key Found: b'939351  '
2nd Key Found: b'339311  '
Flag: 9af5126b7bc7f825b3cae0e32bd1acb4

Compress and Attack (130 points)

Your goal is to find the flag. compress_and_attack.py nc mercury.picoctf.net 33976

Hint : The flag only contains uppercase and lowercase letters, underscores, and braces (curly brackets)

compress_and_attack.py:

#!/usr/bin/python3 -u

import zlib
from random import randint
import os
from Crypto.Cipher import Salsa20

flag = open("./flag").read()


def compress(text):
    return zlib.compress(bytes(text.encode("utf-8")))

def encrypt(plaintext):
    secret = os.urandom(32)
    cipher = Salsa20.new(key=secret)
    c = cipher.encrypt(plaintext)
    return cipher.nonce + c

def main():
    while True:
        usr_input = input("Enter your text to be encrypted: ")
        compressed_text = compress(flag + usr_input)
        encrypted = encrypt(compressed_text)

        nonce = encrypted[:8]
        encrypted_text =  encrypted[8:]
        print(nonce)
        print(encrypted_text)
        print(len(encrypted_text))

if __name__ == '__main__':
    main()

知識點: zlib 壓縮Salsa20 加密法

這裡最主要是這兩行:

compressed_text = compress(flag + usr_input)
encrypted = encrypt(compressed_text)

把 flag 和 usr_input 放在一起壓縮,再把壓縮後的字串加密。

加密方法用了 Salsa20,它沒什麼可利用的漏洞,不過由於它是 Stream 的特性,所以加密後與明文字長度一致

而 zlib 壓縮的特性,是把重複出現的連續字元消除掉的性質。

比如說:

'hello' + 'world' -> zlib 壓縮 -> Salsa20 加密 -> 長度為 a
'hello' + 'helab' -> zlib 壓縮 -> Salsa20 加密 -> 長度為 b
'hello' + 'hello' -> zlib 壓縮 -> Salsa20 加密 -> 長度為 c

那麼以長度來說就會: a > b > c (兩個字串完全一樣時,壓縮後的長度也是最短)

以後稍微更改 compress_and_attack.py 來實驗看看”

#!/usr/bin/python3 -u

import zlib
from random import randint
import os
from Crypto.Cipher import Salsa20

#flag = open("./flag").read()
flag = "picoCTF{hello}"


def compress(text):
    return zlib.compress(bytes(text.encode("utf-8")))

def encrypt(plaintext):
    secret = os.urandom(32)
    cipher = Salsa20.new(key=secret)
    c = cipher.encrypt(plaintext)
    return cipher.nonce + c

def main():
    while True:
        usr_input = input("Enter your text to be encrypted: ")
        compressed_text = compress(flag + usr_input)
        encrypted = encrypt(compressed_text)

        nonce = encrypted[:8]
        encrypted_text =  encrypted[8:]
        print(nonce)
        print(encrypted_text)
        print(len(encrypted_text))

if __name__ == '__main__':
    main()
$ py compress_and_attack.py
Enter your text to be encrypted: picoCTF{world}
b'\xc2\xb6\xba\xb8V\x1c\xe6^'
b'_\x81\xd4\x88ID\x1e\xe2sD\xdf\x8dk\x06\xf6U\xa7\xbbq\x9b\x99\x05\x91I`e3x\xaap'
30
Enter your text to be encrypted: picoCTF{helab}
b'\xc3\x12\xd9-$\xcbQ\xd1'
b'\xa8\x13\xe2\x0c\x1a\x93M_\xf3\xd9\xa0\xa8\xda\x83\xc7\xbb\xbfG\x85[\xa5\x94Y\x97\xb1p0'
27
Enter your text to be encrypted: picoCTF{hello}
b'21\x8d\xc1`P\xc2\xf0'
b'\xb3\x1a\x96\x8d\xb2\x8f\xe2\x80\x18\xd9\x00\x04\x98\x7f\x8e\x8a\xaa\x96~\xd88\xe8\x87Z\xa4'
25

符合猜想,而且我們知道 flag 的前端是 picoCTF{ ,而 flag 的結尾是 } ,因此可以逐個字元去猜試,每次取長度最短值

payload
import string
from pwn import *

conn = remote("mercury.picoctf.net", 33976)


def get_ciphertext_length(b: bytes):
    conn.recvuntil(b"Enter your text to be encrypted: ")
    conn.send(b + b"\n")
    conn.recvline()
    conn.recvline()
    return int(conn.recvline().decode().strip())


flag = b"picoCTF{"
chrs = b"{_}" + (string.ascii_lowercase + string.ascii_uppercase).encode()

from itertools import product

while True:
    try:
        l = get_ciphertext_length(flag + b"-") # There is probably no "-" in flag
        for i in chrs:
            c = bytes([i])
            b = flag + c
            if get_ciphertext_length(b) < l:
                break
        flag = flag + c
        print(flag)
        if c == b"}":
            break
    except:
        conn = remote("mercury.picoctf.net", 33976)
        
[+] Opening connection to mercury.picoctf.net on port 33976: Done
b'picoCTF{s'
b'picoCTF{sh'
b'picoCTF{she'
b'picoCTF{sher'
b'picoCTF{sheri'
b'picoCTF{sherif'
b'picoCTF{sheriff'
b'picoCTF{sheriff_'
b'picoCTF{sheriff_y'
b'picoCTF{sheriff_yo'
b'picoCTF{sheriff_you'
b'picoCTF{sheriff_you_'
b'picoCTF{sheriff_you_s'
b'picoCTF{sheriff_you_so'
b'picoCTF{sheriff_you_sol'
b'picoCTF{sheriff_you_solv'
b'picoCTF{sheriff_you_solve'
b'picoCTF{sheriff_you_solved'
b'picoCTF{sheriff_you_solved_'
b'picoCTF{sheriff_you_solved_t'
b'picoCTF{sheriff_you_solved_th'
b'picoCTF{sheriff_you_solved_the'
b'picoCTF{sheriff_you_solved_the_'
b'picoCTF{sheriff_you_solved_the_c'
b'picoCTF{sheriff_you_solved_the_cr'
b'picoCTF{sheriff_you_solved_the_cri'
b'picoCTF{sheriff_you_solved_the_crim'
b'picoCTF{sheriff_you_solved_the_crime'
b'picoCTF{sheriff_you_solved_the_crime}'

Scrambled: RSA (140 points)

Hmmm I wonder if you have learned your lesson… Let’s see if you understand RSA and how the encryption works. Connect with nc mercury.picoctf.net 61477.

Hint 1 : Look at the ciphertext, anything fishy, maybe a little bit long?

Hint 2 : What happens if you encrypt the same input multiple times?

Hint 3 : Is RSA deterministic, why would outputs vary?

1. netcat 連線

$ nc mercury.picoctf.net 61477
flag: 10119999503707510464984248800588501014461368602832168586207062806075566979668530298934188458641843683156824220303048222826254071327336198958676392441651939866810469731135567562501929389152360881650745505785635517994491707517846205755231627225573477114934716177513473311913713581586138901708036818705133704976862047101387558476571786384034278336435489616297156011913162262231274339531242570247153151234171971861597668603482593016582840017990292675499671116901689869155592572780229060082768211790490106749503251217498718384830523456974462649774778450587684002040419336222728534779988547158464825474510800044007532045416389792509208043490824276229652835140377270867684820259804882761400777375465065257541872661817826434068561085026871103511143217520341803825799421328709167327293008235142077638067393940478124610849002396912379986883729247650894271786640840038877678772795410498648975905318693779386236254926598774787295010340139586932102483998055670933245139431866956419745412722662447909916029004344347629159319039121609567018108711232677840741154011244225285967881087475090180920582511460945531411232496048347620634201471049523109872124692186805589362058421519934793258763489281189489095335282892290721574392505723426393069157278014113670535222317943730954732866968367186709890674773744668191711062782960623365376203908035425272393869437013537741484958754947899479947463749038973862525328285654240881463517005711830937964066100973466479607392404419176768477057068602774587907520446404496141120029307734766335584406586674354675147945925238022442525781034870083565240737840276219665301079583588892621300646075707356753687597630676415428113827564889055244629210296203694983896953426995626654499008490178110086190939952334447998225034931128348491159070824843631797431874120104868509115518302838635693642607813447851818875985511834126657516813493160126951202912216818850680415303616424685249505259128131480141649137454925437280047756981697264832799056547604977446135984648406777591265404476159537258595911545839972710458345053891816437170692443781796232394209586624235327340606243191502319140726343765325272257830530777813724768717938208477544874481999517807022652039901528967468472515482628017566509965379985588243679075858105550512909355953050308079188705133691178256460987478525830322156532875572488899563272745209689318088034980270934201276705232145798748056509795994556537194446325519860259975564322521424135656298666747583558878247703698913988803508097550289264948589226397175372808023029133361485264228351944707489971196727515719154981628637222484561991671086250568854158973735878069471063464612542933811769336347992069332594665638705651024520827353845602326283023004560153123237804360296978194542334394988577777270138280316650452813703370536789775273014146711235805897052805170432052113187103468225347095108200607829502219612936595248521658859958663849436548381309990675722355154626935032024964173609965315777998602920762598442553702005624948974182577418173379107577405474171898320063252854550317575612330511137945904425694488442919103242805247934233043565222062775579363347697742673071468875847917541544072360496256611127356562545391163162086233796341396096742490012548952386221944709170887399650472568831575863104260365072741872309433283180647408174992032977479238625378093498635104151521909251901586454320646315857348793195156778141834040037815688729439135563174399517326339310729627770846066866106774212382186266147399601499651965580459397951786882576764003762437426500133596165361312133583625920640457262324831101397948413809896886740107545002979080348383402411932206792139724179141193302820200732878889407684365719641007788442313537005103518995321326919665118448991837788721109632644850041123483117819055438561644838746855166982924406019194307450891541990717813380447014607934010494398582972350159487294415927622039001301585817508409085463023224826993545634381991150618150834101853521364302387418608474908649267344963452179074122693645973571909278178118877622733310953045338948011301240574510263198870903795152694732538542417819798006330298955620879799101217683690645900471039101700986662595845372117434409343329555797985545292550309003556342057502146938190209593792629836309489909459124846636582788272234954874771702043655412239201362192415607497007762247447665147330281714605497517382139185364375616184610005298209730763521412663118003282899688134356754656917040923850891611304943734356574103593210391417948805308665840875652943353691215416104333156600495525562339845925955085169575382127224077245743874295398230719219524626959403229462271892494564889952510997287875548432984154690001045194995272326752912819936311278658254010353192441179096079023572858806822685671659901339801204897931392689437348589795994528825736152830854491495432043485312724872257508917535123001764651599490804779284545699855254025178528878107855380502406952325097564873900506170340106944678019647281898063572219593113534661265449208203262369698056732705939431526771365440492816741250314255825517997969766524292766420385478871518551299229004353627548453811526425420468236620509424588286078955768010897026869998678269296734402981040173628119571103362816056044044944447361523697921633888317078273449189857551708321459428356804053986061650261012071270482690237318126744749172364311314371751437119123490814421523500228815058717066246272161309567728892473512054187463477659064212535312418762948977778837813036822218201422667436787908153944651179330012001703070840550792897580492814444144731653297307849895511276821033372478709069073207057547574193605005789036139115393482917368033820686174553957942897936544540468666582380631649196863795506356365307955098251665828162052018050755261053950027190678606774954560949368219791715565654187755357507556089258895319205874889173458272585005864565939696227323108836133471986403564780453133180252105786203549604345948664123939776510704566751080081284433326899252946020103778871724706942963465085697786074011936869873305364438148285018248070269611289772877076559873966216514576007038511061088010314946380272008585413019185518723879723689934585413829513467268826115380837746986415339273609278345683237365590473450855308493703355670261849846124296897978490994831743114376158857551955620449238824682700869014959855039696206847143828722797850927258118118504229260387717228435520900956498533139960316696411042646974153848707397970480626037482232199564842361414217743039489357182104252000777150709043139176149273823962777296242235751571329017126189757780309094932081402709421312294290100984383635852086565455372342913623588195148236042744666170228194919405415231988494562577848144724158541396826731675858603893840269649856856048305155407038231779378147433414265059485595902851517100628218882158645697342387926475471328051758579844062049705425062267228261761356800786596114836939544142933533948318237835227703046351394857914736176787099330557223434499820649276221193770155233119484531015547742102635774860021454540851823158062954863528008126194050352594412319989261783454817354930256067994208767183706034777488442259277471138680668391114174496418101049972678382621035208629078470477488097203772950700570275324652346683881760812731691512457394341008176691760218095614820509766132061626550264654965222043818026893261711932556711373126192018406002344655186869770874926753775824251688215493210735813874595804263742173969388618856492880822729016587338086044355272896587443522684091894943340738019378077388385563257238009818561506000289452163729979441923997577873685350977241772642543923623251666337734400635623592060430640801993970301381329948856294965262819551477470375404774968196859541741776271964846969014636115770544412927792343258744058187410551630148307375839234978788395193474884653604730735458791574794235612845382953014476024022537944048401235962277825436028931067666961765981690272215979581206853462418599267840854628994854447744573147870599805017911421629514359458413633155888730870714600240076485337388226999210211309270829804067177461814766454302298048558753919259350441442614684863473668913905
n: 120664285736585777441311487802523466687139343981460632702654060040302898675231867244231868935452696396161312398687603651903458480541933936762900209532488151458587561072058640468838421292228167806765134537939887563863718121523511257678373217414625692533760260445739379702033240356563800324171164449719872747217
e: 65537
I will encrypt whatever you give me:

得到一個很長的 flag

2. 試一些 input

I will encrypt whatever you give me: a
Here you go: 19158140201670685532637450649771400428590342393347859895336028381847776184756217307083721501476872025807005333870957358342257934000086333597302565042880730740887351238086707703178291344836797853121643620729924368348607786634389135320609077694634344176381621142115700163892216994042482125037780396729218592522

可見由單一字元加密,生成了一段很長的數字,雖然是經過 RSA 加密,但顯然不是傳統 RSA 加密。

3. 分析 input 和 output 之間的關連

I will encrypt whatever you give me: a
Here you go: 6679456739988861659356158210102565781176553350736994353026721669500826325372181454033928175867582195440528571838592017775398956609634403844080864923907426200824366918579107387706988899588808310029491748691126913708385140781620052518202736647146324603442827618195207943581018256196751466012160670884402707501
I will encrypt whatever you give me: b
Here you go: 49024709989385457501466929196274817850688545820274377799719797130319832154715164183009694183814000641343041436054175844994833820619130424908247056248635492390623563596330143249910287599522632774244548010794717711938115481089546344862409734103590793258016086021523513536553073311043408735412958556152091349117
I will encrypt whatever you give me: ab
Here you go: 458954810388099483382039890397797447678961157537616382769588408334999371061825154071912931253985017328189241155324703423607785589221941728694013790720073465337066788295292748372689831969901973893523824483892707527730150303783990884541698599071837483768752025627615037878277207553226051487667945350731737602836679456739988861659356158210102565781176553350736994353026721669500826325372181454033928175867582195440528571838592017775398956609634403844080864923907426200824366918579107387706988899588808310029491748691126913708385140781620052518202736647146324603442827618195207943581018256196751466012160670884402707501
I will encrypt whatever you give me: ab
Here you go: 667945673998886165935615821010256578117655335073699435302672166950082632537218145403392817586758219544052857183859201777539895660963440384408086492390742620082436691857910738770698889958880831002949174869112691370838514078162005251820273664714632460344282761819520794358101825619675146601216067088440270750145895481038809948338203989039779744767896115753761638276958840833499937106182515407191293125398501732818924115532470342360778558922194172869401379072007346533706678829529274837268983196990197389352382448389270752773015030378399088454169859907183748376875202562761503787827720755322605148766794535073173760283

很顯然的看到 2 次的 ab 產生了不同的加密字串。

$ ab1=458954810388099483382039890397797447678961157537616382769588408334999371061825154071912931253985017328189241155324703423607785589221941728694013790720073465337066788295292748372689831969901973893523824483892707527730150303783990884541698599071837483768752025627615037878277207553226051487667945350731737602836679456739988861659356158210102565781176553350736994353026721669500826325372181454033928175867582195440528571838592017775398956609634403844080864923907426200824366918579107387706988899588808310029491748691126913708385140781620052518202736647146324603442827618195207943581018256196751466012160670884402707501
$ ab2=667945673998886165935615821010256578117655335073699435302672166950082632537218145403392817586758219544052857183859201777539895660963440384408086492390742620082436691857910738770698889958880831002949174869112691370838514078162005251820273664714632460344282761819520794358101825619675146601216067088440270750145895481038809948338203989039779744767896115753761638276958840833499937106182515407191293125398501732818924115532470342360778558922194172869401379072007346533706678829529274837268983196990197389352382448389270752773015030378399088454169859907183748376875202562761503787827720755322605148766794535073173760283
$ a=6679456739988861659356158210102565781176553350736994353026721669500826325372181454033928175867582195440528571838592017775398956609634403844080864923907426200824366918579107387706988899588808310029491748691126913708385140781620052518202736647146324603442827618195207943581018256196751466012160670884402707501
$ b=49024709989385457501466929196274817850688545820274377799719797130319832154715164183009694183814000641343041436054175844994833820619130424908247056248635492390623563596330143249910287599522632774244548010794717711938115481089546344862409734103590793258016086021523513536553073311043408735412958556152091349117

2021-12-08_20-51

可以看出即使 ab1 與 ab2 字串不相同,但仍然含有 a 的子字串(而且只是 2 組數字順序不同而已),但卻沒有 b 的子字串,這裡猜測對於 ab 的這個字串,a 是經過一次 RSA 加密,而 b 卻經過 2 次 RSA 加密。

驗證猜想:

I will encrypt whatever you give me: abc
Here you go: 8641698107742562794975492048978603420894215624914404616091428077039194019641747190593547281124280656466029836399736016117483135757865152718391127587214133807832940169120730329973878469219193952482250918682917855818962224446351245059144767380402412735912043986521072756121955745974804094219960325767273559575122960342958301766518832296818028008598779546269946183840853930713203794365147537660989591233218725976355146559304839807930971706872070687569976524377220509537719341828440454620240891916497537819056213611466930876587361887088777899692790997781801202462503064048790411192320583028083048399261868742239630204349134910302953149709814167834533675072353253965770186324626981805508525232011099222769889919767360438973205253832636536065906494331983205036682776292056607781747775772764065631306241833453758651032611392323903475756160981083715404304889821489225246150685874940494376057006933430222731762925703581822854122807765
I will encrypt whatever you give me: ab
Here you go: 86416981077425627949754920489786034208942156249144046160914280770391940196417471905935472811242806564660298363997360161174831357578651527183911275872141338078329401691207303299738784692191939524822509186829178558189622244463512450591447673804024127359120439865210727561219557459748040942199603257672735595751134910302953149709814167834533675072353253965770186324626981805508525232011099222769889919767360438973205253832636536065906494331983205036682776292056607781747775772764065631306241833453758651032611392323903475756160981083715404304889821489225246150685874940494376057006933430222731762925703581822854122807765
I will encrypt whatever you give me: a
Here you go: 134910302953149709814167834533675072353253965770186324626981805508525232011099222769889919767360438973205253832636536065906494331983205036682776292056607781747775772764065631306241833453758651032611392323903475756160981083715404304889821489225246150685874940494376057006933430222731762925703581822854122807765

2021-12-08_21-01_1

一次加密的 a 字串在 ab 字串裡,同時也在 abc 字串裡。

那不是紅字的部份是什麼呢? 這裡猜想是 2 次加密的 b 字串

$ b=86416981077425627949754920489786034208942156249144046160914280770391940196417471905935472811242806564660298363997360161174831357578651527183911275872141338078329401691207303299738784692191939524822509186829178558189622244463512450591447673804024127359120439865210727561219557459748040942199603257672735595751

2021-12-08_21-07

猜對了,在 abc 字串裡的確有 1 次加密的 a 字串和 2 次加密的 b 字串,那剩餘不是紅色的部分,就是 3 次加密的 c 字串。

所以解法是暴力解,比如加密字串是 pico,我們可以很快找出 p 是 pico 裡的子字串。

但下一個 i,必須是 pa, pb, pc 這樣一個個比對出來,直到 pi 裡的 i 在 pico 的子字串裡,

然後就 pia, pib, pic 一個個與 pi 比對,這裡的 c 才會在 pico 的子字串裡。

4. payload

from pwn import *
import string

r = remote("mercury.picoctf.net", 61477)
r.recvuntil(b"flag: ")
flag_ct = r.recvline().decode()
r.recvline()  # n
r.recvline()  # e


def encrypt(s: str):
    r.sendlineafter(b"I will encrypt whatever you give me: ", s)
    r.recvuntil(b"Here you go: ")
    return r.recvline().decode().strip()


def get_represent(prefix: str, c: str, dt: dict):
    s = encrypt(prefix + c)
    for k in dt.keys():
        s = s.replace(k, "")
    return s


dt = {}
known = "picoCTF{"
flag = ""
for c in known:
    rp = get_represent(flag, c, dt)
    assert rp in flag_ct
    dt[rp] = c
    flag += c
    flag_ct = flag_ct.replace(rp, "")

chrs = "_}" + string.digits + string.ascii_lowercase + string.ascii_uppercase

while not flag.endswith("}"):
    for c in chrs:
        rp = get_represent(flag, c, dt)
        if not rp in flag_ct:
            continue
        dt[rp] = c
        flag += c
        flag_ct = flag_ct.replace(rp, "")
        print(flag)
        break
        
$ py solve1.py
[+] Opening connection to mercury.picoctf.net on port 61477: Done
/home/ank/CTF/picoCTF/scrambled_RSA/solve1.py:12: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  r.sendlineafter(b"I will encrypt whatever you give me: ", s)
picoCTF{b
picoCTF{ba
picoCTF{bad
picoCTF{bad_
picoCTF{bad_1
picoCTF{bad_1d
picoCTF{bad_1d3
picoCTF{bad_1d3a
picoCTF{bad_1d3a5
picoCTF{bad_1d3a5_
picoCTF{bad_1d3a5_4
picoCTF{bad_1d3a5_49
picoCTF{bad_1d3a5_498
picoCTF{bad_1d3a5_4981
picoCTF{bad_1d3a5_49817
picoCTF{bad_1d3a5_498172
picoCTF{bad_1d3a5_4981729
picoCTF{bad_1d3a5_4981729}
[*] Closed connection to mercury.picoctf.net port 61477

XtraORdinary (150 points)

Check out my new, never-before-seen method of encryption! I totally invented it myself. I added so many for loops that I don’t even know what it does. It’s extraordinarily secure!

output.txt

encrypt.py

output.txt:

57657535570c1e1c612b3468106a18492140662d2f5967442a2960684d28017931617b1f3637

encrypt.py:

#!/usr/bin/env python3

from random import randint

with open('flag.txt', 'rb') as f:
    flag = f.read()

with open('secret-key.txt', 'rb') as f:
    key = f.read()

def encrypt(ptxt, key):
    ctxt = b''
    for i in range(len(ptxt)):
        a = ptxt[i]
        b = key[i % len(key)]
        ctxt += bytes([a ^ b])
    return ctxt

ctxt = encrypt(flag, key)

random_strs = [
    b'my encryption method',
    b'is absolutely impenetrable',
    b'and you will never',
    b'ever',
    b'ever',
    b'ever',
    b'ever',
    b'ever',
    b'ever',
    b'break it'
]

for random_str in random_strs:
    for i in range(randint(0, pow(2, 8))):
        for j in range(randint(0, pow(2, 6))):
            for k in range(randint(0, pow(2, 4))):
                for l in range(randint(0, pow(2, 2))):
                    for m in range(randint(0, pow(2, 0))):
                        ctxt = encrypt(ctxt, random_str)

with open('output.txt', 'w') as f:
    f.write(ctxt.hex())
    

1.閱讀和修改程式碼

先把它改成能運行的程度

#!/usr/bin/env python3

from random import randint

#with open('flag.txt', 'rb') as f:
    #flag = f.read()
flag = b'Hello'

#with open('secret-key.txt', 'rb') as f:
    #key = f.read()
key = b'123'

def encrypt(ptxt, key):
    ctxt = b''
    for i in range(len(ptxt)):
        a = ptxt[i]
        b = key[i % len(key)]
        ctxt += bytes([a ^ b])
    return ctxt

ctxt = encrypt(flag, key)

random_strs = [
    b'my encryption method',
    b'is absolutely impenetrable',
    b'and you will never',
    b'ever',
    b'ever',
    b'ever',
    b'ever',
    b'ever',
    b'ever',
    b'break it'
]

for random_str in random_strs:
    for i in range(randint(0, pow(2, 8))):
        for j in range(randint(0, pow(2, 6))):
            for k in range(randint(0, pow(2, 4))):
                for l in range(randint(0, pow(2, 2))):
                    for m in range(randint(0, pow(2, 0))):
                        ctxt = encrypt(ctxt, random_str)

print("ctxt.hex =",ctxt.hex())
#with open('output.txt', 'w') as f:
#    f.write(ctxt.hex())

這裡發現關鍵的 encrypt() function 只是單純的把 2 組字串做 xor 加密

def encrypt(ptxt, key):
    ctxt = b''
    for i in range(len(ptxt)):
        a = ptxt[i]
        b = key[i % len(key)]
        ctxt += bytes([a ^ b])
    return ctxt

由於我們知道 XOR 的特性:

$N \oplus A \oplus A = N$

所以能知道如果同樣的字串做”雙數次”的 XOR,等於沒做。

  1. 因此重複的 “ever” 可以省略成一個
  2. 而嵌套的 loop 迴圈也可以當成是一個 for m in range(randint(0, pow(2, 0))),要麼做 XOR,要麼沒有。

2.找出 key

題目可以這樣子理解:

原先有一組 flag, 與 key 做了 XOR,

再與以下 5 組句子隨機做了複數次的 XOR。

A = 'my encryption method'
B = 'is absolutely impenetrable'
C = 'and you will never'
D = 'ever'
E = 'break it'

所以 ctxt 有這些可能的組合:

$ctxt = flag\oplus xor$

$ctxt = flag\oplus xor \oplus D$

$ctxt = flag\oplus xor \oplus A \oplus C \oplus D$

$ctxt = flag\oplus xor \oplus A \oplus B \oplus C \oplus D \oplus E$

“單數次”的 XOR 等同做了一次 XOR ,而”雙數次”的 XOR 等於沒有做,現在有 5 個句子,所以以排列組合來說

ctxt 一共有 $2^5 = 32$ 種可能性

這裡最難的地方是我們不知道 key,但我們知道 flag 的 prefix 應該是 picoCTF{,因此可以在這種前提下,把 32 種 key 的窮舉出來。

from random import randint
import itertools
def encrypt(ptxt, key):
    ctxt = b''
    for i in range(len(ptxt)):
        a = ptxt[i]
        b = key[i % len(key)]
        ctxt += bytes([a ^ b])
    return ctxt

key = b''
random_strs = [
    b'my encryption method',
    b'is absolutely impenetrable',
    b'and you will never',
    b'ever',
    b'break it'
]

flag_prefix = b'picoCTF{'
ctxt = '57657535570c1e1c612b3468106a18492140662d2f5967442a2960684d28017931617b1f3637'

for i in range(0,6):
    c = bytes.fromhex(ctxt)
    new_string = list(itertools.combinations(random_strs, i))
    for sub_new_string in new_string:
        for word in sub_new_string:
            c = encrypt(c, word)

        key = encrypt(c, flag_prefix)
        key = key[:len(flag_prefix)].decode()
        if key.isprintable():
            print(key)
            

這裡我用了 key.isprintable() 函數把 key 的范圍再縮少至 7 種

Bhr~a'0R
Elrmoq<T
Bhr~a'0R
Elrmoq<T
Bhr~a'0R
Africa!A
Elrmoq<T

其實這題有點用懵的,因為這個 key 要符合,有 2 個前提條件,第 1 是 flag 必須裡有 picoCTF{ 這個 prefix,第 2 是 key 的長度必須少於 picoCTF{ 這個 prefix 的長度。

然後再看看上面 7 個可能的 key,最有可能的便是 Africa!A 了,除了它是有意義的字詞以外,還有它最後的 A 重複了,表示 key 很有可能是 Africa! 的循環。

payload

找到了 key,用同樣的腳本便能窮舉出 32 個 flag (當然用 flag.isprintable() 縮減至 4 個)

from random import randint
import itertools
def encrypt(ptxt, key):
    ctxt = b''
    for i in range(len(ptxt)):
        a = ptxt[i]
        b = key[i % len(key)]
        ctxt += bytes([a ^ b])
    return ctxt

key = b''
random_strs = [
    b'my encryption method',
    b'is absolutely impenetrable',
    b'and you will never',
    b'ever',
    b'break it'
]

key = b'Africa!'
ctxt = '57657535570c1e1c612b3468106a18492140662d2f5967442a2960684d28017931617b1f3637'

for i in range(0,6):
    c = bytes.fromhex(ctxt)
    new_string = list(itertools.combinations(random_strs, i))
    for sub_new_string in new_string:
        for word in sub_new_string:
            c = encrypt(c, word)

        flag = encrypt(c, key)
        flag = flag.decode()
        if flag.isprintable():
            print(flag)
            
tcckOD[nr4=wHs4W5Re0}e5X3hm9>aTd1>'..y
tcckOD[nr4=wHs4W5Re0}e5X3hm9>aTd1>'..y
picoCTF{w41t_s0_1_d1dnt_1nv3nt_x0r???}
tcckOD[nr4=wHs4W5Re0}e5X3hm9>aTd1>'..y

得出flag picoCTF{w41t_s0_1_d1dnt_1nv3nt_x0r???}


triple-secure (150 points)

To get the flag, you must break RSA not once, but three times!

public-key.txt

encrypt.py

public-key.txt:

n1: 15192492059814175574941055248891268822162533520576381643453916855435310880285336743521199057138647926712835561752909538944229702432795423884081992987060760867003375755338557996965825324749221386675061886921763747311599846248565297387814717840084998677273427776535730840343260681623323972936404815862969684384733188827100528542007213405382537935243645704237369770300643318878176739181891072725262069278646319502747718264711249767568106460533935904219027313131270918072460753061248221785076571054217566164086518459844527639082962818865640864990672033657423448004651989761933295878220596871163544315057550871764431562609
n2: 15896482259608901559307142941940447232781986632502572991096358742354276347180855512281737388865155342941898447990281534875563129451327818848218781669275420292448483501384399236235069545630630803245125324540747189305877026874280373084005881976783826855683894679886076284892158862128016644725623200756074647449586448311069649515124968073653962156220351541159266665209363921681260367806445996085898841723209546021525012849575330252109081102034217511126192041193752164593519033112893785698509908066978411804133407757110693612926897693360335062446358344787945536573595254027237186626524339635916646549827668224103778645691
n3: 16866741024290909515057727275216398505732182398866918550484373905882517578053919415558082579015872872951000794941027637288054371559194213756955947899010737036612882434425333227722062177363502202508368233645194979635011153509966453453939567651558628538264913958577698775210185802686516291658717434986786180150155217870273053289491069438118831268852205061142773994943387097417127660301519478434586738321776681183207796708047183864564628638795241493797850819727510884955449295504241048877759144706319821139891894102191791380663609673212846473456961724455481378829090944739778647230176360232323776623751623188480059886131
e: 65537
c: 5527557130549486626868355638343164556636640645975070563878791684872084568660950949839392805902757480207470630636669246237037694811318758082850684387745430679902248681495009593699928689084754915870981630249821819243308794164014262751330197659053593094226287631278905866187610594268602850237495796773397013150811502709453828013939726304717253858072813654392558403246468440154864433527550991691477685788311857169847773031859714215539719699781912119479668386111728900692806809163838659848295346731226661208367992168348253106720454566346143578242135426677554444162371330348888185625323879290902076363791018691228620744490

encrypt.py:

#!/usr/bin/env python3

from Crypto.Util.number import getPrime, bytes_to_long

with open('flag.txt', 'rb') as f:
    flag = f.read()

p = getPrime(1024)
q = getPrime(1024)
r = getPrime(1024)

n1 = p * q
n2 = p * r
n3 = q * r

moduli = [n1, n2, n3]

e = 65537
c = bytes_to_long(flag)

for n in moduli:
    c = pow(c, e, n)

with open('public-key.txt', 'w') as f:
    f.write(f'n1: {n1}\n')
    f.write(f'n2: {n2}\n')
    f.write(f'n3: {n3}\n')
    f.write(f'e: {e}\n')
    f.write(f'c: {c}\n')

1.分析

這裡看似很安全的,做了 3 輪的 RSA 加密

第一輪 : $C_1 = M^e(mod \space n1)$

第二輪 : $C_2 = {C_1}^e(mod \space n2)$

第三輪 : $C_3 = {C_2}^e(mod \space n3)$

但這裡有個重大的漏洞,是 n1, n2, n3 之間的關係

n1 = p * q
n2 = p * r
n3 = q * r

因為

$n1 * n2 * n3 = (p * q * r)^2$

我們雖然很難在一個大數裡分解出 2 個大質數,但針對大數進行開平方卻是十分容易的

$mix = p * q *r$

那 r, q, p 這 3 個質數也很容易找出來

r = mix // n1
q = mix // n2
p = mix // n3

2.payload

找出質數就同於破解了 RSA 加密,反向操作一遍解密即可

import libnum
import gmpy2
n1 = 15192492059814175574941055248891268822162533520576381643453916855435310880285336743521199057138647926712835561752909538944229702432795423884081992987060760867003375755338557996965825324749221386675061886921763747311599846248565297387814717840084998677273427776535730840343260681623323972936404815862969684384733188827100528542007213405382537935243645704237369770300643318878176739181891072725262069278646319502747718264711249767568106460533935904219027313131270918072460753061248221785076571054217566164086518459844527639082962818865640864990672033657423448004651989761933295878220596871163544315057550871764431562609
n2 = 15896482259608901559307142941940447232781986632502572991096358742354276347180855512281737388865155342941898447990281534875563129451327818848218781669275420292448483501384399236235069545630630803245125324540747189305877026874280373084005881976783826855683894679886076284892158862128016644725623200756074647449586448311069649515124968073653962156220351541159266665209363921681260367806445996085898841723209546021525012849575330252109081102034217511126192041193752164593519033112893785698509908066978411804133407757110693612926897693360335062446358344787945536573595254027237186626524339635916646549827668224103778645691
n3 = 16866741024290909515057727275216398505732182398866918550484373905882517578053919415558082579015872872951000794941027637288054371559194213756955947899010737036612882434425333227722062177363502202508368233645194979635011153509966453453939567651558628538264913958577698775210185802686516291658717434986786180150155217870273053289491069438118831268852205061142773994943387097417127660301519478434586738321776681183207796708047183864564628638795241493797850819727510884955449295504241048877759144706319821139891894102191791380663609673212846473456961724455481378829090944739778647230176360232323776623751623188480059886131
e = 65537
c = 5527557130549486626868355638343164556636640645975070563878791684872084568660950949839392805902757480207470630636669246237037694811318758082850684387745430679902248681495009593699928689084754915870981630249821819243308794164014262751330197659053593094226287631278905866187610594268602850237495796773397013150811502709453828013939726304717253858072813654392558403246468440154864433527550991691477685788311857169847773031859714215539719699781912119479668386111728900692806809163838659848295346731226661208367992168348253106720454566346143578242135426677554444162371330348888185625323879290902076363791018691228620744490

mix = int(gmpy2.iroot(n1 * n2 * n3, 2)[0])

r = mix // n1
q = mix // n2
p = mix // n3

phi1 = (p - 1) * (q - 1)
phi2 = (p - 1) * (r - 1)
phi3 = (q - 1) * (r - 1)

d1 = libnum.modular.invmod(e, phi1)
d2 = libnum.modular.invmod(e, phi2)
d3 = libnum.modular.invmod(e, phi3)

c = pow(c, d3, n3)
c = pow(c, d2, n2)
c = pow(c, d1, n1)
m = c

flag = libnum.n2s(m)
print(flag)

求得 flag picoCTF{1_gu3ss_tr1pl3_rs4_1snt_tr1pl3_s3cur3!!!!!!}

後續請參見 PicoCTF write up - Cryptography (密碼學篇 - 下)