[Writeup] TJCTF 2019 (2)

[Forensics] Mind Blown - 30pts

Written by rj9

One of my friends keeps sending me weird memes from his favorite subreddit but I don’t quite understand this one…

meme

Solution

binwalk 一下會發現題目給的圖片中藏了另外兩張圖片

可以用 binwalk --dd=".*" meme.jpg 匯出裡面檔案,或是直接 dd 也可以
匯出後直接觀察圖片即可

Flag tjctf{kn0w_y0ur_m3tad4ta}

[Misc] Othello - 30pts

Written by boomo

That’s not rain, is it?

Solution

音檔聽起來像是有人在唸 Flag
不過音檔是倒過來且減速過的
只要先把整段音檔 Reverse 並調快約 2.3 倍即可清楚的聽到他在唸的 Flag 了

Flag tjctf{aka_r3v3rsi}

[Reversing] Broken Parrot - 40pts

Written by evanyeyeye

I found this annoying parrot. I wish I could just ignore it, but I’ve heard that it knows something special.

Solution

當你直接 strings parrot | grep tjctf
會噴出 tjctf{my_b3l0v3d_5qu4wk3r_w0n7_y0u_l34v3_m3_4l0n3}
然而這不是這題的 Flag

用 IDA Decompile 後可以知道
他會先判斷輸入的字串長度是否為 34 (包含換行字元)
接著會判斷

  1. 第 0 ~ 5 個字元是否跟假 Flag 的第 0 ~ 5 個字元相等
  2. 第 6 ~ 8 個字元是否跟假 Flag 的第 14 ~ 16 個字元相等
  3. 第 10 ~ 33 個字元是否跟假 Flag 的第 27 ~ 50 個字元相等
  4. 第 9 個字元是否為 d

Flag tjctf{3d_d0n7_y0u_l34v3_m3_4l0n3}

[Forensics] Cable Selachimorpha - 40pts

Written by jfrucht25

Although Omkar is the expert at web, I was still able to intercept his communications. Find out what password he used to login into his website so that we can gain access to it and see what Omkar is up to.

capture.pcap

Solution

題目說要找出他登入的密碼,也就是說只要看 http 通訊的資料即可
在 Wireshark 上方的 Apply a display filter ... 中輸入 http
稍微翻一下即可找到登入資訊

或者直接 strings capture.pcap | grep tjctf

Flag tjctf{b0mk4r_br0k3_b10n}

[Misc] Simply Secure Secrets - 50pts

Written by evanyeyeye

nc p1.tjctf.org 8000

Solution

nc 過去後會看到這個畫面

稍微看了一下發現有一個 secret name 叫 tjctf ,沒意外的話 Flag 應該藏在裡面

透過訊息,我們得知 secret pin 是一個六位數

也就是說我們可以試著暴力破解
下面這份 Code 大約要花 6 分鐘才能找到 secret pin

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
from pwn import *
from threading import Thread

HOST = "p1.tjctf.org"
PORT = 8000

Range = 5000

def find(i):
r = remote(HOST, PORT)
r.recv()
for j in range(i * Range, i * Range + Range):
r.sendline("r")
r.recv()
r.sendline("tjctf")
r.recv()
r.sendline(str(j).zfill(6))
res = r.recv()
# print(res)
if "{" in res:
print("secret pin: {}".format(j))
print("secret message: {}".format(res))
with open("_flag.txt", "w") as f:
f.write(res)
r.close()
exit()

for i in range(10**6 / Range):
thread = Thread(target=find, args=(i,))
thread.daemon = True
thread.start()

while True:
time.sleep(1)

Flag tjctf{1_533_y0u_f0rc3d_y0ur_w4y_1n}

[Forensics] SOS - 70pts

Written by Alaska47

Help! I swiped this off some extraterrestrial musician’s laptop, but I think I’m getting trolled. I tried to intercept their communications, but their frequency is just too high. There’s something wrong, but I just can’t put my ear on it…

Solution

觀察音檔的 Spectrogram (時頻譜) 會發現在頻率很高的地方有 Morse Code

手動把 Morse Code 打出來再轉成 Plaintext 即可

Morse: .-- --- .---.-.-- .-- .... .- - .- - .-. --- .-.. .-.. --..-- .- -- .. .-. .. --. .... - ..--.. .-- . .-.. .-.. --..-- . -. --- ..- --. .... --- ..-. - .... .- - --..-- .... . .-. . .----. ... - .... . ..-. .-.. .- --. ---... - .--- -.-. - ..-. .--.- -- -.-- ...- --- .. -.-. . .. ... --. .-. --- .-- .. -. --. -- --- .-. ... . .--.--

