概要
SECCON Beginners CTF 2021 にソロで参加した。
昨年別のイベントで初めてCTFに触れて以来、2度目のCTFだったがwelcome含めて7問解くことができた。
勉強と備忘録も兼ねて、解けた問題と惜しかった問題について自分なりのWriteupを書いていく。
[crypto]simple_RSA
与えられたファイルを解凍すると、以下の2つのファイルが入っていた。
n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283
e = 3
c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613
from Crypto.Util.number import *
from flag import flag
flag = bytes_to_long(flag.encode("utf-8"))
p = getPrime(1024)
q = getPrime(1024)
n = p * q
e = 3
assert 2046 < n.bit_length()
assert 375 == flag.bit_length()
print("n =", n)
print("e =", e)
print("c =", pow(flag, e, n))
最初はmsieveとyafuでn
の素因数分解をしてみたが終わらず、解き方が違うと考えて他の方法を探った。
調べてみるとe
が小さいときに使えるLow Public Exponent Attackなるものを発見。
これを利用して複合に成功し、flagを得ることができた。
参考:公開鍵暗号系の処理メモ「Low Public-Exponent Attack」
from Crypto.Util.number import *
import gmpy2
n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283
e = 3
c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613
m,result = gmpy2.iroot(c,e)
print(m)
print(bytes.fromhex(format(m,'x')).decode('utf-8'))
ctf4b{0,1,10,11...It's_so_annoying.___I'm_done}
[reversing]only_read
バイナリが配布されたので、とりあえずstrings
で中身を見てみるが、flagらしきものは無い。
実行してみると、入力を受け付けた後Incorrect
と表示される。
Ghidraで解析してみると以下のような部分があったので、この通りに読んでflagを得た。
if (((((((char)local_28 == 'c') && (local_28._1_1_ == 't')) && (local_28._2_1_ == 'f')) &&
(((local_28._3_1_ == '4' && (local_28._4_1_ == 'b')) &&
((local_28._5_1_ == '{' && ((local_28._6_1_ == 'c' && (local_28._7_1_ == '0')))))))) &&
(((char)local_20 == 'n' &&
((((((local_20._1_1_ == '5' && (local_20._2_1_ == 't')) && (local_20._3_1_ == '4')) &&
((local_20._4_1_ == 'n' && (local_20._5_1_ == 't')))) &&
((local_20._6_1_ == '_' && ((local_20._7_1_ == 'f' && ((char)local_18 == '0')))))) &&
(local_18._1_1_ == 'l')))))) &&
((((local_18._2_1_ == 'd' && (local_18._3_1_ == '1')) && ((char)local_14 == 'n')) &&
((local_14._1_1_ == 'g' && (local_12 == '}')))))) {
puts("Correct");
}
else {
puts("Incorrect");
}
ctf4b{c0n5t4nt_f0ld1ng}
[pwnable]rewriter
用意されたサーバーに接続してみると以下のように表示された。
また、サーバーで動いているプログラムも配布された。
[Addr] |[Value]
====================+===================
0x00007ffc3a4cb4a0 | 0x0000000000000000 <- buf
0x00007ffc3a4cb4a8 | 0x0000000000000000
0x00007ffc3a4cb4b0 | 0x0000000000000000
0x00007ffc3a4cb4b8 | 0x0000000000000000
0x00007ffc3a4cb4c0 | 0x0000000000000000 <- target
0x00007ffc3a4cb4c8 | 0x0000000000000000 <- value
0x00007ffc3a4cb4d0 | 0x0000000000401520 <- saved rbp
0x00007ffc3a4cb4d8 | 0x00007f3d74a36bf7 <- saved ret addr
0x00007ffc3a4cb4e0 | 0x0000000000000001
0x00007ffc3a4cb4e8 | 0x00007ffc3a4cb5b8
Where would you like to rewrite it?
>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <err.h>
#define BUFF_SIZE 0x20
void win() {
execve("/bin/cat", (char*[3]){"/bin/cat", "flag.txt", NULL}, NULL);
}
void show_stack(unsigned long *stack);
int main() {
unsigned long target = 0, value = 0;
char buf[BUFF_SIZE] = {0};
show_stack(buf);
printf("Where would you like to rewrite it?\n> ");
buf[read(STDIN_FILENO, buf, BUFF_SIZE-1)] = 0;
target = strtol(buf, NULL, 0);
printf("0x%016lx = ", target);
buf[read(STDIN_FILENO, buf, BUFF_SIZE-1)] = 0;
value = strtol(buf, NULL, 0);
*(long*)target = value;
}
void show_stack(unsigned long *stack) {
printf("\n%-20s|%-20s\n", "[Addr]", "[Value]");
puts("====================+===================");
for (int i = 0; i < 10; i++) {
printf(" 0x%016lx | 0x%016lx ", &stack[i], stack[i]);
if (&stack[i] == stack)
printf(" <- buf");
if (&stack[i] == ((unsigned long)stack + BUFF_SIZE))
printf(" <- target");
if (&stack[i] == ((unsigned long)stack + BUFF_SIZE + 0x8))
printf(" <- value");
if (&stack[i] == ((unsigned long)stack + BUFF_SIZE + 0x10))
printf(" <- saved rbp");
if (&stack[i] == ((unsigned long)stack + BUFF_SIZE + 0x18))
printf(" <- saved ret addr");
puts("");
}
puts("");
}
__attribute__((constructor))
void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
}
指定したアドレスを書き換えるプログラムのようなので、どうにかしてwin()
を呼んでやれば良いと考た。
gdb
でwin()
のアドレスを探ると0x4011f6
と判明。
saved ret addr
を0x4011f6
に書き換えるように入力するとflagを得られる。
※この書き換えをローカル環境でやってもflagファイルが無いのでエラーとなる。私は、なぜかその後リモートで試すことなく諦めてしまった。長時間やりすぎたのに加え、他の問題に気を取られて頭が回って居なかったのだと思う。
ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}
[web]osoba
指定のULRに飛ぶとこんなページになっていた。
ページの詳細を見ると/?page=public/wip.html
にアクセスし、エラーが表示される。
問題文に「フラグはサーバの /flag にあります!」と書かれていたので/?page=/flag
に書き換えてみるとflagが表示された。
ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}
[misc]git-leak
ファイルを解凍すると、以下のような構成だった。
dist/
├.git/
├README.md
└note.
git reflog
で履歴を見てみると、以下のようになっていた。
e0b545f (HEAD -> master) HEAD@{0}: commit (amend): feat: めもを追加
80f3044 HEAD@{1}: commit (amend): feat: めもを追加
b3bfb5c HEAD@{2}: rebase -i (finish): returning to refs/heads/master
b3bfb5c HEAD@{3}: commit (amend): feat: めもを追加
7387982 HEAD@{4}: rebase -i: fast-forward
36a4809 HEAD@{5}: rebase -i (start): checkout HEAD~2
7387982 HEAD@{6}: reset: moving to HEAD
7387982 HEAD@{7}: commit: feat: めもを追加
36a4809 HEAD@{8}: commit: feat: commit-treeの説明を追加
9ac9b0c HEAD@{9}: commit: change: 順番を変更
8fc078d HEAD@{10}: commit: feat: git cat-fileの説明を追加
d3b47fe HEAD@{11}: commit: feat: fsckを追記する
f66de64 HEAD@{12}: commit: feat: reflogの説明追加
d5aeffe HEAD@{13}: commit: feat: resetの説明を追加
a4f7fe9 HEAD@{14}: commit: feat: git logの説明を追加
9fcb006 HEAD@{15}: commit: feat: git commitの説明追加
6d21e22 HEAD@{16}: commit: feat: git addの説明を追加
656db59 HEAD@{17}: commit: feat: add README.md
c27f346 HEAD@{18}: commit (initial): initial commit
reset
しているあたりが怪しいと思い、その前まで戻してみると、flag.txt
を発見。
中身を確認するとflagが表示された。
$ git reset --hard "HEAD@{7}"
$ git checkout
$ ls
README.md flag.txt note.md
$ cat flag.txt
ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}
[misc]Mail_Address_Validator
用意されたサーバーにアクセスすると、以下のように表示された。
また、サーバーで動いているプログラムも配布された。
I check your mail address.
please puts your mail address.
#!/usr/bin/env ruby
require 'timeout'
$stdout.sync = true
$stdin.sync = true
pattern = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
begin
Timeout.timeout(60) {
Process.wait Process.fork {
puts "I check your mail address."
puts "please puts your mail address."
input = gets.chomp
begin
Timeout.timeout(5) {
if input =~ pattern
puts "Valid mail address!"
else
puts "Invalid mail address!"
end
}
rescue Timeout::Error
exit(status=14)
end
}
case Process.last_status.to_i >> 8
when 0 then
puts "bye."
when 1 then
puts "bye."
when 14 then
File.open("flag.txt", "r") do |f|
puts f.read
end
else
puts "What's happen?"
end
}
rescue Timeout::Error
puts "bye."
end
メールアドレスのチェックに5秒以上かかるとタイムアウトしてflagを表示するようなので、正規表現でのマッチングのステップ数を表示してくれるサイトを参考に、この条件でのマッチングに時間がかかる文字列を組み上げてflagを得た。
参考:regex101.com
ab.a@abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
[misc]depixelization
ファイルを解凍すると、以下の画像ファイルとプログラムが入っていた。
import cv2
import numpy as np
flag = "**********flag**********"
print("FLAG: " + flag)
images = np.full((100, 85, 3), (255,255,255), dtype=np.uint8)
for i in flag:
# char2img
img = np.full((100, 85, 3), (255,255,255), dtype=np.uint8)
cv2.putText(img, i, (0, 80), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA)
# pixelization
cv2.putText(img, "P", (0, 90), cv2.FONT_HERSHEY_PLAIN, 7, (0, 0, 0), 5, cv2.LINE_AA)
cv2.putText(img, "I", (0, 90), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA)
cv2.putText(img, "X", (0, 90), cv2.FONT_HERSHEY_PLAIN, 9, (0, 0, 0), 5, cv2.LINE_AA)
simg = cv2.resize(img, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_NEAREST) # WTF :-o
img = cv2.resize(simg, img.shape[:2][::-1], interpolation=cv2.INTER_NEAREST)
# concat
if images.all():
images = img
else:
images = cv2.hconcat([images, img])
cv2.imwrite("output.png", images)
このプログラムを使ってモザイク化されたflagの画像のようなので、同じようにflagのフォーマットである[a-z],[A-Z],[0-9],[{}_!#$%&.]
の画像を生成し、マッチングするプログラムを作成すればflagが得られると考えた。
しかし、pythonで画像処理のコードを書いたことが無く残り時間も少なかったため、フラグの文字数が30文字程度でピクセルもはっきり見えていることから、これから調べてコード書くより目視でマッチングした方が早いのではないかという結論に至った。
したがってpixelization.py
を少し書き換えて以下のようにそれぞれの文字をモザイク化した画像を生成し、目視でマッチングしてflagを得た。
[1234567890].png
[_!#$%&.].png
ctf4b{1f_y0u_p1x_y0u_c4n_d3p1x}
感想
最後の問題のようにゴリ押しで解いてしまった問題や、分かっていたのに解けなかった問題もあり課題も残るが、自分としてはよく解けた方だと思う。今回が2回目のCTFだったが、今後も色々勉強しながらまた機会があれば挑戦して行きたい。