セキュリティブログ

The Flare-On Challengeとは?Flare-On 11の問題紹介とFlare-On 12への誘い

The Flare-On Challengeとは?Flare-On 11の問題紹介とFlare-On 12への誘い

更新日:2024.11.11

高度解析部高度解析課の古川です。本記事ではThe Flare-On Challengeについて紹介します。

The Flare-On Challengeは、2014年から毎年秋に6週間にわたって開催されている、リバースエンジニアリングに特化した個人戦のCTFです。Google Cloudに買収されたMandiant社のFLARE teamによって主催されています。今年で第11回の開催となる「Flare-On 11」には、国内外から合計4,166名が参加しました。

弊社からは4名のメンバーが参加し、そのうち3名が全ての問題を解くことに成功しました。また国内で全問題をクリアしたのは4名で、そのうちが3名が弊社メンバーでした。

本記事では、The Flare-On Challenge に参加したことがない方に向けて、どのような問題が出題されるのかを紹介したいと思います。これが、来年以降、日本からの参加者が増えるきっかけとなれば幸いです。

本記事を読んで興味を持った問題について、より詳細な解説をお求めの場合は、弊社メンバーの SuperFashi による全問題の詳細な Writeup も公開されています (https://gmo-cybersecurity.com/blog/flare-on-11-write-up/) ので、ぜひ併せてご覧ください。

出題された問題

今年のFlare-on 11では全部で10問が出題されました。各問題の概要を表にしてまとめました。問題の難易度は筆者の個人的な感覚で設定しており、1〜5の5段階で表記しています。数値が大きいほど難易度が高いことを示しています。

問題名 解析対象のフォーマット 難易度 概要
frog Python 1 pygameを使ったPythonスクリプトを解析する問題
checksum Go言語、PE、x86-64 2 x86-64 PE(Windowsの実行ファイル形式)のGo言語バイナリを解析する問題
aray Yaraルール 2 Yaraルールにマッチするデータを特定する問題
Meme Maker 3000 JavaScript 2 難読化されたJavaScriptを解析する問題
sshd コアダンプ、ELF、x86-64 3 XZ Utils バックドアを題材にした問題
bloke2 Verilog 2 Verilogを用いて実装されたハッシュ関数のバックドアを解析する問題
fullspeed .NET NativeAOT、PE、x86-64 4 .NET NativeAOTで作成されたRAT(Remote Access Tool)とC2の通信を復号する問題
clearlyfake EVM、JavaScript、PowerShell 2 スマートコントラクトをLOTS(Living off Trusted Sites)として利用する攻撃を題材にした問題
serpentine PE、x86-64 5 構造化例外処理を用いた難読化コードを解析する問題
Catbert Ransomware UEFI、x86-64 3 UEFIで動作するランサムウェアを模したUEFI Shellコマンドを解析する問題

一般的にリバースエンジニアリングと言われると機械語を解析するというイメージが多いかもしれませんが、表を見てわかるように、機械語のみならず、Python、JavaScript、PowerShellといったスクリプト言語やYaraルール(ドメイン特化言語)、Verilog(ハードウェア記述言語)、スマートコントラクトといった幅広いプログラムの解析が求められる問題が出題されました。

frog

「frog」は、pygame を使用した Python スクリプトを解析する問題です。実行してみると、マップ上に表示されたカエルを操作して中央のオブジェクトまで移動するとフラグが表示されますが、壁があるため内部に入ることができないという設定でした。

スクリプトを改変して、カエルの位置を変更したり壁をすり抜けたりするなど、ゲームチートのようなアプローチも可能です。また、フラグの生成処理を解析してフラグを取得することもできます。

試しにカエルの初期位置をゴールの位置に変更したところ、以下のようにフラグが表示されました。

難読化などもされていないシンプルな Python プログラムを解析する問題だったため、非常に簡単な問題でした。例年の The Flare-On Challenge でも同様に、すべての問題が非常に難しいわけではなく、リバースエンジニアリングを始めたばかりの方でも解けるような問題が用意されていることが多いです。

checksum

「checksum」は、x86-64 PE 形式にコンパイルされた Go 言語のバイナリを解析する問題です。実行してみると、以下のように足し算の計算を入力するように求められます。

Z:\flareon\02_checksum>checksum.exe
Check sum: 6203 + 5594 = 

いくつか入力を試みると、表示が変わり、式を表示せずにチェックサムの入力を求められるようになります。ここで適当な数字を入力すると、Not a valid checksum... と表示されました。

Z:\ctf\flareon\02_checksum>checksum.exe
Check sum: 6203 + 5594 = 11797
Good math!!!
------------------------------
Check sum: 5871 + 6819 = 12690
Good math!!!
------------------------------
Check sum: 6754 + 9313 = 16067
Good math!!!
------------------------------
Check sum: 7958 + 5408 = 13366
Good math!!!
------------------------------
Check sum: 2607 + 5376 = 7983
Good math!!!
------------------------------
Check sum: 4039 + 1903 = 5942
Good math!!!
------------------------------
Check sum: 3308 + 9496 = 12804
Good math!!!
------------------------------
Checksum: 0
Not a valid checksum...

バイナリを解析すると、最後のチェックサムは以下の関数で検証されていることが分かりました。

// main.a
__int64 __golang main_a(void *a1, __int64 a2, __int64 a3)
{
  __int64 v3; // r14
  __int64 v4; // rax
  _BYTE *v6; // rdx
  __int64 i; // rbx
  __int64 v8; // rsi
  _BYTE *v9; // rdi
  unsigned __int64 v10; // rax
  char v11; // dl
  __int64 v12; // rbx
  __int64 v13; // rax
  _BYTE *v15; // [rsp+0h] [rbp-8h]
  void *retaddr; // [rsp+10h] [rbp+8h] BYREF
  void *a1a; // [rsp+18h] [rbp+10h]

  while ( (unsigned __int64)&retaddr <= *(_QWORD *)(v3 + 16) )
  {
    a1a = a1;
    runtime_morestack_noctxt((int)a1, a2, a3);
    a1 = a1a;
  }
  if ( !a1 )
    a1 = &runtime_noptrbss;
  v15 = a1;
  v4 = runtime_makeslice((unsigned __int64 *)&RTYPE_uint8, a2, a2);
  v6 = v15;
  for ( i = 0LL; a2 > i; ++i )
  {
    v8 = v4;
    v9 = v6;
    v10 = i - 11 * ((__int64)((unsigned __int128)(i * (__int128)0x5D1745D1745D1746LL) >> 64) >> 2);
    v11 = v6[i];
    if ( v10 >= 11 )
      runtime_panicIndex(v10, i, 11LL, v9);
    *(_BYTE *)(v8 + i) = aFlareon2024[v10] ^ v11;
    v4 = v8;
    v6 = v9;
  }
  v12 = v4;
  v13 = encoding_base64__ptr_Encoding_EncodeToString(runtime_bss, v4, a2, a2);
  if ( v12 == 88 )
    return runtime_memequal(
             v13,
             "cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA==");
  else
    return 0LL;
}

同様の処理を Python コードに書き起こすと、以下のようになります。

import base64

def xor(a: bytes, b: bytes) -> bytes:
    if len(a) >= len(b):
        return bytes(a[i] ^ b[i % len(b)] for i in range(len(a)))
    else:
        return bytes(a[i % len(a)] ^ b[i] for i in range(len(b)))

def main_a(data):
    xored = xor(data, b'FlareOn2024')
    encoded = base64.b64decode(xored)
    return encoded == 'cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA=='

以下のコードを実行することで、関数 main_a が True を返すような引数 data を見つけることができます。

import base64

def xor(a: bytes, b: bytes) -> bytes:
    if len(a) >= len(b):
        return bytes(a[i] ^ b[i % len(b)] for i in range(len(a)))
    else:
        return bytes(a[i % len(a)] ^ b[i] for i in range(len(b)))

encoded = 'cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA=='
decoded = base64.b64decode(encoded)
print(xor(decoded, b'FlareOn2024').decode())

実行結果として、7fd7dd1d0e959f74c133c13abb740b9faa61ab06bd0ecd177645e93b1e3825ddという ASCII 文字列が表示されました。この文字列をチェックサムに入力すると、Noice!! というメッセージが表示されました。

Z:\flareon\02_checksum>checksum.exe
Check sum: 1252 + 4963 = 6215
Good math!!!
------------------------------
Check sum: 7903 + 6596 = 14499
Good math!!!
------------------------------
Check sum: 4459 + 12 = 4471
Good math!!!
------------------------------
Check sum: 482 + 4086 = 4568
Good math!!!
------------------------------
Check sum: 7027 + 3361 = 10388
Good math!!!
------------------------------
Checksum: 7fd7dd1d0e959f74c133c13abb740b9faa61ab06bd0ecd177645e93b1e3825dd
Noice!!

%LocalAppData%\REAL_FLAREON_FLAG.jpgに以下の内容の画像ファイルが生成されており、フラグを取得することができました。

C/C++ でコンパイルされた実行ファイルとは呼び出し規約が異なるため、IDA や Ghidra をそのまま使用して解析するのは困難でした。しかし、実行ファイルに関数名が残されていたため、動的デバッグなどを併用して解析すれば、リバースエンジニアリングを始めたばかりの方でも解ける難易度だったと思われます。

aray

「aray」は、与えられた Yara ルールにマッチするデータを特定する問題です。Yara ルールとは、マルウェアの識別や分類を目的とした OSS ツールである Yara のルール作成に用いられる DSL およびルールを記述したファイルを指します。

Yaraルールは検知対象のファイル内容を計算して特定の値と一致するのか検証することができます。この問題のYaraルールは加減算、XOR、モジュロなどの算術演算の他にmd5やsha256などのハッシュ関数を利用して条件式を構築していました。

以下にYaraルールをパースし、Z3を用いて条件式を満たす入力を探索するPythonスクリプトを示します。

import re
from z3 import *

def parse_yara_conditions(yara_content):
    # Extract conditions between 'condition:' and closing brace
    condition_match = re.search(r'condition:\s*(.*?)\s*}', yara_content, re.DOTALL)
    if not condition_match:
        return None

    conditions = condition_match.group(1).strip()
    # Split conditions by 'and'
    return [cond.strip() for cond in conditions.split(' and ')]

def create_z3_variables(size=85):
    # Create array of bytes representing the file content
    file_bytes = [BitVec(f'b{i}', 32) for i in range(size)]
    return file_bytes

def add_uint_constraints(solver, file_bytes, conditions):
    for cond in conditions:
        # Handle uint8 conditions
        uint8_match = re.match(r'uint8\((\d+)\)(.*?)(\d+)', cond)
        if uint8_match:
            pos = int(uint8_match.group(1))
            op = uint8_match.group(2).strip()
            val = int(uint8_match.group(3))

            if op == '>':
                solver.add(file_bytes[pos] > val)
            elif op == '<':
                solver.add(file_bytes[pos] < val)
            elif op == '==':
                solver.add(file_bytes[pos] == val)
            elif op == '&':
                solver.add((file_bytes[pos] & val) == 0)  # For & 128 == 0
            elif op == '%':
                solver.add(URem(file_bytes[pos], val) < val)
            elif op == '^':
                solver.add(file_bytes[pos] ^ val == int(cond.split('==')[1].strip()))
            elif op == '+':
                solver.add(file_bytes[pos] + val == int(cond.split('==')[1].strip()))
            elif op == '-':
                solver.add(file_bytes[pos] - val == int(cond.split('==')[1].strip()))
            continue

        # Handle uint32 conditions
        uint32_match = re.match(r'uint32\((\d+)\)(.*?)(\d+)(.*?)(\d+)', cond)
        if uint32_match:
            pos = int(uint32_match.group(1))
            op = uint32_match.group(2).strip()
            val = int(uint32_match.group(3))

            # Create 32-bit value from 4 bytes
            uint32_val = Concat(Extract(7, 0, file_bytes[pos+3]), Extract(7, 0, file_bytes[pos+2]), Extract(7, 0, file_bytes[pos+1]), Extract(7, 0, file_bytes[pos]))

            if op == '^':
                solver.add(uint32_val ^ val == int(uint32_match.group(5)))
            elif op == '+':
                solver.add(uint32_val + val == int(uint32_match.group(5)))
            elif op == '-':
                solver.add(uint32_val - val == int(uint32_match.group(5)))
            continue

def add_filesize_constraints(solver, file_bytes, conditions):
    # Add filesize constraints
    filesize = BitVecVal(85, 32)  # Given in the YARA rule
    for cond in conditions:
        if 'filesize' in cond:
            filesize_match = re.match(r'filesize \^ uint8\((\d+)\) != (\d+)', cond)
            if filesize_match:
                pos = int(filesize_match.group(1))
                val = int(filesize_match.group(2))
                solver.add(filesize ^ file_bytes[pos] != val)

def add_hash_constraints(solver, file_bytes, conditions):
    solver.add(file_bytes[8] == 0x72)
    solver.add(file_bytes[9] == 0x65)
    solver.add(file_bytes[34] == 0x65)
    solver.add(file_bytes[35] == 0x41)
    solver.add(file_bytes[63] == 0x6e)
    solver.add(file_bytes[64] == 0x2e)
    solver.add(file_bytes[14] == 0x20)
    solver.add(file_bytes[15] == 0x73)
    solver.add(file_bytes[56] == 0x66)
    solver.add(file_bytes[57] == 0x6c)
    solver.add(file_bytes[0] == 0x72)
    solver.add(file_bytes[1] == 0x75)
    solver.add(file_bytes[78] == 0x6e)
    solver.add(file_bytes[79] == 0x3a)
    solver.add(file_bytes[76] == 0x69)
    solver.add(file_bytes[77] == 0x6f)
    solver.add(file_bytes[50] == 0x33)
    solver.add(file_bytes[51] == 0x41)
    solver.add(file_bytes[32] == 0x75)
    solver.add(file_bytes[33] == 0x6c)

def solve_yara_rule(yara_content):
    # Parse conditions
    conditions = parse_yara_conditions(yara_content)
    if not conditions:
        return None

    # Create solver
    solver = Solver()

    # Create variables
    file_bytes = create_z3_variables()

    # Add constraints
    add_uint_constraints(solver, file_bytes, conditions)
    add_filesize_constraints(solver, file_bytes, conditions)
    add_hash_constraints(solver, file_bytes, conditions)

    # Add basic byte constraints
    for b in file_bytes:
        solver.add(b >= 0)
        solver.add(b < 256)

    # Check if satisfiable
    if solver.check() == sat:
        model = solver.model()
        solution = bytearray()
        for i in range(85):  # filesize is 85
            byte_val = model[file_bytes[i]].as_long()
            solution.append(byte_val)
        return solution
    else:
        return None

def main():
    # Read YARA rule content
    with open('aray.yara', 'r') as f:
        yara_content = f.read()

    # Solve the rule
    solution = solve_yara_rule(yara_content)

    if solution:
        print("Solution found!")
        print("Bytes:", solution)
        # Optionally write to file
        with open('solution.bin', 'wb') as f:
            f.write(solution)
    else:
        print("No solution found.")

if __name__ == "__main__":
    main()

以下に実行結果を示します。

$ python solve.py
Solution found!
Bytes: bytearray(b'rule flareon { strings: $f = "1RuleADayK33p$Malw4r3Aw4y@flare-on.com" condition: $f }')

Yaraルールのようなチューリング完全でない言語でもリバースエンジニアリングの対象になるのは面白いですね。

Meme Maker 3000

「Meme Maker 3000」は、HTML と JavaScript ファイルが提供され、難読化された JavaScript を解析する問題です。HTML を開くと、以下のような画面が表示されます。

JavaScript ファイルを解析していると、文字列の分割や置換による難読化が施されていることがわかります。JavaScript の難読化に対応するためのツールは多数公開されていますが、今回は synchrony (https://github.com/relative/synchrony) を使用することで、以下のように難読化を解除することができました。変数 a0e は画像ファイルのデータを含むため、ここでは省略します。

const a0c = [
    'When you find a buffer overflow in legacy code',
    'Reverse Engineer',
    'When you decompile the obfuscated code and it makes perfect sense',
    'Me after a week of reverse engineering',
    'When your decompiler crashes',
    "It's not a bug, it'a a feature",
    "Security 'Expert'",
    'AI',
    "That's great, but can you hack it?",
    'When your code compiles for the first time',
    "If it ain't broke, break it",
    "Reading someone else's code",
    'EDR',
    'This is fine',
    'FLARE On',
    "It's always DNS",
    'strings.exe',
    "Don't click on that.",
    'When you find the perfect 0-day exploit',
    'Security through obscurity',
    'Instant Coffee',
    'H@x0r',
    'Malware',
    '$1,000,000',
    'IDA Pro',
    'Security Expert',
  ],
  a0d = {
    doge1: [
      ['75%', '25%'],
      ['75%', '82%'],
    ],
    boy_friend0: [
      ['75%', '25%'],
      ['40%', '60%'],
      ['70%', '70%'],
    ],
    draw: [['30%', '30%']],
    drake: [
      ['10%', '75%'],
      ['55%', '75%'],
    ],
    two_buttons: [
      ['10%', '15%'],
      ['2%', '60%'],
    ],
    success: [['75%', '50%']],
    disaster: [['5%', '50%']],
    aliens: [['5%', '50%']],
  },
  a0e = {
    'doge1.png':'',
    'draw.jpg':'',
    'drake.jpg':'',
    'two_buttons.jpg':'',
    'fish.jpg':'',
    'boy_friend0.jpg':'',
    'success.jpg':'',
    'disaster.jpg':'',
    'aliens.jpg':''
  }
function a0f() {
  document.getElementById('caption1').hidden = true
  document.getElementById('caption2').hidden = true
  document.getElementById('caption3').hidden = true
  const a = document.getElementById('meme-template')
  var b = a.value.split('.')[0]
  a0d[b].forEach(function (c, d) {
    var e = document.getElementById('caption' + (d + 1))
    e.hidden = false
    e.style.top = a0d[b][d][0]
    e.style.left = a0d[b][d][1]
    e.textContent = a0c[Math.floor(Math.random() * (a0c.length - 1))]
  })
}
a0f()
const a0g = document.getElementById('meme-image'),
  a0h = document.getElementById('meme-container'),
  a0i = document.getElementById('remake'),
  a0j = document.getElementById('meme-template')
a0g.src = a0e[a0j.value]
a0j.addEventListener('change', () => {
  a0g.src = a0e[a0j.value]
  a0g.alt = a0j.value
  a0f()
})
a0i.addEventListener('click', () => {
  a0f()
})
function a0k() {
  const a = a0g.alt.split('/').pop()
  if (a !== Object.keys(a0e)[5]) {
    return
  }
  const b = a0l.textContent,
    c = a0m.textContent,
    d = a0n.textContent
  if (
    a0c.indexOf(b) == 14 &&
    a0c.indexOf(c) == a0c.length - 1 &&
    a0c.indexOf(d) == 22
  ) {
    var e = new Date().getTime()
    while (new Date().getTime() < e + 3000) {}
    var f =
      d[3] +
      'h' +
      a[10] +
      b[2] +
      a[3] +
      c[5] +
      c[c.length - 1] +
      '5' +
      a[3] +
      '4' +
      a[3] +
      c[2] +
      c[4] +
      c[3] +
      '3' +
      d[2] +
      a[3] +
      'j4' +
      a0c[1][2] +
      d[4] +
      '5' +
      c[2] +
      d[5] +
      '1' +
      c[11] +
      '7' +
      a0c[21][1] +
      b.replace(' ', '-') +
      a[11] +
      a0c[4].substring(12, 15)
    f = f.toLowerCase()
    alert(atob('Q29uZ3JhdHVsYXRpb25zISBIZXJlIHlvdSBnbzog') + f)
  }
}
const a0l = document.getElementById('caption1'),
  a0m = document.getElementById('caption2'),
  a0n = document.getElementById('caption3')
a0l.addEventListener('keyup', () => {
  a0k()
})
a0m.addEventListener('keyup', () => {
  a0k()
})
a0n.addEventListener('keyup', () => {
  a0k()
})

alert(atob('Q29uZ3JhdHVsYXRpb25zISBIZXJlIHlvdSBnbzog') + f) という処理に注目すると、Q29uZ3JhdHVsYXRpb25zISBIZXJlIHlvdSBnbzog を Base64 デコードした結果が Congratulations! Here you go: という文字列であることがわかります。さらに、変数 f がフラグを示していることが推測されます。

関数 a0k を解析した結果、キャプションの内容が特定の条件を満たしたとき、その内容に基づいてフラグが生成されることが判明しました。開発者ツールのコンソールで実行することでフラグを表示するコードを以下に示します。

a = Object.keys(a0e)[5];
b = a0c[14];
c = a0c[a0c.length - 1];
d = a0c[22];
var f =
  d[3] +
  'h' +
  a[10] +
  b[2] +
  a[3] +
  c[5] +
  c[c.length - 1] +
  '5' +
  a[3] +
  '4' +
  a[3] +
  c[2] +
  c[4] +
  c[3] +
  '3' +
  d[2] +
  a[3] +
  'j4' +
  a0c[1][2] +
  d[4] +
  '5' +
  c[2] +
  d[5] +
  '1' +
  c[11] +
  '7' +
  a0c[21][1] +
  b.replace(' ', '-') +
  a[11] +
  a0c[4].substring(12, 15);
f = f.toLowerCase();
alert(atob('Q29uZ3JhdHVsYXRpb25zISBIZXJlIHlvdSBnbzog') + f);

機械語ではなくJavaScriptで書かれており、難読化も非常に高度な手法が用いられていないため、リバースエンジニアリングを始めたばかりの方にオススメな問題でした。

sshd

「sshd」は、XZ Utils バックドアを題材にした問題で、改ざんされた共有ライブラリ liblzma とコアダンプを解析する課題です。XZ Utils バックドアとは、2024 年 3 月に圧縮ユーティリティライブラリである XZ Utils にバックドアが仕掛けられたソフトウェアサプライチェーン攻撃を指します。

この問題ではliblzmaに関数 RSA_public_decrypt の呼び出しをフックする処理を追加しています。以下にフック処理のデコンパイル結果を示します。

__int64 __fastcall sub_9820(unsigned int a1, char *a2, __int64 a3, __int64 a4, unsigned int a5)
{
  const char *v9; // rsi
  void *v10; // rax
  void *v12; // rax
  void (*v13)(void); // [rsp+8h] [rbp-120h]
  chacha20_context a1a; // [rsp+20h] [rbp-108h] BYREF
  unsigned __int64 v15; // [rsp+E8h] [rbp-40h]

  v15 = __readfsqword(0x28u);
  v9 = "RSA_public_decrypt";
  if ( !getuid() )
  {
    if ( *(_DWORD *)a2 == 0xC5407A48 )
    {
      chacha20_init_context(&a1a, (uint8_t *)a2 + 4, (uint8_t *)a2 + 36, 0LL);
      v12 = mmap(0LL, dword_32360, 7, 34, -1, 0LL);
      v13 = (void (*)(void))memcpy(v12, &unk_23960, dword_32360);
      f_chacha20_xor(&a1a, v13, dword_32360);
      v13();
      chacha20_init_context(&a1a, (uint8_t *)a2 + 4, (uint8_t *)a2 + 36, 0LL);
      f_chacha20_xor(&a1a, v13, dword_32360);
    }
    v9 = "RSA_public_decrypt ";
  }
  v10 = dlsym(0LL, v9);
  return ((__int64 (__fastcall *)(_QWORD, char *, __int64, __int64, _QWORD))v10)(a1, a2, a3, a4, a5);
} 

関数 RSA_public_decrypt が呼び出された際、復号対象のデータが特定のバイト列(0xC5407A48)で始まる場合、その後のバイト列を鍵、ノンス、暗号化データとして扱い、ChaCha20 で復号して結果をシェルコードとして実行します。

コアダンプから鍵、初期化ベクトル、暗号化データを抽出し、復号する Python プログラムを以下に示します。

import struct

from Crypto.Cipher import ChaCha20

LIBPATH = 'liblzma.so.5.4.1'
COREPATH = 'sshd.core.93794.0.0.11.1725917676'

def get_key_and_nonce():
    d = open(COREPATH, 'rb').read()
    pattern = struct.pack('<I', 0xC5407A48)
    key_offset = d.find(pattern) + 4
    key = d[key_offset:key_offset + 32]
    nonce_offset = key_offset + 32
    nonce = d[nonce_offset:nonce_offset + 12]
    return key, nonce

def get_encrypted_shellcode():
    d = open(LIBPATH, 'rb').read()
    enc = d[0x23960:0x23960 + 0xF96]
    return enc

def main():
    encrypted_shellcode = get_encrypted_shellcode()
    key, nonce = get_key_and_nonce()
    cipher = ChaCha20.new(key=key, nonce=nonce)
    shellcode = cipher.decrypt(encrypted_shellcode)
    open('shellcode.bin', 'wb').write(shellcode)

if __name__ == '__main__':
    main()

シェルコードを解析すると、特定のサーバーにファイルを暗号化して送信する機能が実装されていることがわかりました。コアダンプから鍵、ノンス、暗号化データを抽出した後、C2 サーバーを模倣した Python スクリプトを作成してシェルコードを実行することでフラグを取得することができます。

以下に C2 サーバーを模倣した Python スクリプトを示します。

from pwn import *

context.log_level = 'debug'

def main():
    s = listen(1337).wait_for_connection()

    key = bytearray([0x8D, 0xEC, 0x91, 0x12, 0xEB, 0x76, 0x0E, 0xDA, 0x7C, 0x7D, 0x87, 0xA4, 0x43, 0x27, 0x1C, 0x35, 0xD9, 0xE0, 0xCB, 0x87, 0x89, 0x93, 0xB4, 0xD9, 0x04, 0xAE, 0xF9, 0x34, 0xFA, 0x21, 0x66, 0xD7])
    nonce = bytearray([0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11])
    fpath = b"certificate_authority_signing_key.txt.enc"

    s.send(key)
    s.send(nonce)
    s.send(p32(len(fpath)))
    s.send(fpath)

    s.interactive()

if __name__ == '__main__':
    main()

以下に実行結果を示します。

$ xxd certificate_authority_signing_key.txt.enc
00000000: a9f6 3408 422a 9e1c 0c03 a808 9470 bb8d  ..4.B*.......p..
00000010: aadc 6d7b 24ff 7f24 7cda 839e 92f7 071d  ..m{$..$|.......
$ python solve.py
[+] Trying to bind to :: on port 1337: Done
[+] Waiting for connections on :::1337: Got connection from ::ffff:127.0.0.1 on port 39464
[DEBUG] Sent 0x20 bytes:
    00000000  8d ec 91 12  eb 76 0e da  7c 7d 87 a4  43 27 1c 35  │····│·v··│|}··│C'·5│
    00000010  d9 e0 cb 87  89 93 b4 d9  04 ae f9 34  fa 21 66 d7  │····│····│···4│·!f·│
    00000020
[DEBUG] Sent 0xc bytes:
    bytearray(b'\x11') * 0xc
[DEBUG] Sent 0x4 bytes:
    00000000  29 00 00 00                                         │)···│
    00000004
[DEBUG] Sent 0x29 bytes:
    b'certificate_authority_signing_key.txt.enc'
[*] Switching to interactive mode
[DEBUG] Received 0x27 bytes:
    00000000  23 00 00 00  73 75 70 70  31 79 5f 63  68 61 31 6e  │#···│supp│1y_c│ha1n│
    00000010  5f 73 75 6e  64 34 79 40  66 6c 61 72  65 2d 6f 6e  │_sun│d4y@│flar│e-on│
    00000020  2e 63 6f 6d  49 a6 d1                               │.com│I··│
    00000027
#\x00\x00\x00supp1y_cha1n_sund4y@flare-on.comI\xa6\[*] Got EOF while reading in interactive

ここでは、ChaCha20 の暗号化と復号が同じ処理である特性を利用して、シェルコードの復号処理を解析せずにフラグを取得することを試みました。

この問題は、XZ Utils バックドアというキャッチーな題材を元に、コアダンプ解析や ChaCha20 を使用した暗号化・復号の解析を学べる非常に興味深い問題でした。

bloke2

「bloke2」は、Verilogを用いて実装されたハッシュ関数のバックドアを解析する問題です。Verilogのコードには、BLAKE2をベースにしたハッシュ関数が実装されていました。コードを解析すると、data_mgr.v内でtstというフラグが最終的なハッシュ出力を変更するために使用されていることがわかりました。

h <= h_in ^ (TEST_VAL & {(W*16){tst}});

このtstはfinishという信号を基に設定されているようです。そのため、ハッシュ処理を実行する際に、finishが1になるようにコードを変更しました。以下に差分を示します。

$ diff -bur ./bloke2 ./bloke2_solve
diff -bur ./bloke2/bloke2b_tb.v ./bloke2_solve/bloke2b_tb.v
--- ./bloke2/bloke2b_tb.v   2024-09-12 02:23:07
+++ ./bloke2_solve/bloke2b_tb.v 2024-11-04 15:55:32
@@ -70,7 +70,7 @@

        // Set our start and finish lines correctly.
        start <= 1'b1;
-       finish <= 1'b0;
+       finish <= 1'b1;
        @(posedge clk);
        start <= 1'b0;
        finish <= 1'b0;

この修正をもとにVerilogを実行したところ、フラグが確認できました。

$ cd ./bloke2_solve
$ make tests | grep flare-on
Received message: please_send_help_i_am_trapped_in_a_ctf_flag_factory@flare-on.com

Verilogは一般的な手続き型言語や関数型言語とは異なる言語体系を持つため、開発経験がないととっつきにくいことが多いです。したがって、この問題はVerilogのリバースエンジニアリング入門として良い問題でした。

fullspeed

「fullspeed」はRATの実行ファイルとRAT・C2間の通信をキャプチャしたpcapファイルが提供され、RATを解析し暗号プロトコルの脆弱性を利用することで、RAT・C2間の通信の通信を復号する問題です。RATはC#で開発されたものを.NET NativeAOTでコンパイルした実行ファイルでした。

.NETのAOT(Ahead-Of-Time)コンパイルは、アプリケーションのコードを事前にネイティブコードに変換する技術です。通常、.NETアプリケーションは実行時に中間言語(IL)からネイティブコードに変換されるJIT(Just-In-Time)コンパイルを使用しますが、AOTではビルド時にこの変換を行います。.NETには3種類のAOTコンパイル方式がありますが、その中でもNativeAOTはWindows、macOS、Linux向けに使用される方式で、アプリケーションを完全にネイティブコードにコンパイルします。

RATとC2サーバーはTCPのセッションを確立すると、ECDH(Elliptic Curve Diffie-Hellman)を利用した鍵交換を行います。このため、ECDHの後の通信内容は暗号化されており、通信内容を見ることができません。

ECDHで利用された暗号パラメータに対して既存の攻撃手法をもとに解析を進めると、Gの位数が1つを除いて小さな素数から構成されていることから、Pohlig-Hellmanを使った攻撃が可能であることがわかりました。このとき、最大の素因数を除外した状態で復元した秘密鍵が112ビットであることから、残りの16ビットを総当りで探索する必要があります。

以下に秘密鍵及び共有鍵を復元するsageスクリプトを示します。

from sage.all import *

M = 0xc90102faa48f18b5eac1f76bb40a1b9fb0d841712bbe3e5576a7a56976c2baeca47809765283aa078583e1e65172a3fd
A = 0xa079db08ea2470350c182487b50f7707dd46a58a1d160ff79297dcc9bfad6cfc96a81c4a97564118a40331fe0fc1327f
B = 0x9f939c02a7bd7fc263a4cce416f4c575f28d0c1315c4f0c282fca6709a5f9f7f9c251c9eede9eb1baa31602167fa5380
E = EllipticCurve(Integers(M),[A,B])

G = E.point((0x087b5fe3ae6dcfb0e074b40f6208c8f6de4f4f0679d6933796d3b9bd659704fb85452f041fff14cf0e9aa7e45544f9d8, 0x127425c1d330ed537663e87459eaa1b1b53edfe305f6a79b184b3180033aab190eb9aa003e02e9dbf6d593c5e3b08182))
P = E.point((0x195b46a760ed5a425dadcab37945867056d3e1a50124fffab78651193cea7758d4d590bed4f5f62d4a291270f1dcf499, 0x357731edebf0745d081033a668b58aaa51fa0b4fc02cd64c7e8668a016f0ec1317fcac24d8ec9f3e75167077561e2a15))
Q = E.point((0xb3e5f89f04d49834de312110ae05f0649b3f0bbe2987304fc4ec2f46d6f036f1a897807c4e693e0bb5cd9ac8a8005f06, 0x85944d98396918741316cd0109929cb706af0cca1eaf378219c5286bdc21e979210390573e3047645e1969bdbcb667eb))

fac = list(factor(G.order()))
moduli = []
remainders = []

# 最大の素因数を除外
fac_excl_max = fac[:-1]

for prime_power in fac_excl_max:
    p_factor = prime_power[0]
    e_factor = prime_power[1]
    modulus = p_factor ** e_factor
    exponent = G.order() // modulus
    G0 = G * exponent
    P0 = P * exponent
    # 離散対数を計算
    dl = discrete_log(P0, G0, operation='+', ord=modulus)
    moduli.append(modulus)
    remainders.append(dl)

# 部分的な秘密鍵とそのモジュラスを計算
d_partial = crt(remainders, moduli)
modulus_partial = prod(moduli)

print(f"部分的に復元した秘密鍵({modulus_partial.bit_length()}ビット): {d_partial}")

# 残りの16ビットを総当りで探索
for k in range(2^16):
    d_candidate = d_partial + k * modulus_partial
    if d_candidate * G == P:
        print("秘密鍵を発見しました: d =", d_candidate)
        break
else:
    print("秘密鍵が見つかりませんでした。")

shared_secret = d_candidate * Q
print(f"shared_secret: {shared_secret.x()}")

以下が実行結果です。

部分的に復元した秘密鍵(112ビット): 3914004671535485983675163411331184
秘密鍵を発見しました: d = 168606034648973740214207039875253762473
shared_secret: 9285933189458587360370996409965684516994278319709076885861327850062567211786910941012004843231232528920376385508032

共有鍵をもとに通信内容を復号するPythonスクリプトを以下に示します。

from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Cipher import ChaCha20
from hashlib import sha512
import sys

ENC_HEX = 'f272d54c31860f3fbd43da3ee32586dfd7c50cea1c4aa064c35a7f6e3ab0258441ac1585c36256dea83cac93007a0c3a29864f8e285ffa79c8eb43976d5b587f8f35e699547116fcb1d2cdbba979c989998c61490bce39da577011e0d76ec8eb0b8259331def13ee6d86723eac9f0428924ee7f8411d4c701b4d9e2b3793f6117dd30dacba2cae600b5f32cea193e0de63d709838bd6a7fd35edf0fc802b15186c7a1b1a475daf94ae40f6bb81afcedc4afb158a5128c28c91cd7a8857d12a661acaecaec8d27a7cf26a1727368535a44e2f3917ed09447ded797219c966ef3dd5705a3c32bdb1710ae3b87fe66669e0b4646fc416c399c3a4fe1edc0a3ec5827b84db5a79b81634e7c3afe528a4da15457b637815373d4edcac2159d056f5981f71c7ea1b5d8b1e5f06fc83b1def38c6f4e694e3706412eabf54e3b6f4d19e8ef46b04e399f2c8ece8417fa4008bc54e41ef701fee74e80e8dfb54b487f9b2e3a277fa289cf6cb8df986cdd387e342ac9f5286da11ca27840845ca68d1394be2a4d3d4d7c82e531b6dac62ef1ad8dc1f60b79265ed0deaa31ddd2d53aa9fd9343463810f3e2232406366b48415333d4b8ac336d4086efa0f15e6e590d1ec06f36'

def main():
    shared_secret = int(sys.argv[1])

    hashed = sha512(long_to_bytes(shared_secret)).digest()
    key = hashed[:0x20]
    nonce = hashed[0x20:0x28]
    print(f"Key: {key.hex()}")
    print(f"Nonce: {nonce.hex()}")
    cipher = ChaCha20.new(key=key, nonce=nonce)

    enc = bytes.fromhex(ENC_HEX.replace('\n', ''))
    dec = cipher.encrypt(enc)
    print(f"Decrypted: {dec}")

if __name__ == "__main__":
    main()

以下が実行結果です。

$ python solve.py 9285933189458587360370996409965684516994278319709076885861327850062567211786910941012004843231232528920376385508032
Key: b48f8fa4c856d496acdecd16d9c94cc6b01aa1c0065b023be97afdd12156f3dc
Nonce: 3fd480978485d818
Decrypted: b"verify\x00verify\x00ls\x00=== dirs ===\r\nsecrets\r\n=== files ===\r\nfullspeed.exe\r\n\x00cd|secrets\x00ok\x00ls\x00=== dirs ===\r\nsuper secrets\r\n=== files ===\r\n\x00cd|super secrets\x00ok\x00ls\x00=== dirs ===\r\n.hidden\r\n=== files ===\r\n\x00cd|.hidden\x00ok\x00ls\x00=== dirs ===\r\nwait, dot folders aren't hidden on windows\r\n=== files ===\r\n\x00cd|wait, dot folders aren't hidden on windows\x00ok\x00ls\x00=== dirs ===\r\n=== files ===\r\nflag.txt\r\n\x00cat|flag.txt\x00RDBudF9VNWVfeTB1cl9Pd25fQ3VSdjNzQGZsYXJlLW9uLmNvbQ==\x00exit\x00"
$ echo RDBudF9VNWVfeTB1cl9Pd25fQ3VSdjNzQGZsYXJlLW9uLmNvbQ== | base64 -d
D0nt_U5e_y0ur_Own_CuRv3s@flare-on.com

解析対象のフォーマットが.NET NativeAOTであるため解析難易度が高く、通信内容の復号にはECDHに対する攻撃手法の知識が必要であることから全体を通じて難易度が高い問題でした。一方で.NET NativeAOTの解析やECDHに対する攻撃の知見が得られる非常に良い問題でした。

clearlyfake

clearlyfakeは、スマートコントラクトをLOTS(Living off Trusted Sites)として利用する攻撃を題材にした問題です。与えられたJavaScriptの難読化を解除すると以下のようなコードが現れました。

const Web3 = require("web3");
const fs = require("fs");
const web3 = new Web3("BINANCE_TESTNET_RPC_URL");
const contractAddress = "0x9223f0630c598a200f99c5d4746531d10319a569";
async function callContractFunction(inputString) {
    try {
        const methodId = "0x5684cff5";
        const encodedData = methodId + web3.eth.abi.encodeParameters(["string"], [inputString]).slice(2);
        const result = await web3.eth.call({
            to: contractAddress,
            data: encodedData
        });
        const largeString = web3.eth.abi.decodeParameter("string", result);
        const targetAddress = Buffer.from(largeString, "base64").toString("utf-8");
        const filePath = "decoded_output.txt";
        fs.writeFileSync(filePath, "$address = " + targetAddress + "\n");
        const new_methodId = "0x5c880fcb";
        const blockNumber = 43152014;
        const newEncodedData = new_methodId + web3.eth.abi.encodeParameters(["address"], [targetAddress]).slice(2);
        const newData = await web3.eth.call({
            to: contractAddress,
            data: newEncodedData
        }, blockNumber);
        const decodedData = web3.eth.abi.decodeParameter("string", newData);
        const base64DecodedData = Buffer.from(decodedData, "base64").toString("utf-8");
        fs.writeFileSync(filePath, decodedData);
        console.log(`Saved decoded data to:${filePath}`)
    } catch (error) {
        console.error("Error calling contract function:", error)
    }
}
const inputString = "KEY_CHECK_VALUE";
callContractFunction(inputString);

関数 callContractFunction はバイナンステストネット上のコントラクト 0x9223f0630c598a200f99c5d4746531d10319a569 (https://testnet.bscscan.com/address/0x9223f0630c598a200f99c5d4746531d10319a569#code) の関数 0x5684cff5 を呼び出します。

関数 0x5684cff5の処理を解析すると、入力値が giV3_M3_p4yL04d! のときにコントラクトのアドレス(0x5324eab94b236d4d1456edc574363b113cebf09d)を返していることがわかります。

このコントラクトをブロック番号 43152014 のときに呼び出しているトランザクションが見つかりました (https://testnet.bscscan.com/tx/0x05660d13d9d92bc1fc54fb44c738b7c9892841efc9df4b295e2b7fda79756c47) 。このトランザクションのインプットデータをBase64デコードするとPowerShellのコードが得られました。難読化を解除したコードを以下に示します。

Set-Variable -Name testnet_endpoint -Value (" ")
Set-Variable -Name _body -Value ('{"method":"eth_call","params":[{"to":"$address","data":"0x5c880fcb"}, BLOCK],"id":1,"jsonrpc":"2.0"}')
Set-Variable -Name resp -Value ((Invoke-RestMethod -Method 'Post' -Uri $testnet_endpoint -ContentType "application/json" -Body $_body).result)
# Remove the '0x' prefix
Set-Variable -Name hexNumber -Value ($resp -replace '0x', '')
# Convert from hex to bytes (ensuring pairs of hex characters)
Set-Variable -Name bytes0 -Value (0..($hexNumber.Length / 2 - 1) | ForEach-Object {
    Set-Variable -Name startIndex -Value ($_ * 2)
    Set-Variable -Name endIndex -Value ($startIndex + 1)  [Convert]::ToByte($hexNumber.Substring($startIndex, 2), 16)
})
Set-Variable -Name bytes1 -Value ([System.Text.Encoding]::UTF8.GetString($bytes0))
Set-Variable -Name bytes2 -Value ($bytes1.Substring(64, 188))
# Convert from base64 to bytes
Set-Variable -Name bytesFromBase64 -Value ([Convert]::FromBase64String($bytes2))
Set-Variable -Name resultAscii -Value ([System.Text.Encoding]::UTF8.GetString($bytesFromBase64))
Set-Variable -Name hexBytes -Value ($resultAscii | ForEach-Object {
    '{0:X2}' -f $_  # Format each byte as two-digit hex with uppercase letters
})
Set-Variable -Name hexString -Value ($hexBytes -join ' ') #Write-Output $hex
StringSet-Variable -Name hexBytes -Value ($hexBytes -replace " ", "")
# Convert from hex to bytes (ensuring pairs of hex characters)
Set-Variable -Name bytes3 -Value (0..($hexBytes.Length / 2 - 1) | ForEach-Object {
    Set-Variable -Name startIndex -Value ($_ * 2)
    Set-Variable -Name endIndex -Value ($startIndex + 1)[Convert]::ToByte($hexBytes.Substring($startIndex, 2), 16)
})
Set-Variable -Name bytes5 -Value ([Text.Encoding]::UTF8.GetString($bytes3))
# Convert the key to bytes
Set-Variable -Name keyBytes -Value ([Text.Encoding]::ASCII.GetBytes("FLAREON24"))
# Perform the XOR operation
Set-Variable -Name resultBytes -Value (@())for (Set-Variable -Name i -Value (0); $i -lt $bytes5.Length; $i++) {
    Set-Variable -Name resultBytes -Value ($resultBytes + ($bytes5[$i] -bxor $keyBytes[$i % $keyBytes.Length]))
}
# Convert the result back to a string (assuming ASCII encoding)
Set-Variable -Name resultString -Value ([System.Text.Encoding]::ASCII.GetString($resultBytes))
Set-Variable -Name command -Value ("tar -x --use-compress-program 'cmd /c echo $resultString > C:\\flag' -f C:\\flag")
Invoke-Expression $command

このコードから他に関数 0x5c880fcb を呼び出しているトランザクションのインプットデータを FLAREON24 という文字列でXORした結果がフラグであることがわかります。一通り試してみると、トランザクション 0xdbf0e117fb3d4db0cd746835cfc4eb026612ac36a80f9f0f248dce061d90ae54 (https://testnet.bscscan.com/tx/0xdbf0e117fb3d4db0cd746835cfc4eb026612ac36a80f9f0f248dce061d90ae54) のインプットデータを FLAREON24 でXORすると N0t_3v3n_DPRK_i5_Th15_1337_1n_Web3@flare-on.comという文字列が得られました。

スマートコントラクトをLOTSのように扱ってペイロードを送信するアイデアは興味深く、スマートコントラクトの実装は複雑なものでなかったため、スマートコントラクトに対するリバースエンジニアリングの入門としてちょうどいい問題だったと思います。

serpentine

この問題は、構造化例外処理 (SEH) を活用した難読化コードの解析を求められる非常に難易度の高い課題で、今回のFlare-on 11で最も難しい問題でした。

問題の実行ファイルはx86-64のPEで、コマンドライン引数をフラグとして検証するcrackme形式でした。動的デバッグをすると、hlt命令をきっかけにUNWINDが発生し、コンテキスト切り替えによって処理が進む仕組みが確認できます。以下にUNWIND構造体の情報を加えたディスアセンブル結果を示します。

0x00000000: f4                               hlt 
unwind_info:
Frame register: rax
Frame offset: 0x0
Unwind codes:
Handler: 00000098
0x002e4d49: 49bb497fdd0a01000000             movabs r11, 0x10add7f49
0x000000a2: 4153                             push r11
0x000000a4: 6836547773                       push 0x73775436
0x000000a9: 68434ca068                       push 0x68a04c43
0x000000ae: 68f97f9112                       push 0x12917ff9
0x002e4db8: 48814424189f39ac35               add qword ptr [rsp + 0x18], 0x35ac399f
0x00000107: f4                               hlt 
unwind_info:
Frame register: rax
Frame offset: 0x0
Unwind codes:
  00: UWOP_PUSH_MACHFRAME without error code
  00: UWOP_ALLOC_LARGE, size=0x4
  00: UWOP_PUSH_NONVOL, register=r13
Handler: 000001A7
0x000001a7: 498b6928                         mov rbp, qword ptr [r9 + 0x28]
0x002e4e8c: 488bbde0000000                   mov rdi, qword ptr [rbp + 0xe0] ; ctx->r13
0x000001b2: 480fb6ff                         movzx rdi, dil ; rdi = input_bytes[0x4]
0x0000020a: f4                               hlt 
unwind_info:
Frame register: rax
Frame offset: 0x0
Unwind codes:
Handler: 000002A2
0x000002a2: 4d8b4128                         mov r8, qword ptr [r9 + 0x28]
0x002e4f5e: 498b80b0000000                   mov rax, qword ptr [r8 + 0xb0] ; ctx->rdi
0x002e4fc3: 49c7c2a77437b9                   mov r10, 0xffffffffb93774a7
0x002e502f: 4981c2e505b847                   add r10, 0x47b805e5
0x000002bb: 4152                             push r10
0x000002bd: 48f72424                         mul qword ptr [rsp]
0x000002c1: 4889c5                           mov rbp, rax
0x00000315: f4                               hlt 
unwind_info:
Frame register: rax
Frame offset: 0x0
Unwind codes:
Handler: 000003A8

このようにコードを断片化し、SEHを利用することで各コードブロック間を遷移することで静的解析が非常に困難でした。また、これらの実行コードは書き換え可能な領域にマップされて実行されており、一部の処理には自己書き換えによるコード変更が実行中に行われる解析妨害の機構も組み込まれていました。

より詳しい難読化の解説や解析手法の詳細についてはSuperFashiのWriteup (https://gmo-cybersecurity.com/blog/flare-on-11-write-up/) をご参照ください。

Catbert Ransomware

最後の問題である「Catbert Ransomware」は、UEFI上で動作するランサムウェアを模したUEFI Shellコマンドを解析する課題でした。

qemuを用いて実行すると、以下のように赤い画面のUEFI Shellが表示されました。

このShellには decrypt_file という独自コマンドが実装されており、UEFIを解析すると、独自のVMを用いて暗号化鍵の検証を行っていることが分かります。VMコードを解析して3つの画像を復号すると、それぞれに分割されたフラグの文字列が含まれていました。

3つの画像を復号後、新たにEFIファイルが生成されます。このEFIファイルを実行すると your_mind.jpg.c4tb という新たな暗号化ファイルが生成されますが、パスワードも同時に表示されるため、そのまま実行することでフラグの最後のパートが表示されました。

これらを繋げると th3_ro4d_t0_succ3ss_1s_alw4ys_und3r_c0nstructi0n@flare-on.com というフラグが得られました。

まとめ

簡単ではありますがFlare-On 11で出題された問題について解説を交えて紹介いたしました。全10問の出題内容は以下の特徴がありました。

  1. 解析対象の多様性

    • Python、JavaScript、PowerShell等のスクリプト言語
    • Yaraルール、Verilogといったドメイン特化言語
    • スマートコントラクトや.NET Native AOTといった最新の技術
  2. 幅広い難易度

    • 初心者でも取り組みやすい基礎的な問題(frog、Meme Maker 3000等)
    • 高度な知識を要する難問(serpentine、fullspeed等)
  3. 実践的な題材

    • XZ Utilsバックドアを題材にした問題
    • スマートコントラクトを使ったLOTS攻撃
    • UEFIランサムウェア等、実際のセキュリティインシデントや攻撃手法を題材とした問題

これらの特徴からも分かるように、The Flare-On Challengeを通じてリバースエンジニアリングについて様々なことが学べると思います。ぜひ来年のFlare-On 12に日本からの多くの方々が挑戦し、新たなスキルを身につけるきっかけとなることを期待しています。


GMOサイバーセキュリティ byイエラエではリバースエンジニアリングのスキルを持ったエンジニアを募集しています。IoTやモバイルアプリからペネトレーションテストまで、リバースエンジニアリングを用いた幅広いサービスを提供しておりますので、興味を持たれた方はぜひ以下のURLからお問い合わせください。

採用に関するお問い合わせ

セキュリティ診断のことなら
お気軽にご相談ください
セキュリティ診断で発見された脆弱性と、具体的な内容・再現方法・リスク・対策方法を報告したレポートのサンプルをご覧いただけます。

関連記事

経験豊富なエンジニアが
セキュリティの不安を解消します

Webサービスやアプリにおけるセキュリティ上の問題点を解消し、
収益の最大化を実現する相談役としてぜひお気軽にご連絡ください。

疑問点やお見積もり依頼はこちらから

お見積もり・お問い合わせ

セキュリティ診断サービスについてのご紹介

資料ダウンロード