魚脳の池

CTF:Little Twoos

InterKosenCTF 2020 writeup

チームOJI(Little Twoos)として参戦,5276点で6位でした. ほとんどはチームメイトが解いてくれたので,多分まだ見てない問題を中心に解いた.

No pressure

問題

from Crypto.Cipher import ARC4
from hashlib import sha256
from base64 import b64encode
import zlib
import os

flag = open("./flag.txt", "rb").read()

nonce = os.urandom(32)
while True:
    m = input("message: ")
    arc4 = ARC4.new(sha256(nonce + m.encode()).digest())
    c = arc4.encrypt(zlib.compress(flag + m.encode()))
    print("encrypted! :", b64encode(c).decode())

解き方

zlibが使われたため,おそらく後ろの入力が前のflag文字列とかぶれば,文字列の長さはそれほど増やさないと判断し,KosenCTF{を一文字ずつ入れてみて長さを見てみると,長さはうまく81のままだったため,長さを81以下なる入力をbruteforceで当てに行ったらうまく解けた.

solver

from pwn import *
from base64 import b64decode
from string import printable

flag = 'KosenCTF{'

io = remote('misc.kosenctf.com',10002)
head = 'message: '
while True:
    for x in printable:
        tmp = flag + x
        io.readuntil(head)
        io.sendline(tmp)
        l = io.readline()
        c = l.split(':')[-1].strip()
        d = b64decode(c)
        if len(d) <= 81:
            flag += x
            print flag
            break
    if flag[-1] == '}':
        break

harmagedon

問題

4択でどんどん展開しにいく問題,最終的に自分が選択した文字列がフラグになる.
バイナリファイルをghidraに投げる
1. ロジックの部分はsyscallが多用
2. ケツにめっちゃ長い文字列がある

解き方

whileから抜ける条件は以下のようになる

while c != 0xb77c7c:
    c = (c + input_idx)<<2

ですのでまず文字数を絞る,足し算の部分を無視して純粋に4の何乗を見れば大体な長さが11でわかる
その後は全探索で当てに行けば何番を選べばいいのかが計算できて,最後はそのまま入力すればok

solver

# coding: utf-8
for i in itertools.product([1,2,3,4],repeat=11):
    c = 0
    for x in i:
        c += x
        c = c << 2
    if c == 0xb77c7c:
        print i
# output: (2, 3, 1, 3, 1, 3, 2, 4, 1, 3, 3)
'''
which is your choice? [fRD3]R
which is your choice? [Ymug]u
which is your choice? [kcJQ]k
which is your choice? [yhtP]t
which is your choice? [uDPJ]u
which is your choice? [05n7]n
which is your choice? [V0Np]0
which is your choice? [8GFr]r
which is your choice? [Dar3]D
which is your choice? [UDi8]i
which is your choice? [KS3c]3
congratz. your choices are the flag
'''

bitcrypto

問題

server.pyが配布され,サーバにbitごとの暗号化/復号化のアルゴリズムが実装されたことが判明
具体的に暗号化/復号化は↓な感じ

enc:
(n,z) -> pubkey
x -> random(2,n)
1: pubkey.z * x**2
0: x**2

dec:
for c in ciphertext:
    t = c ** ((privkey.p-1)//2)
    if t == 1:
           m = 1
    else :
           m = 0

queryは2つの入力条件があり
1. 8文字以下 2. keyword含まれない
tokenも2つの条件がある
1. 同じ0の位でも異なる値が求められる
2. 値は全部正数

解き方

暗号化された値を復号化の式に代入すると
${1: z^{(p-1)//2}x^{p-1}}$←①
${0: x^{p-1}}$←②

pは素数のため,フェルマーの小定理により,式①の結果は前半の部分で,式②の結果は常に1になるはず.
なのでここは以下の方法で解く
1. 0(0b110000)を送り,暗号化された結果をもらう
2. 01の結果を一個ずつ取る
3. 0は必ず式②の結果を1にしないと行けないため${getPrime(256)2}$で増殖し,1は基本なんでもokなんで${x_1・2i}$でバリエーションを増やす
4. 最終的に送れば高い確率?で通るはず

solver

from pwn import *
from Crypto.Util.number import *

keyword = 'yoshiking, give me ur flag'
k_bin = ''.join([b for b in "{:b}".format(bytes_to_long(keyword))])

query = '0' # 110000

io = remote('crypto.kosenctf.com',13003)
#io = remote('localhost',10001)
io.readuntil('your query: ')
io.sendline(query)
enc = io.readline()
enc = eval(enc.split(':')[-1].strip())
assert len(enc) == 6
print enc
x_1 = enc[0]
x_0 = enc[2]
print io.readuntil('your token: ')

token = []
for idx,x in enumerate(k_bin):
    if x == '1' :
        token.append(x_1 * pow(2,idx))
    else:
        t = getPrime(256)
        token.append(t**2)
token = repr(token).replace('L','')
assert len(token.split(',')) == len(k_bin)
io.sendline(token)
print io.readline()
print io.readline()

ochazuke

問題

ECDSAの問題で,サーバ側はochazuke以外の文字列の署名の発行がやってくれる,なんやかんやでochazukeの署名作ればokという予測

解き方

普段なECDSAとはkの求め方です
k = Zn(ZZ(sha1(message).hexdigest(), 16)) * private_keykはmessagesha1値に依存することが判明し,sha1collisionで同じ結果にすることが可能
またkを一致すればprivate_keyを割り出すことも可能
よって流れは以下のうように
1. sha1collisionでハッシュ値が同じpdf生成
2. サーバに送りそれぞれの署名をもらえる
3. private_keyを割り出す
4. ochazukeの署名を自分で計算する
5. サーバに送る

solver

from pwn import *
from binascii import hexlify
from hashlib import sha1
from Crypto.Util.number import *

with open('shattered-1.pdf','rb') as f:
    pt1 = f.read()
with open('shattered-2.pdf','rb') as f:
    pt2 = f.read()

pt1 = hexlify(pt1)
pt2 = hexlify(pt2)

ip = 'crypto.kosenctf.com'
port = 13005

# io = remote(ip,port)
# io.readline()
# pubkey = [98664527284046924431103876265370791373438293020179316375883642857046660842422,51449822108608164116773906593599196539335313713052966364410874461652593273305]
# io.readuntil(':')
# io.sendline(pt1)

# # sig1
# sig1 = io.readline()
# sig1 = eval(sig1.split(':')[-1])
# print sig1
# io.close()


# io = remote(ip,port)
# io.readline()
# io.readuntil(':')
# io.sendline(pt2)

# # sig2
# sig2 = io.readline()
# sig2 = eval(sig2.split(':')[-1])
# print sig2

r1,s1 = (31150389226879548734307094288693105064656618066963344177653161349106620886514L, 12841807904550618931158792373525971859617703967016459921869962189487250843902L)
r2,s2 = (31150389226879548734307094288693105064656618066963344177653161349106620886514L, 96420393187933516068408660378146591249838645116318546612272298822307206485147L)

h = int('38762cf7f55934b34d179ae6a4c80cadccbb7f0a',16)

n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 # EC.order()

privkey = ((int(pt1,16) - int(pt2,16)) * inverse(h*(s1-s2),n)) % n
print privkey

io = remote(ip,port)
sig = '(98165594340872803797719590291390519389514915039788511532783877037454671871717, 115665584943357876566217145953115265124053121527908701836053048195862894185539)'
io.readline()
io.readuntil(':')
io.sendline(pt2)
io.readline() # your signature
print io.readuntil(':')
io.sendline(sig)
print io.readline()

署名用スクリプトはサーバスクリプトそのまま

from Crypto.Util.number import bytes_to_long
from binascii import unhexlify
from hashlib import sha1
from sage.all import *
import re

def sign(private_key, message):
    z = Zn(bytes_to_long(message))
    k = Zn(ZZ(sha1(message).hexdigest(), 16)) * private_key
    assert k != 0
    K = ZZ(k) * G
    r = Zn(K[0])
    assert r != 0
    s = (z + r * private_key) / k
    assert s != 0
    return (r, s)
private_key = 313681195146870630150443675574660225833
EC = EllipticCurve(
    GF(0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff),
    [-3, 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b]
)
n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 # EC.order()
Zn = Zmod(n)
G = EC((0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
        0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5))
print(sign(private_key,b'ochazuke'))

GoogleCTF 2020 android

GoogleCTF 2020 android

初見

アプリを開く


keys goes hereになんか入力しCHECKを押して正解かどうかが判断してくれるようです,flagは正解のあと表示するか,入力内容はflagになるなのかの2通りが想定する.

解析

静的解析

apkをjadxに投げる同時にapktoolでばらします

  1. AndroidManifest.xmlをチェックし,ActivityやLayoutもそれぞれ一個しかいないことが確認
  2. jadxの結果にerrorで一番見たいところが見れないが,EditText,TextViewとButtonの定義はとにかく確認できた
  3. jadxからcom.googlectf.sandbox配下にクラス一個しかいないに対し,apktool方のはő$1.smaliő.smali2つのsmaliがあるが不自然でした
  4. 念の為objectionのFRIDA-DEXdumpを使い,jadxから全部開いて確認したが,やはり結果は変わらなかった

    動的解析

    objection使って今読み込んだactivityやら,インスタンスやらを見てみた

adb shell
su
/data/local/tmp/frida-server &

まずroot済みのスマホ上にfrida-serverを起動する

objection -g com.google.ctf.sandbox explore 
android hooking list activities
> com.google.ctf.sandbox.ő
> Found 1 classes
android hooking list class
> ...
> com.google.ctf.sandbox.ő
> com.google.ctf.sandbox.ő$1
> ...
android hooking list class_methods com.google.ctf.sandbox.ő
> protected void com.google.ctf.sandbox.ő.onCreate(android.os.Bundle)
> Found 1 method(s)
android hooking list class_methods com.google.ctf.sandbox.ő$1
> public void com.google.ctf.sandbox.ő$1.onClick(android.view.View)
> Found 1 method(s)

上の結果からボタンのonClick関数はどうやらő$1の方にあることが判明.
そして他の関数が見かけないことからonClick関数の中に文字判定が含まれると睨んだ.

smaliをひたすら読む(罠に引っかかる)

そうすると,自分の中にő$1の動作をわかるためにsmaliを読むしかなかったため,素直にドキュメントを観ながらő$1.smaliを読んでみた.
smaliはレジスターベースなので,読んでるうちにレジスター値の変化に対応しないといけません,自分はそれがなれなくて,膨大な時間をかかった
ő$1.smaliは概ね以下の3つの部分に構成された.
1. 長い文字列
2. なんかのfor文
3. 残りの判断ロジック

smaliの序盤は上のような操作の繰り返しとなり,配列にASCII文字をどんどん入れてることがわかる
それを全部復元したらApparently this is not the flag. What's going on?という長さ49の文字列が発見したが,もちろんこれがflagでも正解でもなく,ただの罠だと思う(多分)

先に読み進むと上の内容が見られて,EditTextつまり入力欄に入っていた文字列の長さは48ぴったりではないとバツになるので,入力内容の長さは48で間違いないでしょう.

cond_2には48文字を4文字1ブロックに分けここではtmp_blockにする,tmp_blockの末尾(0x4番)から逆順で↑のbit演算が絡めた処理を行う.唯一の違いはそれぞれ0x18,0x10,0x8,0x0位左シフトすること.その結果を一つの配列に格納,そうすると長さ12の配列が出来上がり.

さらに読み進むと,配列の値に対して↑の処理を行う,ő.classを見てみるとちょうどこの関数だけがうまくデコンパイルされて,それが↓のような関数でした.

    public static long[] m0(long a, long b) {
        if (a == 0) {
            return new long[]{0, 1};
        }
        long[] r = m0(b % a, a);
        return new long[]{r[1] - ((b / a) * r[0]), r[0]};
    }

smaliの最後にinvという文字列も見えたので,拡張ユークリッド法にすぐ気づき,配列の値に対して全部その逆を求め,入れ直された.
ここで一旦配列を探すことに切り替える.Android Studioを開き,[Attach Debugger to Android Process]をクリックし,問題アプリのプロセスを選択.手元にソースコードがないため,ブレークポイントが思うように設置できないけど,Exceptionの2箇所(?)にどうやら止めるらしいので,とりあえず入れる.
そこで入力欄に仮に'CTF{'+'a'*43+'}'みたいな長さ48の答えを入れ,CHECKを押すと,↓のような2つの配列が発見,ő.őは進むことに連れてどんどん値が更新される.そこで奇しくも最初の値が一致し,上の配列の先頭がCTF{になり,フラグにほぼ確定.

this$0 = {ő@9697} 
 class = {long[12]@9781} 
  0 = 40999019
  1 = 2789358025
  2 = 656272715
  3 = 18374979
  4 = 3237618335
  5 = 1762529471
  6 = 685548119
  7 = 382114257
  8 = 1436905469
  9 = 2126016673
  10 = 3318315423
  11 = 797150821
 ő = 0
 ő.ő = {long[12]@9782} 
  0 = 40999019
  1 = 1633771873
  2 = 1627389952
  3 = 0
  4 = 0
  5 = 0
  6 = 0
  7 = 0
  8 = 0
  9 = 0
  10 = 0
  11 = 0

更に,ő.smaliを確認したら,同様な配列の存在が確認でき,これでflag決定.

pythonに書き換える

javaが使い慣れていないため,ő$1.smalipythonコードに一旦書き換えて,解き方を考えるようにした.

input_array = 'CTF{'+'a'*43+'}'
assert len(input_array) == 48
l = len(input_array)
chunks = l // 4

idx = 0 # v5
sl = [0x271986b,0xa64239c9,0x271ded4b,0x1186143,0xc0fa229f,0x690e10bf,0x28dca257,0x16c699d1,0x55a56ffd,0x7eb870a1,0xc5c9799f,0x2f838e65]
rl = [0]*12
while idx < chunks:
    tmp_a = ord(input_arrya[idx * 4 + 3]) << 0x18
    v = tmp_a
    tmp_b = ord(input_arrya[idx * 4 + 2]) << 0x10
    v |= tmp_b 
    tmp_c = ord(input_arrya[idx * 4 + 1]) << 0x8
    v |= tmp_c
    tmp_d = ord(input_arrya[idx * 4])
    v |= tmp_d
    rl[idx] = v

for x,y in zip(sl,rl):
    if x != inverse(y,0x100000000):
        print('❌')
        break
else:
    print('🏁')

to solve

そうすると4文字ずつbruteforceすればよいので,あとは回すだけ.

from string import ascii_letters,digits,printable
from Crypto.Util.number import inverse
import itertools
import json

m = 0x100000000
# candi = ascii_letters + digits + '{_!?.}'
candi = '{_!?. }@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
# candi = printable
target = [0x271986b,0xa64239c9,0x271ded4b,0x1186143,0xc0fa229f,0x690e10bf,0x28dca257,0x16c699d1,0x55a56ffd,0x7eb870a1,0xc5c9799f,0x2f838e65]


def round(a,b,c,d):
    tmp_a = ord(a) << 0x18
    v = tmp_a
    tmp_b = ord(b) << 0x10
    v |= tmp_b 
    tmp_c = ord(c) << 0x8
    v |= tmp_c
    tmp_d = ord(d)
    v |= tmp_d
    return v

flag = ''


for tt in target[1:]:
    print(tt)
    for comb in itertools.product(candi,repeat=4):
        a,b,c,d = comb
        r = round(a,b,c,d)
        if inverse(r,m) == tt:
            tmp = d + c + b + a
            flag += tmp
            print(flag)
            break

感想

最近割とAndroidを勉強したものの,Smaliを読むことがなかったため,すごく勉強になりました. あと久しぶりに点を入れたので,解けてホッとした...

SECCON Beginners CTF 2020 writeup

SECCON Beginners 2020 writeup

f:id:Gyonou:20200526031510p:plain 同期と一緒にチーム Little Twoosとして参加しました,チーム内では「普段取り組めないジャンルを挑戦する」という方針を予め決めており,自分は今回暗号ではなく,リバーシングをメインに解いてみました.

rev

mask

root@ae548dfbc2cd:/CTF/2020/ctf4b/mask# ./mask
Usage: ./mask [FLAG]
root@ae548dfbc2cd:/CTF/2020/ctf4b/mask# ./mask abcdefg
Putting on masks...
a`adede
abc`abc
Wrong FLAG. Try again.

まずはバイナリを動かしてみる,自分は時々回すこと忘れ,慌ててghidraとか開けちゃう癖があるので,そろそろ自分なりのアプローチ方法を決めなきゃ...
本題に戻ると,どうやら[FLAG]を与えるとそれが正解かどうかを検証してくれるプログラムらしい.あと出力の中に特に真ん中の二行が気になります.
ghidraでバイナリを開き,main関数をデコンパイルして仮のc言語のコードに戻します.

undefined8 main(int iParm1,long lParm2){

...
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  if (iParm1 == 1) {
    puts("Usage: ./mask [FLAG]");
  }
  else {
    strcpy((char *)flag,*(char **)(lParm2 + 8)); //flagを受け取り
    fLen = strlen((char *)flag); //flagの長さ
    fLen_int = (int)fLen;
    puts("Putting on masks...");
    count = 0;
    while (count < fLen_int) {
      local_98[(long)count] = flag[(long)count] & 0x75;// flag文字ごと0x75と論理積を取る
      local_58[(long)count] = flag[(long)count] & 0xeb;// flag文字ごと0xebと論理積を取る
      count = count + 1;
    }
    local_98[(long)fLen_int] = 0;
    local_58[(long)fLen_int] = 0;
    puts((char *)local_98);
    puts((char *)local_58);
    iVar1 = strcmp((char *)local_98,"atd4`qdedtUpetepqeUdaaeUeaqau");//0x75と論理積を取った文字列を検証
    if ((iVar1 == 0) &&
       (iVar1 = strcmp((char *)local_58,"c`b bk`kj`KbababcaKbacaKiacki"), iVar1 == 0)) {//0xebと論理積を取った文字列を検証
      puts("Correct! Submit your FLAG.");
    }
    else {
      puts("Wrong FLAG. Try again.");
    }
  }
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
...
}

上のコードは多少ghidraと違いがあります(デコンパイルwindowでL押せば書き換える).
コメントどおり,第2引数のflagは片方文字ごと0x75と論理積をとってatd4...と一致するのかを検証.
片方文字ごと0xebと論理積を取ってcbbk...と一致するのかを検証.
よって,方針としては:

 
FLAG\ \&\ chr(0x75)*len=atd4...\\
FLAG\ \&\ chr(0xeb)*len=c'bb...

この2つの式を満たすflagを当てにう\行くことです.

ここで最近revのwriteupで見かけたz3を使って計算しました.

from z3 import *


l = 29
mask1 = 0x75
mask2 = 0xeb
ct1 = b'atd4`qdedtUpetepqeUdaaeUeaqau'
ct2 = b'c`b bk`kj`KbababcaKbacaKiacki'


solver = Solver()
flag = [BitVec('b%i' %i,8) for i in range(l)]
m1 = [BitVec('b%i' %i,8) for i in range(l)]
m2 = [BitVec('b%i' %i,8) for i in range(l)]

for i in range(l):
    m1[i] = flag[i] & mask1
    m2[i] = flag[i] & mask2

    solver.add(m1[i] == ct1[i])
    solver.add(m2[i] == ct2[i])
    solver.add(And(flag[i]>=33,flag[i]<=126))


solver.add(flag[0] == ord("c"))
solver.add(flag[1] == ord("t"))
solver.add(flag[2] == ord("f"))


if solver.check() == sat:
    m = solver.model()
    print(''.join([chr(m[f].as_long()) for f in flag]))

z3はMMAさんのwikiを参考しながら書いてみた.
上のコードは2つの等式に基づき,更に色々制限条件を付け加えました,これによって解くスピードが変わります(多分).
ここでz3使うのが多少オーバーキルかもしれませんが,論理演算自分はちょっと苦手なので,確実な方法を選びました.

yakisoba

まずは下調べです.
とりあえずchecksecでセキュリティ機構を確認した.

checksec yakisoba
[*] '/CTF/2020/ctf4b/yakisoba/yakisoba/yakisoba'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

結構対策されてる様子.次はghidraに投げてデコンパイルしてみた.

ulong FUN_00100680(void)

{
  int flag;
  uint uVar1;
  long in_FS_OFFSET;
  byte input [40];
  long local_20;
  
  uVar1 = 1;
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  __printf_chk(1,"FLAG: ");
  flag = __isoc99_scanf(&DAT_001010fb,input);
  if (flag != 0) {
    uVar1 = check_flag(input);
    if (uVar1 == 0) {
      puts("Correct!");
    }
    else {
      uVar1 = 0;
      puts("Wrong!");
    }
  }
  if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
    return (ulong)uVar1;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

どうやら読み取ったflagをcheck_flag(実際デコンパイルされた関数名は違う)関数でチェックしてるようで,更にcheck_flag関数に飛んでいくと,めちゃくちゃ長いif文が現れて,ちょっとだけ読んでみたが,すぐ眠くなったので,main関数とcheck_flag関数のコードを全文コピーして新しいcコードを書き,コンパイルしたバイナリをangrに投げる作戦に変更.

import angr

project = angr.Project('./problem',auto_load_libs=False)

state = project.factory.entry_state()
simgr = project.factory.simulation_manager(state)
simgr.explore(find=0x0401282,avoid=0x040128e)
state = simgr.found[0]

print(state.posix.dumps(0))

↑はangrのコードですが,もう一回gdbやghidraを開き,puts("Correct!");puts("Wrong!");のアドレスを調べる.
そしてfindのところにputs("Correct!");のアドレスを入れ,avoidのところにputs("Wrong");のアドレスを入れれば「avoidのアドレスまで行かず,findのアドレスまで実効できる」ちょうどいい引数をangrが自動に探してくれるコードになります,自分も1,2回しか使ったことがなかったので,詳しい使い方はまだ勉強する必要がある.

ghost

なんか検索したらpostscriptだと判明.
仕様書?的なものをよんで,頑張って半分ぐらいまでコードを読んだが,睡魔が再び襲わってきたので,とりあえずそのまま動かしてctf4b{を入力したら,結果が3417 61039 39615 14756 10315 49836まさか配布されたoutput.txt最初の結果と一致したことが気づき,bruteforceにした.

from pwn import *
from string import printable

output = '3417 61039 39615 14756 10315 49836 44840 20086 18149 31454 35718 44949 4715 22725 62312 18726 47196 54518 2667 44346 55284 5240 32181 61722 6447 38218 6033 32270 51128 6112 22332 60338 14994 44529 25059 61829 52094'
output = output.split(' ')

flag = 'ctf4b{'
for idx in range(6,len(output)):
    for x in printable:
        io = process(['/usr/bin/gs','chall.gs'])
        io.sendline(flag+x)
        io.recvlines(3)
        r = io.recvline().strip().split(' ')[-1]
        if r == output[idx]:
            flag += x
            print(flag)
            break
        else:
            io.close()

subprocessが久しぶりすぎて,書き方忘れたのでpwntoolで無理やり書いてみました,とりあえずctf4b{をベースにして後ろに一文字ずつ追加してgs chall.gsに投げて,その結果はあってるいるのかを確認したコードになります.
ここから余談ですが,postscriptに気になって,一晩明けにもう一回読んでみました.

/flag 64 string def 
/output 8 string def 
(%stdin) (r) file flag readline 
not { (I/O Error\n) print quit } if 
0 1 
2 index 
length { 1 index 1 add 3 index 3 index get xor mul 1 
    463 { 1 index mul 64711 mod } repeat 
    exch pop dup 
    output cvs print ( ) print 
    128 mod 1 add exch 1 add exch } repeat 
(\n) print quit

無理矢理にインデントと開業をつけて少し読みやすくなると思う.
上のコードは
1. flagとoutputというstring型の変数を定義
2. 標準入力からflagを入手 (stack: flag,true)
3. 「trueになってるのかを検証し,popする」的な操作
4. push 0 push 1
5. 2 indexはスタックtopからカウントして2番目(topのindexは0)をまたpushするらしい(stack: flag 0 1 flag)
6. length{...}repeat...操作をlength回を実行する意味で一回おいておきます.
7. 肝心なloop内の内容ですが,コメントつけてみた

1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat 
# get使ってflagの一文字ずつ取り出す,ここは便宜上iにします.
# flag[i]はまずi+1とxorをとり,その結果をされに463乗にしてmodulus 64711にした

exch pop dup 
# いらないものをpop

output cvs print ( ) print 
# 上の計算結果をcvsでstringにcastしprintした,後ろに空白を更に追加

128 mod 1 add exch 1 add exch 
# modulus 128は謎だが,残り i+=1的なものです.

上のpostscriptをpythonに書き換えてみたら

for i in range(len(flag)):
    t = ord(flag[i])^(i+1)
    x = str(pow(t,463,64711))
    print x

これがこれで解けるはずです.

siblangs

SECCON珍しくandroidの問題です,最近割とandroidを勉強したため,意地でも解けたい一心で取り組んだ.
実際apkをインストールしてみたら,どうやらflagを入力してvalidate AとValidate Bでflagをチェックしてるたった一つのactivityのアプリらしい
さらにソースコードの抽出については,apkファイルに対して個人的にapktoolsをunzip両方をやった,その違いと目的は以下のようになります.

  • apktools
    • androidmanifest.xmlが読める
    • valueとかのsourceファイルは見れる
  • unzip
    • dex-(dex2jar)->jar-(unzip)->.classによってjavaで読める

やったこと:

  • androidmanifest.xmlを読んで,どうやらReactNativeで書かれたことがわかる
  • 入力欄に予めctf4bが表示されたのでそれをvscodeにctrl+shift+fでctf4bを検索した.
    そこに難読化されたjsファイルが発見
   function v() {
    var t;
    (0, l.default)(this, v);
    for (var o = arguments.length, n = new Array(o), c = 0; c < o; c++) n[c] = arguments[c];
    return (t = y.call.apply(y, [this].concat(n))).state = {
        flagVal: "ctf4b{",
        xored: [34, 63, 3, 77, 36, 20, 24, 8, 25, 71, 110, 81, 64, 87, 30, 33, 81, 15, 39, 90, 17, 27]
    }, t.handleFlagChange = function(o) {
        t.setState({
            flagVal: o
        })
    }, t.onPressValidateFirstHalf = function() {
        if ("ios" === h.Platform.OS) {
            for (var o = "AKeyFor" + h.Platform.OS + "10.3", l = t.state.flagVal, n = 0; n < t.state.xored.length; n++)
                if (t.state.xored[n] !== parseInt(l.charCodeAt(n) ^ o.charCodeAt(n % o.length), 10)) return void h.Alert.alert("Validation A Failed", "Try again...");
            h.Alert.alert("Validation A Succeeded", "Great! Have you checked the other one?")
        } else h.Alert.alert("Sorry!", "Run this app on iOS to validate! Or you can try the other one :)")
    }, t.onPressValidateLastHalf = function() {
        "android" === h.Platform.OS ? p.default.validate(t.state.flagVal, function(t) {
            t ? h.Alert.alert("Validation B Succeeded", "Great! Have you checked the other one?") : h.Alert.alert("Validation B Failed", "Learn once, write anywhere ... anywhere?")
        }) : h.Alert.alert("Sorry!", "Run this app on Android to validate! Or you can try the other one :)")
    }, t}

どうやらflagはッッ前半後半に分かれて検証されてるみたい,後半は訳わからなかったので,とりあえず置いといて,前半はflagとAkeyios10.3とxorを取ってるのが丸見えでした.

   output = 'AKeyForios10.3'
    l = len(output)
    flag = []
    xored = [34, 63, 3, 77, 36, 20, 24, 8, 25, 71, 110, 81, 64, 87, 30, 33, 81, 15, 39, 90, 17, 27]
    for idx in range(len(xored)):
        tmp = ord(output[idx%l]) ^ xored[idx]
        flag.append(tmp)
    print ''.join(map(chr,flag))
↑のpythonコードで復元できるはず

後半はこのjsと関係なさそうなんで,javaソースコードを見てみるとValidateFlagModule.classはあやしい.
AES使って暗号化してその結果を検証してるみたいなコードっぽい,しかも比較する結果やkeyまであるので,それをそのまま実行してみたら,flagはそのまま出た

    import java.util.*;
    import javax.crypto.Cipher;
    import javax.crypto.SecretKey;
    import javax.crypto.spec.GCMParameterSpec;
    import javax.crypto.spec.SecretKeySpec;

    public class Main {
        public static void main(String[] args) throws Exception {
        // Your code here!
            SecretKey secretKey = new         SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES");
            byte[] arrayOfByte = new byte[43];
            arrayOfByte[0] = 95;
            arrayOfByte[1] = -59;
            arrayOfByte[2] = -20;
            arrayOfByte[3] = -93;
            arrayOfByte[4] = -70;
            arrayOfByte[5] = 0;
            arrayOfByte[6] = -32;
            arrayOfByte[7] = -93;
            arrayOfByte[8] = -23;
            arrayOfByte[9] = 63;
            arrayOfByte[10] = -9;
            arrayOfByte[11] = 60;
            arrayOfByte[12] = 86;
            arrayOfByte[13] = 123;
            arrayOfByte[14] = -61;
            arrayOfByte[15] = -8;
            arrayOfByte[16] = 17;
            arrayOfByte[17] = -113;
            arrayOfByte[18] = -106;
            arrayOfByte[19] = 28;
            arrayOfByte[20] = 99;
            arrayOfByte[21] = -72;
            arrayOfByte[22] = -3;
            arrayOfByte[23] = 1;
            arrayOfByte[24] = -41;
            arrayOfByte[25] = -123;
            arrayOfByte[26] = 17;
            arrayOfByte[27] = 93;
            arrayOfByte[28] = -36;
            arrayOfByte[29] = 45;
            arrayOfByte[30] = 18;
            arrayOfByte[31] = 71;
            arrayOfByte[32] = 61;
            arrayOfByte[33] = 70;
            arrayOfByte[34] = -117;
            arrayOfByte[35] = -55;
            arrayOfByte[36] = 107;
            arrayOfByte[37] = -75;
            arrayOfByte[38] = -89;
            arrayOfByte[39] = 3;
            arrayOfByte[40] = 94;
            arrayOfByte[41] = -71;
            arrayOfByte[42] = 30;
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, arrayOfByte, 0, 12);
            cipher.init(2, secretKey, gCMParameterSpec);
            arrayOfByte = cipher.doFinal(arrayOfByte, 12, arrayOfByte.length - 12);
            String flag = new String(arrayOfByte);
            System.out.println(flag);
        }
    }

web

spy

最初全然わからなかった,チームメンバーから「ユーザによってloginのresponseを受けるまでの時間がかなり異なる場合がある」というヒントをもらい,もう一回ソースコード見てみると,dbに存在んしたuserだと謎のcalc_password_hashによって返すまでの時間がましたことがわかり,とりあえず全ユーザのusernameを投げて,画面の一番下に書いてた所要時間とペアで出力して,ダントツで時間がかかったuserをマーク提出したらflagが降ってきた

# coding: utf-8
import requests
import subprocess
import re

with open('employees.txt') as f:
    em = f.read().strip().split('\n')

url = 'https://spy.quals.beginners.seccon.jp/'

for name in em:
    payload = {'name':name,'password':'abcdefghijklmn'}
    r = requests.post(url,data=payload).text
    t = re.findall(r'\d+\.\d\d\d\d\d\d\d',r)
    print(t,name)

crypto

Noisy equations

最後ほかのチームメンバーはおそらくpwnのhardに集中してると思い,残ったcrypto問題を解いてみた.

from os import getenv
from time import time
from random import getrandbits, seed


FLAG = getenv("FLAG").encode()
SEED = getenv("SEED").encode()

L = 256
N = len(FLAG)


def dot(A, B):
    assert len(A) == len(B)
    return sum([a * b for a, b in zip(A, B)])

coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)]

seed(SEED)

answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs]

print(coeffs)
print(answers)

問題のソースコードと結果からわかったこととしては:

  • coeffsの長さは44ー>flagの長さも44
  • getrandbitsは怪しい
  • ノイズは二箇所
    • dot(coeff,FLAG) -> flag文字ごとに○倍にしてその和をとる
    • + getrandbits(L)-> 一回seedでgetrandbitsをresetした -> 毎回同じ

これから自分の下手くそ日本語解説より直接式を見たほうがわかりやすいと思う


R_{1}=sum(Matrix_{coeffs1}*Vector_{flag})+noise\\
R_{2}=sum(Matrix_{coeffs2}*Vector_{flag})+noise\\
Diff_{coeffs}=Matrix_{coeffs1}-Matrix_{coeffs2}\\
Diff_{R} = R1-R2\\
Diff_{coeffs}*Vector_{flag} = Diff_{R}

あとは連立方程式みたいに求めるとflagが出た,今回はまたせっかくなのでsageを使いました.

from sage.all import *


with open('cache') as f:
    ct = f.read().strip().split('\n')

coeffs1 = eval(ct[0])
answers1 = eval(ct[1])


with open('cache1') as f:
    ct = f.read().strip().split('\n')

coeffs2 = eval(ct[0])
answers2 = eval(ct[1])


def minus(A,B):
    return [a-b for a,b in zip(A,B)]


diff = minus(answers1,answers2)
diff_coeffs = [minus(x,y) for x,y in zip(coeffs1,coeffs2)]


left = Matrix(QQ,diff_coeffs)
right = vector(diff)

sol = left.solve_right(right)
show(sol)
print(''.join(map(chr,sol)))

所感

久しぶりに同期と一緒にワイワイCTFをやってて楽しかったです.同期たちが強すぎて,自分ももうちょっと精進しないと...

angstromctf 2020 writeup

angstromctf 2020 writeup

チーム Little Twoosとして参戦して、最終的に66位でした
開催時間長いため、普段やらないreversingとmiscなどにもてだして、なんとかメンバーと協力して何問を解くことができた

misc

ws1 (misc 30pts)

recording.pcapng recordingというpcapファイルが渡された。ちょっとスクロールしたら、HTTPのパケットが発見し、クリックしたらflagはそこにあった。flag=xxx

ws2 (misc 80pts)

ws2-1 recording.pcapngの中にスクロールしながら探してみたら、flag.jpgらしき文字列が発見、follow->TCP streamで確認する。 ws2-2 すると一個下にJFIFというJPEGと関わる文字列があり、後にマジックナンバーも一致した。HTTPパケットのフィールドを詳しく見てみると、MIMEの下にJPEG File Interchange Formatというのがありexportしたらflag.jpgがあった ws2-3

ws3 (misc 180pts)

相変わらずpcapが渡されて、見てみたら、どうやらgitのやり取りでした、恐らくgit pushとかのコマンドによる通信だと思う(間違えたらごめん)。そこで、PACKという文字列が目に入り、gitと合わせて検索したらgitのパッケージ的なことが判明、それをexportして、git (repository)の復元作業を行った。git packについて詳しい説明は1こちらの解説を参考にした。

git unpack-objects < xxx.pack #git objectを復元、最終的に
git cat-file -p {hash}     #復元

ws3-1

clam clam clam (misc 70pts)

問題サーバーに接続してみたら、ばーとclamclamみたいな文字列が出てきて、しかもflag形式で出現したから、恐らく大量のclamの中に混じっているじゃないかと推測してとにかく大量に受信してctrl-fで探したら、ヒットした

clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
# coding: utf-8
from pwn import *
io = remote('misc.2020.chall.actf.co',20204)
io.sendline('clamclam')
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)

Shifter (misc 160pts)

問題サーバーにつないでみたらどうやらフィボナッチ数列の1-50番目の値分シーザー暗号に打ち込めばOKでしたので、先にフィボナッチの50番目に計算しといて、シーザー暗号で復号化した。

Solve 50 of these epic problems in a row to prove you are a master crypto man like Aplet123!
You'll be given a number n and also a plaintext p.
Caesar shift `p` with the nth Fibonacci number.
n < 50, p is completely uppercase and alphabetic, len(p) < 50
You have 60 seconds!
--------------------
Shift IVSKGTCJETZVRWFRHMNSTPQYHMBXZXHKHI by n=16
:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sympy as sym #sympyのインポート
from pwn import *
from m1z0r3 import *

def Fib(n):
    x = sym.symbols('x',nonnegative=True,integer=True) #変数xの宣言(非負の整数)
    Fib=1/sym.sqrt(5)*(((1+sym.sqrt(5))/2)**(n-1)-((1-sym.sqrt(5))/2)**(n-1)) #一般項の式
    result=Fib.subs(x,n) #xにnを代入
    result=result.evalf() #式を浮動小数点数として評価
    return int(result)

Fiblist=[]
for n in range(1,51):
    Fiblist+=[Fib(n)]

def rotx(x,s):
    r = ''
    for i in s:
        if ord('z') >= ord(i) and ord(i) >= ord('a'):
            r += chr((ord(i)-ord('a')+x)%26+ord('a'))
        elif ord('Z') >= ord(i) and ord(i) >= ord('A'):
            r += chr((ord(i)-ord('A')+x)%26+ord('A'))
        else:
            r += i
    return r

io = remote('misc.2020.chall.actf.co',20300)
print io.recvuntil('--------------------')
for i in range(50):
    io.recvuntil('Shift')
    task = io.recvuntil('\n')
    print task
    n = int(task.split('=')[-1])
    ct = task.split(' ')[1]
    pt = rotx(Fiblist[n],ct)
    print pt
    io.recvuntil(':')
    io.sendline(pt)
io.interactive()

msd (misc 140pts)

LSBのステガノグラフィー方法のMSD(digit)バージョン。 まず問題スクリプトを見てみると、単純に各RGB値の最初の桁にflagのASCII配列をすり替えただけに見えるが、実際1->0の桁落ちや百の位で1/2->3/4/5...に発生するオーバーも考慮しなきゃいけない、だから元の画像が配布されたと思う。解決策としては、エンコードされた画像と元の画像を比較しながら、桁の違いによってそれぞれ対処する感じ。 具体的に: 1. 1,2桁のときかつ桁数一致->最大の位を抽出 2. 3桁かつ元の画像RGBの値は255じゃない->最大の位を抽出、255->とりあえずxで埋める 3. ほかのときは桁落ちなので0で埋める

そしてその出力からactf{のASCII配列9799116102123を検索すればflagがでるはず

from PIL import Image

im = Image.open('output.png')
im2 = Image.open('breathe.jpg')

width, height = im.size



def encode(i, d):
    i = list(str(i))
    i[0] = d

    return int(''.join(i))
    
msb = []

for j in range(height):
    for i in range(width):
        for idx,a in enumerate(im.getpixel((i,j))):
            _n = len(str(a))
            b = im2.getpixel((i,j))[idx]
            _n2 = len(str(b))

            if _n == 2 and _n == _n2:
                msb.append(str(a)[0])
            elif _n == 3 and a == 255:
                if im2.getpixel((i,j))[idx] == 255:
                    msb.append(str(a)[0])
                else:
                    msb.append('x')
            elif _n == 1 and _n == _n2:
                msb.append(str(a)[0])
            else:
                msb.append('0')

print(''.join(msb))        

crypto

Keysar(crypto 40pts)

どうやらkey ANGSTROMCTFを使ってflagを暗号化した->agqr{yue_stdcgciup_padas} key付きのシーザー暗号自分聞いたことないので、key caesarで検索したらそれのonline toolが出てきて打ち込んだら一発に出た

Reasonably Strong Algorithm (crypto 70pts)

n = 126390312099294739294606157407778835887
e = 65537
c = 13612260682947644362892911986815626931

問題分見たらnが小さくて、素因数分解できると思ってfactordb使ってp,qを吐かせて、そのまま復号化のスクリプトを書いた。

from Crypto.Util.number import *

p = 9336949138571181619
q = 13536574980062068373
phi = (p-1) * (q-1)
d = inverse(65537,phi)
ct = 13612260682947644362892911986815626931
n = 126390312099294739294606157407778835887

print long_to_bytes(pow(ct,d,n))

Wacko Images (crypto 90pts)

問題スクリプトとその暗号化された画像が配布された。 見てみると 画像[index](RGB値) * key[index] % 251のような簡易な暗号化でしたので、それの逆算スクリプトを書いて実行したらflagの画像が出た

key = [41, 37, 23]
for x in range (0, a):
    for y in range (0, b):
        pixel = img[x, y]
        for i in range(0,3):
            pixel[i] = pixel[i] * key[i] % 251
        img[x][y] = pixel
from Crypto.Util.number import *
from PIL import Image
from numpy import *

ct = Image.open('enc.png')

img = array(ct)

key = [41,37,23]
n = 251
a,b,c = img.shape

for x in range(0,a):
    for y in range(0,b):
        pixel = img[x,y]
        for i in range(0,3):
            pixel[i] = pixel[i] * inverse(key[i],n) % n
        img[x][y] = pixel

pt = Image.fromarray(img)
pt.save('flag.png')

wacko

Confused Streaming (crypto 100pts)

急に思い出せなくなったのでちゃんと合ってるのか自信がないです...まず問題文を見てみる。注目するのはkeystreamという関数、keyは他の処理で二次元関数の解になる。最後のyield int((ret//1)%2)によって0か1が出力する感じでした。もしここでretは小数だったら全部0になるはず。そこでkeyをできるだけ小さくしてe,dをどう変換してもretを小数になれるkeyを探してサーバーに送ればflag ^ ¥x00*len(flag)のような2進数が降って来ると思う。
ちなみに、自分はa=1 b=4 a=-2にしたはず...

def keystream(key):
    random.seed(int(os.environ["seed"]))
    e = random.randint(100,1000)
    while 1:
        d = random.randint(1,100)
        ret = Decimal('0.'+str(key ** e).split('.')[-1])
        for i in range(d):
            ret*=2
        yield int((ret//1)%2)
        e+=1

one time bad (crypto 100pts)

問題文を見てみると、randomによってp,s,xが生成され、xだけが見せられるような挙動でした。ゴールとしては正しいpを送ることですが、sがわからないとうまくできない。
そこでrandom.seed(int(t))というrandomの初期設定が接続たびに行われて、tもその時のunixtimeであることも判明。そこで接続する時のtimeを記録すれば、randomをサーバーと同じ初期設定ができることによって同じp,s,xが手元に生成することが可能になった。そのpを送ればflagがでると思う、もしできなかったら、unixtimeが多少早かったかもしれないので、微調整すればOKのはず

def genSample():
    p = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(random.randint(1, 30))])
    k = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(len(p))])

    x = otp(p, k)

    return x, p, k
# 省略
t = time.time()
random.seed(int(t))
import random, time
import string
import base64
import os
from pwn import *

def otp(a, b):
    r = ""
    for i, j in zip(a, b):
        r += chr(ord(i) ^ ord(j))
    return r


def genSample():
    p = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(random.randint(1, 30))])
    k = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(len(p))])

    x = otp(p, k)

    return x, p, k