Flag tjctf{myvoiceisgrowingmorse}

[Crypto] Is this the real life - 90pts

Written by etherlyt

…is this just fantasy? public key rsagen

Solution

下面這段 Code 的用意在產生 8 個 $\varphi$ ,並將所有 $\varphi$ 的值打亂丟進 nlist
Line 32 處, block 的值永遠會是 1 ,因為 $d e \equiv 1 \pmod{\varphi}$
Line 34 ~ 35 處, n 的值算出來永遠會是 $0.99\ldots$ ,故 Line 37 處, int(...)**ceil(n) 會等於 int(...)

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
for x in range(8):
n = 0
phi = 1
#RSA pubkey gen
while True:
p = random_prime(2^floor(nbits/2)-1,lbound=2^floor(nbits/2-1),proof=False)
q = random_prime(2^floor(nbits/2)-1, lbound=2^floor(nbits/2-1), proof=False)
n = p*q
phi = (p-1)*(q-1)
d = modinv(e, (p-1)*(q-1))
block = (d*e) % phi
if gcd(phi,e) == 1:
shield = 108.0
n = ((shield.nth_root(2)+10).nth_root(3)-(shield.nth_root(2)-10).nth_root(3))/2
break
phi = int(str(phi)[:4]+str(phi)[::-1][:4])**ceil(n)
nlist.append(phi)

下面這段 Code 中
nmflag[i] 的二進位
cipher[i] 的計算方式為: $\text{cipher}i = \text{nm} \sum{i=0}^{7}{\text{nm}_i \times \text{nlist}_i}$

40
41
42
43
44
45
46
cipher = [0]*len(flag)
for c in range(len(flag)):
nm = str(int(bin(int(flag[c].encode('hex'),16)).replace('0b', '')))
for b in range(len(nm)):
cipher[c] += int(nm[b])*nlist[b]
cipher[c] = cipher[c]*int(nm)
cipher[c] = cipher[c]%modulus

要還原出原本的字串,我們可以照著原本加密的方法將所有可能的字元加密
再判斷 cipher[i] 中的數字是哪一個字元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import string

nlist = [42798447, 60070844, 64372735, 50740679, 96064802, 42258424, 44356317, 77336984]
ct = [292296909762800, 215653060477940, 208434519524352, 292296909762800, 265338299876870, 338411113077906, 217961079953287, 344375715089844, 205288667438400, 16912457697000, 11315622010000, 341537164927645, 314135413320574, 169124576970000, 32145056144756, 344375715089844, 15964193777715, 292296909762800, 344375715089844, 388451782884159, 16912457697000, 26533829987687, 344375715089844, 150925396356060, 281783803794437, 87154851154764, 398222847250224]

flag = string.printable
cipher = [0]*len(flag)

for c in range(len(flag)):
nm = str(int(bin(int(flag[c].encode('hex'),16)).replace('0b', '')))
for b in range(len(nm)):
cipher[c] += int(nm[b])*nlist[b]
cipher[c] = cipher[c]*int(nm)

res = ""
for i in range(len(ct)):
if ct[i] in cipher:
res += flag[cipher.index(ct[i])]
else:
res += "#"

print(res)

Flag tjctf{i_T40ugh7_1t_w43_RsA}

[Misc] Sight at Last - 100pts

Written by jfrucht25

nc p1.tjctf.org 8005

Solution

nc 過去後,會接收到題目內容跟一張 Base64 encoded 的圖片檔

Base64 decode 後的圖片長這樣