#base_time = int(time.time())-1000000
#base_time = 1584147600
now_time = int(time.time())-10
io = remote('misc.2020.chall.actf.co',20301)
#io = remote('localhost',10001)
print(io.recvuntil('> '))
io.sendline('2')
recv_x = io.recvline().strip()
print(recv_x)
io.recvuntil('answer: ')

# for t in range(base_time,now_time):

while True:
    random.seed(now_time)
    x,p,k = genSample()
    x_pre = base64.b64encode(x.encode())
    print(x_pre)
    
    if x_pre == recv_x:
        print('bingo')
        print(p)
        io.sendline(p)
        io.interactive()
        break
    else:
        pass
        #print 'wrong'
        # io.sendline(str(10))
        # ans = io.recvline()
        #print base_time
    now_time += 1


RSA-OTP (crypto 210pts)

他のメンバーのアイディアで協力して解けた問題、ここでは自分の理解のもとに書きます、あってなかったらごめんなさい。 まず問題をみてみると、以下の内容がわかる
1. enc(flag)が降ってくる
2. 暗号化関数は普通のRSA
3. decryptがやってくれる
4. ↑の結果はgetrandbits(1)に邪魔される

そこでRSAのLSBが容易に思いつくが、getrandbits(1)によって使えないですが、decrypt(input)のbit数は把握できる。そこで以下の試みをしました(と思う): 1. flagを左シフトできるようにpow(2,e,n)をどんどんかけていく
python for i in range(464): inp = pow(2,x*e,n)*enc(flag) io.sendline(inp) 当たり前のようにxの増加につれてエられた2進数のbit数は増えます

  1. 仮にx1=460,x2=461にして、その中間値をかけて送ったらどうなるか
    python 1020 1019 1020 ... 1020と1019の間に徘徊することがわかりました、ですので
    (flag * a % n).bit_length() => [1019,1020]
    上の式のa部分は二分探索によってある程度絞られる  flagは近似的にlong_to_bytes(pow(2,1019,n)/a)に等しいといえる よってaを二分探索するようなスクリプトを書いてみた
from pwn import *
from Crypto.Util.number import *
from fractions import Fraction

n = 136018504103450744973226909842302068548152091075992057924542109508619184755376768234431340139221594830546350990111376831021784447802637892581966979028826938086172778174904402131356050027973054268478615792292786398076726225353285978936466029682788745325588134172850614459269636474769858467022326624710771957129
e = 0x10001

# Encrypted flag:
ct = 17482644844951175640843255713372869422739097498066773957636359990466096121278949693816080016671592558403643716793132479255285512907247513385850323834210899918531077167485767118313722022095603863840851451191536627814100144146010392752308431038754246815068245448456643024387011488032896209253644172833489422733
io = remote('crypto.2020.chall.actf.co',20600)


# left = pow(2,460,n)
# right = pow(2,461,n)
# twomod = 1

bounds = [Fraction(pow(2,463,n)),Fraction(pow(2,464,n))]
while True:
    # middle = (left + right)//2
    middle = sum(bounds)/2
    middle = middle.numerator / middle.denominator
    # print long_to_bytes(pow(2, 1019, n) / (middle))
    print long_to_bytes(pow(2, 1022, n) / (middle))
    print middle
    if middle == 0:
        print middle 
        break
    io.recvuntil('Enter message to sign: ')
    io.sendline(str(ct * pow(middle,e,n) % n ))
    io.recvline()
    revflag = io.recvline().strip()
    
    print len(revflag)
    
    # if len(revflag) == 1020:
    if len(revflag) == 1023:
        bounds[1] = sum(bounds)/2
        #right = middle
    else:
        bounds[0] = sum(bounds)/2
        #left = middle