題目要我們回傳任兩圓之圓心距的最小值
這題我們一開始是用 OpenCV 做,不過無奈參數一直調整不好所以才換成這個方法:

  1. 判斷像素是否為黑色 (在圓上)
    因為圓上的像素並非都是黑色的 (#000000) ,所以利用 Threshold 的概念,將像素中 RGB 的值相加取平均,小於某個值就直接視為黑色 (此處用的是 100)

  2. 圓的找法
    循序訪問所有圖上的像素,若該像素為黑,則對該點進行 Flood Fill ,如此即可找出圓

  3. 圓心的位置
    因圓為對稱圖形,故可在進行 Flood Fill 時紀錄 x, y 軸最大與最小值,圓心座標即為兩軸最大最小值之平均值

  4. 任兩點間最短距離
    在此我們使用的是 Divide & Conquer ,分治找出平面最近點對 (Closest Pair)

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import base64, math
from pwn import *
from PIL import Image

HOST = "p1.tjctf.org"
PORT = 8005

filename = "res{}.jpg"
threshold = 100

dx = [-1, 0, 1, 0]
dy = [ 0, 1, 0,-1]

def get_points(i):
im = Image.open(filename.format(i))
pixel = im.load()
map = [[0 for _i in range(400)] for _j in range(400)]

for i in range(400):
for j in range(400):
r, g, b = pixel[j, i]
map[i][j] = True if (r + g + b) / 3 < threshold else False

p = []
for i in range(400):
for j in range(400):
if map[i][j]:
q = []
q.append((i, j))
map[i][j] = False
xm, xM = j, j
ym, yM = i, i
while len(q):
x, y = q.pop()
xm, xM = min(xm, y), max(xM, y)
ym, yM = min(ym, x), max(yM, x)
for d in range(0, 4):
nx, ny = x + dx[d], y + dy[d]
if 0 <= nx and nx < 400 and 0 <= ny and ny < 400 and map[nx][ny]:
map[nx][ny] = False
q.append((nx, ny))
# print("{} {} {} {}".format(xm, xM, ym, yM))
p.append(((xm + xM) / 2, (ym + yM) / 2))
return p

def dis(lhs, rhs):
return math.sqrt(math.pow(lhs[0] - rhs[0], 2) + math.pow(lhs[1] - rhs[1], 2))

def Closest_Pair(p, l, r):
if l == r:
return 0x3f3f3f3f
if r - l == 1:
return dis(p[l], p[r])
m = (l + r) >> 1
d = min(Closest_Pair(p, l, m), Closest_Pair(p, m + 1, r))
tmp = []
for i in range(m, l - 1, -1):
if p[m][0] - p[i][0] >= d:
break
tmp.append(p[i])
for i in range(m + 1, r + 1, 1):
if p[i][0] - p[m][0] >= d:
break
tmp.append(p[i])
tmp.sort(key=lambda tup: tup[1])
for i in range(0, len(tmp) - 1, 1):
for j in range(1, 4, 1):
if i + j >= len(tmp):
break
d = min(d, dis(tmp[i], tmp[i + j]))
return d

r = remote(HOST, PORT)
r.recvline()

for i in range(100):
r.recvline()
tmp = r.recvuntil(">>> ").split('\n')
# print(tmp)
s = ""
for _ in range(len(tmp) - 1):
s += tmp[_]
# print(s)

s = base64.b64decode(s)
with open(filename.format(i), "w") as f:
f.write(s)

p = get_points(i)
p.sort(key=lambda tup: tup[0])
res = Closest_Pair(p, 0, len(p) - 1)

print("# {}: res = {}".format(i, res))
r.sendline(str(res))

s = r.recvline()
print(s.strip())
s = r.recvline()

print(r.recv())
r.close()

Flag tjctf{i5_th1s_c0mput3r_v1si0n?}

[Forensics] Planning Virtual Distruction - 120pts

Written by jfrucht25

In his quest for world domination, Omkar realized that he needed to conquer more than just land, so he turned to conquering the internet. His first target was becoming the king of youtube by overcoming Pewdiepie. As a result, he embodied his Indian culture, creating the channel TSeries. In a step to stop Omkar’s world domination, we need to regain control of the internet. Perhaps you can uncover his plans hidden in this image and make a DIFFERENCE before it is too late.

Solution

在經過一番搜尋,確定了這題用到的方法是 PVD (Pixel Value Difference)
這個連結 中的第二點有提到此方法的實現過程
照著這個邏輯即可寫出下面這份 Code

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
from PIL import Image
import math, binascii

im = Image.open("phase2plans.png")

W, H = im.size

rng = [8, 8, 16, 32, 64, 128]
_rng = [0, 8, 16, 32, 64, 128] # prefix sum
zero = [3, 3, 4, 5, 6, 7]

def get_pos(n):
i = n // W
j = W - n % W - 1 if i & 1 else n % W
return (j, i)

msg = ""
for _ in range(0, H * W, 2):
cur_p = im.getpixel(get_pos(_))
nxt_p = im.getpixel(get_pos(_ + 1))

d = abs(nxt_p - cur_p)

l = 0
while l + 1 < len(_rng) and d >= _rng[l + 1]:
l += 1

b = d - _rng[l]
msg += bin(b)[2:].zfill(zero[l])

res = int(msg, 2)
res = str(hex(res)[2:])[:-1]
with open("res", "w") as f:
f.write(binascii.unhexlify(res))

Flag tjctf{r1p_p3wd5_t53r1s_b4d}