これによってflagの58文字分はわかるようになった actf{this_is_not_what_i_meant_when_i_told_you_to_use_rsa_w 残りの12文字はメンバーのプロパワーによってguessingできた 最後flagはactf{this_is_not_what_i_meant_when_i_told_you_to_use_rsa_with_padding} もっと良い方法があればぜひ教えてくれたら幸いです。

reversing

普段あんまりreversingやらないので手探り状態で簡単な何問しか解けなかった

Revving up (rev 50pts)

とりあえず実行して、支持に従ってgive flagを入力したら、「Now run the program with a command line argument of "banana" and you'll be done!」ということで、./revving up bananaで実行し、give flag入力したらflagが出た。

root@ae548dfbc2cd:/CTF/2020/actf/rev# ./revving_up
Congratulations on running the binary!
Now there are a few more things to tend to.
Please type "give flag" (without the quotes).
give flag
Good job!
Now run the program with a command line argument of "banana" and you'll be done!
root@ae548dfbc2cd:/CTF/2020/actf/rev# ./revving_up banana
Congratulations on running the binary!
Now there are a few more things to tend to.
Please type "give flag" (without the quotes).
give flag
Good job!
Well I think it's about time you got the flag!

Windos Opportunity (rev 50pts)

IDAとかで開いたらflagはそこに書いてあった...

Taking Off (rev 70pts)

ghidraデコンパイルしたら、mainは↓のようになった

undefined8 main(int iParm1,long lParm2)

{
  int iVar1;
  undefined8 uVar2;
  size_t sVar3;
  long in_FS_OFFSET;
  uint local_b4;
  uint local_b0;
  uint local_ac;
  int local_a8;
  int local_a4;
  char *local_a0;
  byte local_98 [136];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("So you figured out how to provide input and command line arguments.");
  puts("But can you figure out what input to provide?");
  if (iParm1 == 5) {
    string_to_int(*(undefined8 *)(lParm2 + 8),&local_b4,&local_b4);
    string_to_int(*(undefined8 *)(lParm2 + 0x10),&local_b0,&local_b0);
    string_to_int(*(undefined8 *)(lParm2 + 0x18),&local_ac,&local_ac);
    iVar1 = is_invalid((ulong)local_b4);
    if (iVar1 == 0) {
      iVar1 = is_invalid((ulong)local_b0);
      if (iVar1 == 0) {
        iVar1 = is_invalid((ulong)local_ac);
        if ((iVar1 == 0) && (local_ac + local_b0 * 100 + local_b4 * 10 == 0x3a4)) {
          iVar1 = strcmp(*(char **)(lParm2 + 0x20),"chicken");
          if (iVar1 == 0) {
            puts("Well, you found the arguments, but what\'s the password?");
            fgets((char *)local_98,0x80,stdin);
            local_a0 = strchr((char *)local_98,10);
            if (local_a0 != (char *)0x0) {
              *local_a0 = 0;
            }
            sVar3 = strlen((char *)local_98);
            local_a4 = (int)sVar3;
            local_a8 = 0;
            while (local_a8 <= local_a4) {
              if ((local_98[(long)local_a8] ^ 0x2a) != desired[(long)local_a8]) {
                puts("I\'m sure it\'s just a typo. Try again.");
                uVar2 = 1;
                goto LAB_00400bc7;
              }
              local_a8 = local_a8 + 1;
            }
            puts("Good job! You\'re ready to move on to bigger and badder rev!");
            print_flag();
            uVar2 = 0;
            goto LAB_00400bc7;
          }
        }
      }
    }
    puts("Don\'t try to guess the arguments, it won\'t work.");
    uVar2 = 1;
  }
  else {
    puts("Make sure you have the correct amount of command line arguments!");
    uVar2 = 1;
  }
LAB_00400bc7:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}


そこからわかった事としては
1. iParm1 == 5 => 引数は4 ./taking_off a1 a2 a3 a4 2. local_ac + local_b0 * 100 + local_b4 * 10 == 0x3a4a1*10+a2*100+a3 == 932./taking_off 3 9 2 a4 3. iVar1 = strcmp(*(char **)(lParm2 + 0x20),"chicken") => a4 = "chicken" 4. if ((local_98[(long)local_a8] ^ 0x2a) != desired[(long)local_a8])さらなるpasswordが求められ、passwordの文字のそれぞれ0x20とxorとった結果をdesiredと比較してるみたい 最後はdesiredに保存される5a464f4b594f0a4d435c4f0a4c464b4d2aを↑の方法でxorを取るとpasswordはgive me flag\x00と判明最後向こうのサーバーで実行したらflagを取れた

# coding: utf-8
from pwn import *
from Crypto.Util.number import *
des = 0x5a464f4b594f0a4d435c4f0a4c464b4d2a
des = long_to_bytes(des)
print xor(des,chr(0x2a)*len(des))

Autorev,Asseble! (rev 125pts)

ghidraデコンパイルしたらすごい分岐の多いスクリプトが出てきた、自力で読める自信がなく、一か八かで先日インストールしたangrでやってみた。
いくつのチュートリアルを参考して、とりあえず基本のスクリプトを書いた。そこでfindは正解した時表示する関数のアドレスで、avoidは間違えた時表示する関数のアドレスになる

import angr

project = angr.Project('./autorev_assemble',auto_load_libs=False)

state = project.factory.entry_state()
simgr = project.factory.simulation_manager(state)
simgr.explore(find=0x0408953,avoid=0x0408961)
state = simgr.found[0]

print(state.posix.dumps(0))

↑のスクリプトを回して、flagそのまま出た

Patcherman (rev 100pts)

if ((_DAT_00201054 != 0xffffffff) && (_DAT_00201050 == 0x1337beef))この分岐判断の結果によってプログラムがフリーズするかどうかがわかる。
後に_DAT_00201054ptrace(0,1337,4919,1337)の返り値だと判明し成功したら0エラーしたら-1を返す
_DAT_00201050は既存のやつで0x1337beefと違う値が入ってる
そこで↑の条件全部を仮に成立させて、次のスクリプトをそのままコピペして実行したらflagが出た
(solverは汚くてごめん...)

#include <stdio.h>

int dword_601050 = 322420463;
int dword_6010AC = 322420463;
int dword_601054 = 0;
int dword_601060 [] = {0x5cd883ea,0xef6680c0,0xafcc7086,0x7e3ab87f,0x7dc5773a,0x38208035,0x0456c3f5};

int sub_4006FF(int a1, int a2)
{
  return a1 + a2;
}

int sub_400672()
{
  int v0; // ebx
  int v1; // ebx
  int v2; // ebx
  int v3; // eax
  v0 = sub_400657(dword_6010AC, 0);
  v1 = (unsigned int)sub_400657(dword_6010AC, 2) ^ v0;
  v2 = (unsigned int)sub_400657(dword_6010AC, 3) ^ v1;
  v3 = sub_400657(dword_6010AC, 5);
  dword_6010AC = (unsigned int)dword_6010AC >> 1;
  dword_6010AC |= (v2 ^ v3) << 31;
  return (unsigned int)dword_6010AC;
}

int sub_400657(int a1, int a2)
{
  return (a1 >> a2) & 1;
}

int main(){
    int dword_601090 [7];
    for ( int i = 0; i <= 6; ++i )
    {
        int v3 = sub_400672();
        
        dword_601090[i] = sub_4006FF(dword_601060[i] ^ (unsigned int)dword_601054, v3);
    }
    printf("Here have a flag:\n%s\n", dword_601090);
}

Califrobnication (rev 120pts)

ディレクトリにあるflag.txtを↓にあるスクリプトを経てエンコードされた文字列が表示される

  memfrob(local_48,__n);
  strfry(local_48);
  printf("Here\'s your encrypted flag: %s\n",local_48);
  • memfrobはただ文字ごと42とxorを取るだけそんな気にしない
  • strfryはshuffle関数で配列を撹乱する たが、他のメンバーによるとstrfryのrandomstateという初期設定は時間とpid依存であることが判明
    そこでホームディレクトリに以下のpythonスクリプトを置き、問題ディレクトリに入り、pythonスクリプトを実行して、暗号文を獲得する同時にtimeとpidを入手する
import  time
import subprocess
from subprocess import Popen
import os
import signal

cmd = "/problems/2020/califrobnication/./califrobnication"
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
print( "process id = %s" % proc.pid )
out = proc.stdout.readline().strip()
with open('/home/team5469/out-hex.txt','w') as f:
    f.write(out.encode('hex'))
print(time.time())

そこから配布されたc言語ファイルを基づいて、strfryを書き換え、pidとtimeをすり替え,flagのところにstring.printable[:48]を変えて実行

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>

// typedef struct random_data {
//         int     *fptr;
//         int     *rptr;
//         int     *state;
//         int     rand_type;
//         int     rand_deg;
//         int     rand_sep;
//         int     *end_ptr;
// } RANDOMD;
// time -= 1
char * strfry_mod (char *string) { static int init; static struct random_data rdata; int t = 1584547877; if (!init) { static char state[32]; rdata.state = NULL; initstate_r (t ^ 7269, state, sizeof (state), &rdata); init = 1; } size_t len = strlen (string); if (len > 0)
    for (size_t i = 0; i < len - 1; ++i)
      {
    int32_t j;
    random_r (&rdata, &j);
    j = j % (len - i) + i;

    char c = string[i];
    string[i] = string[j];
    string[j] = c;
      }

  return string;
}


int main() {
    FILE *f;
    char flag[50];
    f = fopen("flag.txt", "r");
    fread(flag, 50, 1, f);
    strtok(flag, "\n");
    //memfrob(&flag, strlen(flag));
    strfry_mod(&flag);
    printf("Here's your encrypted flag: %s\n", &flag);
}

これでprintable[:48]をどのようにshuffleされるのがわかるようになって、このパターンを基づいて組み直したらflagが出た

# coding: utf-8
from string import printable
ct = '48657265277320796f757220656e6372797074656420666c61673a2043474543574b75754f18124e4e4c1e4c4c484b4b5845441b4e465e44494b1f131e1f511b1a494c755e49451c58434b49'.decode('hex')
ct = ct.split(' ')[-1]
t = ''
for i in range(0, len(ct)):
    t += chr(ord(ct[i]) ^ 42) 
flag = [0 for _ in range(48)]
a = printable[:48]
pattern = 'n9bhL0du7IBH5iKw3l8Gjstvyg2mefJDzA4ECocaqFkx6rp1'
for x,y in zip(pattern,t):
    
    idx = a.index(x)
    flag[idx] = y
    
print ''.join(flag) 

感想

5日開催マジで長すぎて、結構疲れた...また色々他ジャンルの問題に挑戦できてすごくいい経験になった

tags: ctf writeup

SECCON CTF 2019 QUALS write-up

チームm1z0r3として参加(傍観)しました。まともに問題を解けず、主にスポートに専念しましたので、以下は自分が考察してた問題と他の方のwrite-up見て自分で実装した問題を軽く解説します。

(競技中)解けた問題

coffee_break [Crypto,56pts]

なんの変哲のない問題です、割とすぐ方針を決めて実装に移した。
まず問題です↓:

import sys
from Crypto.Cipher import AES
import base64


def encrypt(key, text):
    s = ''
    for i in range(len(text)):
        s += chr((((ord(text[i]) - 0x20) + (ord(key[i % len(key)]) - 0x20)) % (0x7e - 0x20 + 1)) + 0x20)
    return s


key1 = "SECCON"
key2 = "seccon2019"
text = sys.argv[1]

enc1 = encrypt(key1, text)
cipher = AES.new(key2 + chr(0x00) * (16 - (len(key2) % 16)), AES.MODE_ECB)
p = 16 - (len(enc1) % 16)
enc2 = cipher.encrypt(enc1 + chr(p) * p)
print(base64.b64encode(enc2).decode('ascii'))
# FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905

問題の流れ:
1. key1使ってflagに対して一文字ずつ何らかの操作を行う
2. key2使って先程変形されたflagに対してAES-ECBを仕掛ける
3. 最後はbase64エンコードを行う

key1key2が貰い物なので、手順1までは無難に戻せる、
ここで手順1にある何らかの操作は特に深堀りの必要がない、
なぜならflagは表示できる文字列のため、brute-forceが可能となり、一文字ずつ当たればflagが出る。
イメージ:
fx(flag,key1) = AES.decrypt(Base64.decode(ct),key2)
あとはbrute-forceの実装をすればOK。
競技中の実装は他のメンバーがやってくれたが、ここで自分の実装を載せます

from string import printable
import base64
import sys
from Crypto.Cipher import AES
from Crypto.Util.number import *
from binascii  import hexlify, unhexlify

enc = "FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905"
key1 = "SECCON"
key2 = "seccon2019"
def bf(key,ct):
    flag = ''
    for idx in range(43):
        for t in printable:
            s = chr((((ord(t) - 0x20) + (ord(key[idx % len(key)]) - 0x20)) % (0x7e - 0x20 + 1)) + 0x20)
            if s == ct[idx]:
                flag += t
                break

    print flag

enc_decbase64 = base64.b64decode(enc)
print (len(enc_decbase64))

cipher = AES.new(key2 + chr(0x00) * (16 - (len(key2) % 16)), AES.MODE_ECB)
enc_decaes = cipher.decrypt(enc_decbase64)
print (hexlify(enc_decaes))

enc_no_padding = enc_decaes[:-5]

print (hexlify(enc_no_padding))

        
bf(key1,enc_no_padding)

Sandstorm [misc 279pts]

問題として黒の斑点が目立つ画像をもらいました。
f:id:Gyonou:20191101175509p:plain
一見訳わからなかったので、とりあえずexiftool使ってみました
f:id:Gyonou:20191101211101p:plain Adam7 Interlaceという見慣れていない単語が見つけた、プラス元画像にAdamというメッセージがあったため可能性としてはゼロではないので、一応メンバーに報告して、一旦離脱しました。
それからほかのメンバーがAdam7の仕様に基づいて実装したらflagになるQRコードが出たとの事。
ここで一応Adam7 Interlaceについて、簡単に説明すると、
それは画像表示のアルゴリズムの一種である、ある順番にピクセルを表示することによって重い(?)画像をまずなんとなく模様が見れるようにするアルゴリズム

https://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Adam7_interlacing_representation.gif/220px-Adam7_interlacing_representation.gif
wikiの例が超絶にわかりやすい。
そこでみそになる表示の順番ですが、
画像を8*8のブロックに分割して、↓にある数字順に表示します。
今回の問題はの1のところ、つまり一番左上にあるピクセルだけ抽出すれば、QRコードになれるとのことでした。

1 6 4 6 2 6 4 6
7 7 7 7 7 7 7 7
5 6 5 6 5 6 5 6
7 7 7 7 7 7 7 7
3 6 4 6 3 6 4 6
7 7 7 7 7 7 7 7
5 6 5 6 5 6 5 6
7 7 7 7 7 7 7 7

抽出の方法についてですが、もちろんpillowなど画像系のライブラリを使ってゴリゴリやるのも当然解けるですが、ここではちょっと別の方法を紹介します。
使うのがImageMagickです

ImageMagick で PNG の形式を変換 - awm-Tech
↑こちらを参考にとりあえずコマンドを打てみました
$ convert sandstorm.png -filter Point -fx "!(i%8)*!(j%8)*u" sandstorm-adam7-1.png -fxオプションはある程度のプログラミングっぽいオペレーションを実行するような役割です。
uは今扱っている画像のソースを意味して、!(i%8)*!(j%8)その中にピクセルの座標がともに8の倍数のときだけ抽出する事を意味します。
こうすると画像は↓なります。 f:id:Gyonou:20191101215412p:plain
QRコードらしき模様が見えました、そこからさらに8*8のブロックを全部左上のピクセルにしてみたらQRコードが無事に発見、スキャンしたらフラグが出た。
f:id:Gyonou:20191101215618p:plain

(競技後)解けた問題

ZKPay [Crypto]

正直あんまり触れたくない問題ですね、まさかマイナスを入れるなんて思わなかった、というのが最初やったとき実は小数も試しに入れてみたけど、小数点が向こうに取られて、数字だけ残されるようになったのでなんか符号全般取り除いてるなと勝手に思い込んちゃった...

Crazy Repetition of Codes [Crypto]

CRCint('1'*10000)回というありえない事象です。この問題も割りと早い段階で「CRCの結果は2の32乗通りしかない、どっかでループしてる」との事を気づいたが、その裏付けになる鳩ノ巣理論やそれに対応できる実装が思い浮かばなく(2の32乗は計算不可能という固定概念が抱えてた...)、解けずじまいでした。
詳しい解説はfalconctfのduckさんのwriteupにあるので、ここは簡単に方針と実装を紹介して終わりにしたいと思います。
まず速い言語のCRC実装をググって2の32乗を回して(c++だと多分5分ぐらいで終わるのかな)、その最後5回ぐらいの結果を出してみたら、きちんと全部2**32-1回の時点で0になるため、int('1'*10000)の結果は実質int('1'*10000)%(2**32-1)回ました時の結果が一緒になるはず、一気に計算可能になりました。
そこから全部のCRCを計算しつなげてAESで復号化すればflagが出る。
CRC計算のcpp例は↓

#include <vector> // vectorを使うためにインクルードする
#include <iostream>
#include <string>
#include <stdint.h>
#include <cstring>
using namespace std;
#include <stdlib.h>
#include <algorithm> //find
#include <time.h>
#include <math.h>
#include <list>
#include <iterator> // std::next()関数が所属するヘッダ

static const unsigned int crc32tab[256] = { 
    0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
    0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
    0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
    0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
    0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
    0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
    0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
    0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
    0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
    0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
    0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
    0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
    0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
    0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
    0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
    0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
    0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
    0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
    0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
    0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
    0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
    0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
    0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
    0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
    0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
    0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
    0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
    0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
    0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
    0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
    0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
    0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
    0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
    0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
    0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
    0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
    0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
    0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
    0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
    0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
    0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
    0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
    0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
    0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
    0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
    0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
    0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
    0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
    0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
    0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
    0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
    0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
    0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
    0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
    0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
    0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
    0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
    0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
    0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
    0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
    0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
    0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
    0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
    0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
};


unsigned int crc32(unsigned int crcinit,char *p, int len)
{
    //unsigned int crcinit = 0;
    unsigned int crc = 0;

    crc = crcinit ^ 0xFFFFFFFF;
    for (; len--; p++) {
        crc = ((crc >> 8) & 0x00FFFFFF) ^ crc32tab[(crc ^ (*p)) & 0xFF];
    }
    return crc ^ 0xFFFFFFFF;
}
unsigned int exploit(unsigned int crcinit,char *p,int len,long long max){
    long long i = 0;
    long long c = 0;
    c = crcinit;
    while (i < max){
        c = crc32(c,p,strlen(p));
        i += 1;
    }
    return c;
}

int main() {
    vector<char*> pt{"TSG","is","here","at","SECCON","CTF!"};
    long long i = 0;
    unsigned int c = 0;
    unsigned int c0 = 0;
    clock_t start = clock();
    long long m = 169873741;

    for(auto x : pt) {
        long long res = exploit(c,x,strlen(x),m);
        printf("crc for %s : %x\n",x,res);
    }

}

所感

去年より面白い問題が多い気がして、個人的に悔しい部分が多かった、また自分の実力不足に痛感した大会になりました。
学生として最後のSECCON予選となり、決勝に行けたらいいな... (1106)

リファレンス

CTFと共に生きる

ImageMagick で PNG の形式を変換 - awm-Tech

Adam7 algorithm - Wikipedia

Padding Oracle系の問題を解いてみた

今週の勉強会は自分が担当だったので、前からずっと取り扱いかったPadding Oracle Attackの問題を繰り出しました。

基本

概略

  • 共通鍵ブロック暗号のCBCモードに対する攻撃手法
  • 14年SSL3.0のPOODLEという脆弱性もこの手法

CTFに置いての位置付け

  • ちょくちょく見る
  • 初見としはうざい

特徴 (あくまでも経験上の話)

CBCモード              ←必須
AES類のブロック暗号         ←必須
サーバーが復号化をやってくれる
サーバーコード多くの場合は公開
IVまでこっちから送られる       ←場合によって

何ができる?

  • Decryption Attack 難易度:高(特にEncryption Attackと融合する時)
    暗号文の復号ができる
    平文はflagの場合

  • Encryption Attack 難易度:中
    暗号文の改造による平文の改ざん
    平文を特定なコマンド/データに改ざんし認証をバイパスできる

解法

詳しい解説は↓にある自分のスライドをみてください

Encryption Attack

要はCBCモードに暗号化された(既知)平文を自分が思う通りにすり替える手法

例題: INS'hack 2019 Jean-Sébastien Bash

競技中解けなかった問題です、ちょうど当てはまるので解いてみた。
まずはserver.py

inshack-2019/server.py at master · InsecurityAsso/inshack-2019 · GitHub
少し読んでみると以下のものがわかるようになりました:
* unpad失敗したらWhat do you mean?が返ってくる
* /cmd {暗号文}の後ろの暗号文をサーバーが復号化してos.system()で内容(コマンドに想定)を実行

netcatでサーバーを立てて接続して、色々探ってみたら

Welcome on my server. /help for help

>/help
/help
This is a tool so that only me can execute commands on my server
(without all the GNU/Linux mess around users and rights).

- /help for help
- /exit to quit
- /cmd <encrypted> to execute a command

Notes (TODO REMOVE THAT) ---------------------------
Ex:
/cmd AES(key, CBC, iv).encrypt(my_command)
/cmd 986b60ec3b7beef7bfd1642a25d20f02


>/cmd 986b60ec3b7beef7bfd1642a25d20f02
/cmd 986b60ec3b7beef7bfd1642a25d20f02
Running ls -l
合計 8
-rw-r--r-- 1 root root   21  6月  9 00:22 flag.txt
-rw------- 1 root root    0  6月  9 00:21 nohup.out
-rw-r--r-- 1 ingo ingo 2047  6月  9 00:19 server.py

どうやら例としてはls -lを暗号化したようです。
server.pyと同じディレクトリにflag.txtを置いてました。
しかも暗号文の長さは32ということはたった1ブロックだったことです。 lsではなくて、cat flagだったらflagが見えるはずということでEncryption Attackを起用。

方針としては:
* 長さは32かつunpadもうまくunpadも通る暗号文を探す
* Encryption Attackによって後ろの1ブロックを; cat flag.txt\x02\x02にすり替える

イメージとしては:

https://dl.dropboxusercontent.com/s/x77pmkuqk8f7u90/jean_bash.png?

最後diffと後ろの暗号文とくっつけてサーバーに送れば潰された1ブロック目を飛ばして、後ろのcat flag.txtが実行されるはず。
solverは↓

# -*- coding: utf-8 -*-
from pwn import *
from Crypto.Util.number import long_to_bytes,bytes_to_long
from binascii import hexlify, unhexlify
import os
import random
import string
from Crypto.Cipher import AES
import sys
import signal


ip,port = 'localhost',6000
io = remote(ip,port)
print io.readuntil('>')
# 32byte復号してくれる暗号文を探す
ct = ''
n = 0
cmd = '/cmd {}'
pt_base = ''
while True:
  ct = hex(n)[2:].zfill(64)
  io.sendline(cmd.format(ct))
  print io.readline()
  rep = io.readline()
  print rep,len(rep)
  if 'What' in rep:
    n += 1
    io.readuntil('>')
    continue

  # Runningの後ろの部分を切り取る
  pt_base = rep.strip()[8:]
  if len(pt_base) >= 30 and len(pt_base) <= 32:
    # pt_base
    io.readuntil('>')
    break
  print io.readuntil('>')
  n += 1
def pad(s):
    return s + (16 - len(s) % 16) * chr(16 - len(s) % 16)
#padding付き w\x8a\x8b\xb6\x1d\x9a|\x89E\x1c\xbcS:<8\x0fa\x81= N-k + ¥x02¥x02
print '[+] start exploit'
target = pad('; cat flag.txt')
print pt_base[16:]
print len(pt_base)
d = xor(pad(pt_base[16:]),target)
assert len(d) == 16
print d
# assert xor(diff,pad(pt_base[16:])) == target
ct = hexlify(d) + ct[32:]
assert len(ct) == 64
io.sendline('/cmd {}'.format(ct))
while True:
  print io.readline()

Decryption Attack

平文が知らない/知りたいときはこの手法を使います。padding方式を逆手にとって、知りたいブロックを全部paddingに変換させまだxorを取る前のDec(ct)の値を求める。←さえ分かれば平文やそのあとEncryption Attackも展開できます。

例題: picoctf 2018 magic padding oracle

まずはサーバーコード

github.com

読んでみたら以下のようなものがわかりました:
* cookieusername,is_admin,expiresこの三つから構成されたjson形式
* is_admin==trueexpiresのtimestampは「現在」の時間より未来の方であれば認証が通る
* unpad失敗したらinvalid paddingが返ってくる

サーバーがいまだに生きているので、接続してみたら

Welcome to Secure Encryption Service version 1.51

Here is a sample cookie: 5468697320697320616e204956343536a2e32fc2e97693e94706c97a85bb71cb23c7271421a8435ba8d6abbbb5e11fd5a27a82dba1480f03948f90ff0f122eeb397e8aa13e44c1e4fcfd7293cf99bf3b340213f70e50b6039a84189af70e5840
What is your cookie?

cookieはiv+暗号文、プラス暗号文の長さは80
全体としての方針としては:
* まずDecryption Attackで平文の全貌を暴く
* ↑を基づいて改ざんして認証をバイパス

今回こそちゃんと理解したいので、モジュールを頼らず実装を挑戦してみました。
まずはDecryption Attackの部分
* 最後の2ブロックから復号するから、基本[iv+block1+block2]の形式でサーバーに送る
* ブロック内も最後のバイトから弄る、invalid paddingが出ない時はbrute-forceの成功を意味します
* 最終的に全部\x10にpaddingされた時、元の平文が復元できる

イメージとしては:

https://dl.dropboxusercontent.com/s/ln0h2f6azxpvqyz/decryption.png https://dl.dropboxusercontent.com/s/lvaswift1t5def0/decryption1.png

だんだん復号させた平文をpaddingに埋め尽くされにいって、最終的には全部paddingにする過程はDecryption attackのミソです。
この部分の実装は↓

for i in range(5,0,-1):
  b = ''
  for idx in range(15,-1,-1):
    #print 16-idx
    for d in range(256):
      if i == 5 and idx == 15 and d == 0:
        continue
      ip,port = 'localhost',6000
      #io = remote(ip,port)
      s,f = sock(ip,port)
      #io.readuntil('Here is a sample cookie: ')
      read_until(f,'Here is a sample cookie: ')
      sample_cookie = read_until(f).strip()
      iv,cookie = sample_cookie[:32],sample_cookie[32:]
      #io.readuntil('?')
      read_until(f,'?')
      #print 'cookie length is',len(unhexlify(cookie))
      cookie = unhexlify(cookie)
      iv = unhexlify(iv)
      assert len(cookie) % 16 == 0


      block = chunk(iv+cookie,16)
      #assert len(block) == 6
      current,prev = block[i],block[i-1]
      assert len(prev) == 16 and len(current) == 16
      x1 = left_pad(chr(d)+b)
      x2 = left_pad((16-idx-1) * chr(16-idx))
      x3 = left_pad(''.join([chr(j) for j in range(16-idx-1,0,-1)]))
      prev = xor(prev,xor(x1,xor(x2,x3)))
      #io.sendline(iv+hexlify(prev+current))
      s.send(hexlify(iv+prev+current)+'\n')
      #io.readline()
      read_until(f)
      #io.readline()
      read_until(f)
      #rep = io.readline()
      rep = read_until(f)
      #print rep
      if 'invalid' in rep:
        #print '[+] nope'
        continue
      else:
        print "0x{:02x}: ".format(d) + "{:02x}".format(16-idx) * (16-idx)
        b = chr(d) + b
        if idx == 0:
          print xor(xor(prev,'\x10'*16),block[i-1])
        break

動かす時はこんな感じ:

0x0c: 01
0x0f: 0202
0x0e: 030303
0x09: 04040404
0x08: 0505050505
0x0b: 060606060606
0x0a: 07070707070707
0x05: 0808080808080808
0x04: 090909090909090909
0x07: 0a0a0a0a0a0a0a0a0a0a
0x06: 0b0b0b0b0b0b0b0b0b0b0b
0x01: 0c0c0c0c0c0c0c0c0c0c0c0c
0x00: 0d0d0d0d0d0d0d0d0d0d0d0d0d
0x73: 0e0e0e0e0e0e0e0e0e0e0e0e0e0e
0x2d: 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
0x75: 10101010101010101010101010101010
}\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f
0x38: 01
0xb4: 0202
0x7e: 030303
0x93: 04040404
0x20: 0505050505
0x12: 060606060606
0xe4: 07070707070707
0xf1: 0808080808080808
0x3f: 090909090909090909
0x34: 0a0a0a0a0a0a0a0a0a0a
0x28: 0b0b0b0b0b0b0b0b0b0b0b
0x64: 0c0c0c0c0c0c0c0c0c0c0c0c
0x08: 0d0d0d0d0d0d0d0d0d0d0d0d0d
0xe2: 0e0e0e0e0e0e0e0e0e0e0e0e0e0e
0xac: 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
0x43: 10101010101010101010101010101010
s_admin": "true"

平文がわかった上に、Encryption Attackが展開できます。
すり替えたいcookieは↓
{"username": "guest", "expires":"2030-01-07", "is_admin": "true"}
少なくても二箇所が元のcookieと違い、1ブロックのEncryption Attackが済まないので、
いっそう全ブロックのEncryption Attackを実装しました。
まずはイメージ:

https://dl.dropboxusercontent.com/s/6zmzk5pubd05sni/step1.png

https://dl.dropboxusercontent.com/s/p4ftwishchcy51l/step2.png

https://dl.dropboxusercontent.com/s/ny4chb9s3sl23at/step3.png

基本はDecryption Attackが済み、Dec(ct)が分かる状態からさらに変化させる為に差分をつけ加えるとうまく欲しい平文が復号させることができます。
ですから実装もさっきのやつとだいたい一緒です。

wanted = '{"username": "guest", "expires":"2030-01-07", "is_admin": "true"}'
wanted = "This is an IV456" + pad(wanted)
wanted_chunck = chunk(wanted,16)
# encrypt attackでflagを取る
c = ['\x00'*16]
for i in range(5,0,-1):
  b = ''
  for idx in range(15,-1,-1):
    #print 16-idx
    for d in range(256):
      if i == 5 and idx == 15 and d == 0:
        continue
      ip,port = '118.27.6.108',6001
      #io = remote(ip,port)
      s,f = sock(ip,port)
      #io.readuntil('Here is a sample cookie: ')
      read_until(f,'Here is a sample cookie: ')
      sample_cookie = read_until(f).strip()
      iv,cookie = sample_cookie[:32],sample_cookie[32:]
      #io.readuntil('?')
      read_until(f,'?')
      #print 'cookie length is',len(unhexlify(cookie))
      cookie = unhexlify(cookie)
      iv = unhexlify(iv)
      #assert len(cookie) % 16 == 0


      block = chunk(iv+cookie,16)
      #assert len(block) == 6
      current,prev = block[i],block[i-1]
      assert len(prev) == 16 and len(current) == 16
      x1 = left_pad(chr(d)+b)
      x2 = left_pad((16-idx-1) * chr(16-idx))
      x3 = left_pad(''.join([chr(j) for j in range(16-idx-1,0,-1)]))
      current = xor(current,c[0])
      prev = xor(prev,xor(x1,xor(x2,x3)))
      #io.sendline(iv+hexlify(prev+current))
      s.send(hexlify(block[0]+prev+current)+'\n')
      #io.readline()
      read_until(f)
      #io.readline()
      read_until(f)
      #rep = io.readline()
      rep = read_until(f)
      #print rep
      if 'invalid' in rep:
        #print '[+] nope'
        continue
      else:
        print "0x{:02x}: ".format(d) + "{:02x}".format(16-idx) * (16-idx)
        b = chr(d) + b
        if idx == 0:
          dec = xor(prev,'\x10'*16)
          pt = xor(dec,block[i-1])
          diff = xor(pt,wanted_chunck[i])
          c = [diff] + c
          print xor(xor(diff,block[i-1]),dec)
        break

result = ''.join(c)
ip,port = 'localhost',6000
#io = remote(ip,port)
s,f = sock(ip,port)
#io.readuntil('Here is a sample cookie: ')
read_until(f,'Here is a sample cookie: ')
sample_cookie = read_until(f).strip()
assert len(unhexlify(sample_cookie)) == len(result)
print [ord(i) for i in result]
last = hexlify(xor(unhexlify(sample_cookie),result))
read_until(f,'?')
s.send(last+'\n')
while  True:
  print read_until(f)

所感

やはり難しかった、さらにこれをメンバーに説明するのも大変でした、でもいい経験になりました。

リファレンス

Padding Oracle AttackによるCBC modeの暗号文解読と改ざん - security etc...

SECCON Beginners CTF 2019 write-up

チームm1z0r3として参加(傍観)しました。開催時ちょうど出先だったので、まともに参加できず、warm-up以外はただ眺めて方針を言っただけでした。。。
帰ってからちゃんとcrypro全問とmisc一問解きました。

解けた問題

[warmup] So Tired[Crypto,115pts]

ちょうど電車の中なので、テザリングで問題を解いてた。

最強の暗号を作りました。 暗号よくわからないけどきっと大丈夫!

とやけに長い暗号文が与えられました。最後の方で5+K5v4B8HzgzA==が見えたので、とりあえずbas64でデコードしてみた。
結果をファイルに書き出しfileコマンドで見るとzlibデータということが判明。
zlib.decopress()かましたら、またbase64っぽいやつになった。
試行錯誤の末、この二つの繰り返しだと踏んでsolverを書いて一発でflagが出ました。
↓はそのsolverです

# coding: utf-8
for i in range(100):
    print i
    dd = base64.b64decode(ct)
    if 'ctf4b' in dd:
        print dd 
        break
    dd = zlib.decompress(dd)
    if 'ctf4b' in dd:
        print dd
        break
    
    
for i in range(1000):
    print i
    dd = base64.b64decode(dd)
    if 'ctf4b' in dd:
        print dd 
        break
    dd = zlib.decompress(dd)
    if 'ctf4b' in dd:
        print dd
        break

flagctf4b{very_l0ng_l0ng_BASE64_3nc0ding}

Party[Crypto,223pts]

Let's 暗号パーティ

あと暗号化用スクリプトと暗号文が渡されました。

#暗号化スクリプト  
from flag import FLAG 
from Crypto.Util.number import bytes_to_long, getRandomInteger, getPrime

def f(x, coeff):
    y = 0
    for i in range(len(coeff)):
        y += coeff[i] * pow(x, i)
    return y


N = 512
M = 3
secret = bytes_to_long(FLAG)
assert(secret < 2**N)

coeff = [secret] + [getRandomInteger(N) for i in range(M-1)]
party = [getRandomInteger(N) for i in range(M)]
val = map(lambda x: f(x, coeff), party)
output = list(zip(party, val))
print(output)
[(5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787, 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933), (3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473, 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075), (6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379, 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125)]

単純な多項式方程式の計算でした

$$a+bx+cx^2$$

のような式が三つがありました
よって連立してsympy使って解いた。

# solver.py
import sympy
from Crypto.Util.number import long_to_bytes
exec(open('encrypted').read().strip())
a,b,c = sympy.symbols('a b c')
expr = []

for x,y in z:
    tmp = a + b*x + c*x**2-y
    expr.append(tmp)

r = sympy.solve(expr,[a,b,c])
print long_to_bytes(r[a])

flag ctf4b{just_d0ing_sh4mir}

Go RSA[Crypto,363pts]

Nだけなくしちゃったんだよなあ……。
Server: nc 133.242.17.175 1337

問題文通りnetcatしてみたら
1. enc(flag)がくれる
2. 送りつける平文を暗号化してくれる* 3
3. 秘密鍵d
まぁ、Nがないわけですね。
最初2,3,5を送ってgcdでnを求めようと思ったが、上手くいけず
試しに-1を送ってみたらまさか通った、
eは奇数のため

$$ (-1)^e modn = -1 modn = c1 $$

Nはこれで入手できたので、あとは複合するだけです。

from gmpy import gcd,mpz
from Crypto.Util.number import long_to_bytes
from pwn import *
while True:
    ip,port = "133.242.17.175",1337
    io = remote(ip,port)
    c = io.recvline()
    c = eval(c[19:])
    io.recvuntil('> ')
    io.sendline('-1')
    c2 = eval(io.recvline().strip())
    print c2
    io.recvuntil('> ')
    io.sendline('3')
    c3 = eval(io.recvline().strip())
    io.recvuntil('> ')
    io.sendline('5')
    c5 = eval(io.recvline().strip())
    d = io.recvline()
    d = eval(d[10:])
    
    

    n = c2+1
    print n
    m = pow(c,d,n)
    print long_to_bytes(m)
    if 'ctf4b' in long_to_bytes(m):
        print long_to_bytes(m)
        break
    else:
        continue

flag ctf4b{f1nd_7he_p4ramet3rs}

Bit Flip[Crypro,396pts]

平文を1ビットランダムで反転させる能力を手に入れた!
File: bitflip.py
Server: nc 133.242.17.175 31337

from Crypto.Util.number import bytes_to_long
import random

N = 82212154608576254900096226483113810717974464677637469172151624370076874445177909757467220517368961706061745548693538272183076941444005809369433342423449908965735182462388415108238954782902658438063972198394192220357503336925109727386083951661191494159560430569334665763264352163167121773914831172831824145331
e = 3
FLAG = bytes_to_long(open('flag', 'rb').read())

r = 1 << random.randrange(0, FLAG.bit_length() // 4)
C = pow(FLAG ^ r, e, N)
print(C)

早々にcoppersmithのFranklin-Reiter Related Message Attackと気づいて、メンバーに言ったらすぐ解いてくれたが、自分が解く時結構苦戦しました。

inaz2.hatenablog.com

ももいろさんのコードをそのままやればできるはずだが、なぜか自分は最初diffの部分をbruteforceしようと思った。

from Crypto.Util.number import long_to_bytes
#exec(open('frank').read())
ct = [33460967930651042564008329494837980959989903859210697421802306279164621522687556814718235426111472547179095329371506382390014453846091397832498600503239318321010755518869476348035475014069822783161625493689769776352756995160846240078842031192708266413703448094909589244233878164639106160103912289438284532951,
35767470795467826626462219405218933905532798348984975613284120383492541445125994872372814500011529932489701983142275580480838210299890062839431655315781938554677350092881704540017377748405584214318021791801390834437575079426825881380116885654989459718950805516298665349023074086528990085624070703653325590626,
2564767480510465230025376237485323465247347695767670175449224103561779300575591945349037051568803129039497886094466895116814508667226331922270653397257175611435834481444137923198078723295284476543076934928703086503949903527333767882768961960752424916858651191341982753280463984103242342990373600472836273483,
48435977819599167293699473693205310917010250721248336492061287216379692891690691913767776069993911112239078185069288585022377993407270013401637539048567366526297730605589951913845745872046158333042631889854531385928428660788578942630939788805433004268454651608813773788184844655098499352542867632893087089862,
70243886202594041384149567279846398108812572645105073366462606332641138118730202337380722533868511370427257637609073439393551732783916350915670572090839624554072402040626243669086780305822849504543665527511573152185251528039856817426643797216433781977178180000264280600433349179693943419348506755320080926694,
56073186874991156857455984286684500442906235298857187632607776454964999796759688131079504400025542651062833165579892099321785638532878398946777427851711272910781669617329188445433612821472292131082921802664259975670362919592393670596478369004299401092384846933003092047569839327675819547737050008092855586836,
76973946533903301664341043538088060381425103867343158161579223936021109952721928226649404602045450132057340858205331299178226892039280164374466121273289679601927740750394666755935423240638326819231534726682109186304705616758011069263053347795504543107507654422375687528839427587126700855441423639423394856915,
49654154495482283465834374278541758019608540780454081817664398472317688626329722856959095884025794014552636483510868510209039491680134524145557941720491231286814385904119640028052534147728739758655222825827641976571227089805222280335420420592070351022229307985817960062100669012614681238209307730930907097822,
43127534228950588003064862597617662349110503750643598403895078065209780109199580967321519663285345035915141004019522531717660071908459330437728550039841396677044821855383201329486465004296535288616321397257850646054096663686972817521272024268602548417965146689582261300067924995202141674341056643542839881709,
57689275579576395277674386946337672158924398165151862094706053940830821532864260543417237811918166807469715046584334237149250584727692745234746142171573175423785721026709593407664046881589413366706252864920052167228324308547218195967450249948207887341536607780400807107730207032926117718549982543342765633452,
52359266544766452482523975685608230407291002763343708814586323783536687163032519294172952791019189972735544414177241032748822987128020591624518112210653204778035127348730617786610234768247540041763186245947296196728495533871800544502941653267136708893036674478754400011664069241697039722736589615301087984767,
38844834597621755927122817069901146449872652071559225576929653719687546171913834706512962460338554916524680815254372597395506874917236317267396410010981378138718936802206799767435370326551257259787965495758168188779021198496511984668695738922224971732694952548129382773592529943971677475172616574617049917266,
65629968065768428556355481091340719361116741954090679498200327504380027400680764581274354973606760186939105604187865883111048765492587922739179675879975061207040589702673604068461675060948522167495142009134118186484638258930209624450387799273925135732923965394541431779018125927937023326149589446756683291575,
42931548833801764667442686792357953483640108421931662235100823765650887206728669527853083362065574312034401943127260307486662029021664321290485915120573764222508091524383596332156757300055325893277712136182289687677900418079735270414024337891273172139106857868598826210870661754777950107969285904122398033903]

def short_pad_attack(c1, c2, e, n):
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))

    g1 = x^e - c1
    g2 = (x+y)^e - c2

    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)

    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()

    kbits = n.nbits()//(2*e*e)
    diff = h.small_roots(X=2^kbits, beta=0.5)[0]  # find root < 2^kbits with factor >= n^0.5

    return diff


def related_message_attack(c1, c2,diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]
N = 82212154608576254900096226483113810717974464677637469172151624370076874445177909757467220517368961706061745548693538272183076941444005809369433342423449908965735182462388415108238954782902658438063972198394192220357503336925109727386083951661191494159560430569334665763264352163167121773914831172831824145331

for i in range(len(ct)):
    for j in range(len(ct)-1):
        ct1 = ct[i]
        ct2 = ct[j+1]
        try:
            diff = short_pad_attack(ct1,ct2,3,N)
        except:
            continue
        m = related_message_attack(ct1,ct2,diff,3,N)
        if 'ctf4b' in long_to_bytes(m):
            print long_to_bytes(m)
            break

flag ctf4b{b1tfl1pp1ng_1s_r3lated_m3ss4ge}

Sliding Puzzle[Misc,206pts]

帰りの新幹線で解いてみました、
競技はとっくに終わったが、
一応気になった問題なので、取り込みました。

----------------
|  0 |  2 |  3 |
|  6 |  7 |  1  |
|  8 |  4 |  5 |
----------------

↑のようなpuzzleから0だけ動かして、
数字の昇順に並べ換えらせるまでの手順を遅れれば良い。 なんとなく深さ優先探索(合ってる?)でできそうなので、
競プロの人は普通に解けると思います。
競プロできない自分は他の方の実装を弄って終わった。

github.com

from m1z0r3 import *
from pwn import *
...略...
   #Prints the solutions if the prnt = True
    if prnt == True:
        print solutions
        old_tmp = [flatten for inner in eval(solutions[1]) for flatten in inner]
        old_idx = old_tmp.index(0)
       
        for i in solutions[2:]:
            tmp = [flatten for inner in eval(i) for flatten in inner]
            idx = tmp.index(0)
            diff = idx - old_idx
            if diff == 3:
                ss.append('2')
            elif diff == -3:
                ss.append('0')
            elif diff == 1:
                ss.append('1')
            elif diff == -1 :
                ss.append('3')
            t = PuzzleNode(n, eval(i))
            t.__str__(i)
            print "The next step is :"
            old_idx = idx

    return ss
   #return "The frontier size, number of steps and error code are :" , steps, frontierSize, err,


ip,port = '133.242.50.201',24912
#s,f = sock(ip,port)
io = remote(ip,port)
#print recv_line(f)
n = 0
while True:
    print n
    priv = io.recvline()
    if '----------------' not in priv:
        print priv
        break 
    puzzle = []
    for _ in range(3):
        t = []
        tmp = io.recvline().strip().replace(' | ',',').replace('|','',1).replace('|','')
        for i in tmp.split(','):
            t.append(int(i))
        puzzle.append(t)
    print io.recvline()
    print io.recvline()
    print puzzle
        
    #test =  [[0,2,3],[6,7,1],[8,4,5]]
    #print solvePuzzle(3,test,heuristics[1],True)
    r = solvePuzzle(3, puzzle, heuristics[2], True)
    print r
    r = ','.join(r)
    print r
    io.sendline(r)
    n += 1
print io.recvline()

flag ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

他の問題

気が向いたら

所感

去年より難しいと感じています、楽しかったです。