フィッシングサイトに関する研究のうち、Rami Mustafa Mohammad 氏らの提案する「フィッシングサイトの特徴」について紹介する。2012年頃の研究のため少し古いがなかなか興味深い内容と思う。
Rami M. Mohammad et al. Phishing Websites Features
https://archive.ics.uci.edu/ml/machine-learning-databases/00327/Phishing%20Websites%20Features.docx
本記事の構成は以下の通りである。
- 前半:Rami Mustafa Mohammad 氏らが提案するフィッシングサイトの特徴の紹介
- 後半:彼らが公開しているデータセットを用いたフィッシングサイト判定方法の評価の紹介
前半:フィッシングサイトの特徴
アドレスバーの特徴
(1) IPアドレス
- ドメイン部分にIPアドレスがある場合→フィッシングサイト
- それ以外→正規サイト
(2) 長いURL
フィッシング攻撃者はアドレスバーで疑わしい部分を隠すために、長いURLを使用することがある。
- URL長が54未満 → 正規サイト
- URL長が54以上75以下 → フィッシングサイトの疑いあり
- それ以外 → フィッシングサイト
(3) 短縮URL
- 短縮URL → フィッシングサイト
- それ以外 → 正規サイト
(4) URL 中に "@" 記号
URL 中に "@" 記号があると、ブラウザは"@"マークより前を無視し、"@" 記号の後に本当のアドレスが続くことがある。
- "@" 記号を含むURL → フィッシングサイト
- それ以外 → 正規サイト
(5) "//" によるリダイレクト
URLパスの中に "//" があると、ユーザーは別のウェブサイトにリダイレクトされる。
例:"http://www.legitimate.com//http://www.phishing.com
この「//」が出現する場所を調べてみました。
URLが「HTTP」で始まっている場合、「//」は6番目の位置になり、URLが「HTTPS」を採用している場合は7番目になる。
- URLの最後に出現した"//"の最後の出現位置が7よりも大きい → フィッシングサイト
- それ以外 → 正規サイト
(6)ドメインに(-)で区切られた接頭辞や接尾辞を付加
ダッシュ記号は、正規のURLではほとんど使用されない。
フィッシング攻撃者は、ユーザーが正規のウェブページを扱っていると感じるように、ドメイン名に(-)で区切られた接頭辞または接尾辞を追加する傾向がある。
例: "http://www.Confirme-paypal.com/"
- ドメイン名の一部に(-)記号が含まれる → フィッシングサイト
- それ以外 → 正規サイト
(7) サブドメインとマルチサブドメイン
フィッシング攻撃者はサブドメインを使うことがある。
URLのドメインから先頭の www. と最後の国別トップレベルドメイン(ccTLD)とセカンドレベルドメイン (SLD) を削除して残った文字列の中のドットの個数に着目する。
- ドットの個数=1 → 正規サイト
- ドットの個数=2 → フィッシングサイトの疑いあり
- それ以外 → フィッシングサイト
(8) HTTPS
- httpsを使用し、発行元が信頼され、かつ証明書の期限が1年以上 → 正規サイト
- httpsを使用し、発行元が信頼されていない → フィッシングサイトの疑いあり
- それ以外 → フィッシングサイト
(9) ドメイン登録期間
フィッシングサイトの寿命は短くせいぜい1年以内と推測。一方で信頼できるドメインは数年前から定期的にドメインが登録される。
- ドメインの有効期限≦1年 → フィッシングサイト
- それ以外 → 正規サイト
(10) favicon
もしファビコンがアドレスバーに表示されているドメイン以外から読み込まれた場合、そのウェブページはフィッシングの試みとみなされる可能性がある。
- ファビコンが外部ドメインからロードされている → フィッシングサイト
- それ以外 → 正規サイト
(favicon がどこからダウンロードされているのかアドレスバーでは確認しようがない気もするが)
(11) 非標準ポート
- URL で使われるポート番号が非標準ポート → フィッシングサイト
- それ以外 → 正規サイト
(12) URLのドメイン部分に "HTTPS "トークンが存在
例:"http://https-www-paypal-it-webapps-mpp-home.soft-hair.com/"
- URLのドメイン部分にHTTPトークンが存在 → フィッシングサイト
- それ以外 → 正規サイト
アブノーマルベースの特徴
(13) 外部オブジェクトのリクエストURL
フィッシングサイトではウェブページに埋め込まれる画像・動画・音声などの外部オブジェクトが、別のドメインから読み込まれることがある。
正規サイトのWebページでは、WebページのアドレスとWebページ内に埋め込まれたオブジェクトのほとんどが同じドメインである。
- 外部オブジェクトのリクエストURLの割合が22%未満 → 正規サイト
- 外部オブジェクトのリクエストURLの割合が22%以上61%未満 → フィッシングサイトの疑いあり
- それ以外の場合 → フィッシングサイト
(14) アンカータグの URL
アンカータグ ("a" タグ) の URL に着目する。
アンカータグとウェブサイトのドメイン名が異なる場合は (13) と同様。
- アンカータグの URL の割合が31%未満 → 正規サイト
- アンカータグの URL の割合31%以上かつ67%未満 → フィッシングサイトの疑いあり
- それ以外 → フィッシングサイト
(15) Meta, Script, Link タグのリンク
- ウェブページに含まれるMeta, Script, Link のリンクの割合が17%未満 → 正規サイト
- ウェブページに含まれるMeta, Script, Link のリンクの割合が17%以上かつ81%以下 → フィッシングサイトの疑いあり
- それ以外→ フィッシングサイト
(16) サーバーフォームハンドラ(SFH)
- SFH が「about: blank」または空 → フィッシングサイト
- SFH が別ドメインへの参照 → フィッシングサイトの疑いあり
- それ以外 → 正規サイト
(へぇ~と思って調べてみたんですが、残念ながら SFH が何なのか確認できていません)
(17) 電子メールへの情報送信
- PHPの mail関数や「mailto:」を使用してユーザー情報を送信 → フィッシングサイト
- それ以外 → 正規サイト
(18) 異常なURL
この機能は、WHOISデータベースから抽出することができます。
正規のウェブサイトの場合、IDは通常そのURLの一部である。
ホスト名がURLに含まれていない→フィッシング
それ以外→正規のもの
HTML と JavaScript ベースの特徴
(19) ウェブサイトの転送
- リダイレクトページの数が1以下 → 正規サイト
- リダイレクトページの数が2以上かつ4未満 → フィッシングサイトの疑いあり
- それ以外 → フィッシングサイト
(20) ステータスバーのカスタマイズ
- onMouseOverでステータスバーが変化 → フィッシングサイト
- ステータスバーを変化させない → 正規サイト
(21) 右クリック無効化
- 右クリック無効化 → フィッシングサイト
- それ以外 → 正規サイト
(22) ポップアップウィンドウ
- ポップアップウィンドウにテキストフィールドが含まれる → フィッシングサイト
- それ以外 → 正規サイト
(23) IFrameリダイレクト
- iframeの利用 → フィッシングサイト
- それ以外 → 正規サイト
ドメインベースの特徴
(24) ドメインの年齢
- ドメインの年齢が6ヶ月以上 → 正規サイト
- それ以外 → フィッシングサイト
(25) DNSレコード
- ドメインのDNSレコードがない → フィッシングサイト
- そうでない場合 → 正規サイト
(26) ウェブサイトのトラフィック
訪問者数と訪問したページ数を判断することで、ウェブサイトの人気を測定する。フィッシング・ウェブサイトは短期間しか生存しないため Alexaデータベース登録されない可能性があります。
- ウェブサイトランクが100000以下 → 正規サイト
- ウェブサイトランクが100000より大きい → フィッシングサイトの疑いあり
- それ以外の場合 → フィッシングサイト
(ペーパーでは Alexa データベースでのウェブサイトランクのことのようだが、現在はもっと新しい評価サイトを使った方がよいかも)
(27) ページランク
PageRankは "0" から "1"までの値であり、あるウェブページがインターネット上でどれだけ重要であるかを測定するのにつかわれる。
PageRankの値が大きいほど、そのウェブページはより重要である。
- PageRankが0.2未満 → フィッシングサイト
- それ以外 → 正規サイト
(28) Googleインデックス
ウェブサイトがGoogleのインデックスに登録されているかどうか。
フィッシングサイトの寿命は短いため多くのフィッシングサイトがGoogleインデックスに掲載されない可能性がある。
- Googleにインデックスされるウェブページ → 正規サイト
- そうでない場合 → フィッシングサイト
(29) ページを指すリンクの数
そのウェブページを指すリンクの数はその正当性のレベルを示す。
- ウェブページを指すリンクの数がない → フィッシングサイト
- ウェブページを指すリンクの数が2以下 → フィッシングサイトの疑いあり
- それ以外 → 正規サイト
(30) 統計レポート
- ホストが Top フィッシングIPまたは Top フィッシングドメインに属している → フィッシングサイト
- それ以外 → 正規のもの
関連研究
いずれもググれば PDF などを見つけることができる。
- Mohammad, Rami, McCluskey, T.L. and Thabtah, Fadi (2012) An Assessment of Features Related to Phishing Websites using an Automated Technique. In: International Conferece For Internet Technology And Secured Transactions. ICITST 2012 . IEEE, London, UK, pp. 492-497. ISBN 978-1-4673-5325-0
- Mohammad, Rami, Thabtah, Fadi Abdeljaber and McCluskey, T.L. (2014) Predicting phishing websites based on self-structuring neural network. Neural Computing and Applications, 25 (2). pp. 443-458. ISSN 0941-0643
- Mohammad, Rami, McCluskey, T.L. and Thabtah, Fadi Abdeljaber (2014) Intelligent Rule based Phishing Websites Classification. IET Information Security, 8 (3). pp. 153-160. ISSN 1751-8709
後半:フィッシングサイトに関するデータセット
後半では、公開されているフィッシングサイトに関するデータセットを紹介する。このデータセットでは実際に1万件超のWebサイトについて前半のフィッシングサイトの特徴を抽出しフィッシングサイトかどうかを調べたものとなっている。
本記事ではロジスティク回帰を用いた二項分類器を実装し、前半のフィッシングサイトの特徴を用いたフィッシングサイト判定を評価してみる。
データセットの内容
データセットをダウンロードして、"dataset.arff" というファイル名で保存する。
curl -o dataset.arff https://archive.ics.uci.edu/ml/machine-learning-databases/00327/Training%20Dataset.arff
dataset.arff の中身はこんな感じ。
@relation phishing
@attribute having_IP_Address { -1,1 }
@attribute URL_Length { 1,0,-1 }
@attribute Shortining_Service { 1,-1 }
@attribute having_At_Symbol { 1,-1 }
@attribute double_slash_redirecting { -1,1 }
@attribute Prefix_Suffix { -1,1 }
@attribute having_Sub_Domain { -1,0,1 }
@attribute SSLfinal_State { -1,1,0 }
@attribute Domain_registeration_length { -1,1 }
@attribute Favicon { 1,-1 }
@attribute port { 1,-1 }
@attribute HTTPS_token { -1,1 }
@attribute Request_URL { 1,-1 }
@attribute URL_of_Anchor { -1,0,1 }
@attribute Links_in_tags { 1,-1,0 }
@attribute SFH { -1,1,0 }
@attribute Submitting_to_email { -1,1 }
@attribute Abnormal_URL { -1,1 }
@attribute Redirect { 0,1 }
@attribute on_mouseover { 1,-1 }
@attribute RightClick { 1,-1 }
@attribute popUpWidnow { 1,-1 }
@attribute Iframe { 1,-1 }
@attribute age_of_domain { -1,1 }
@attribute DNSRecord { -1,1 }
@attribute web_traffic { -1,0,1 }
@attribute Page_Rank { -1,1 }
@attribute Google_Index { 1,-1 }
@attribute Links_pointing_to_page { 1,0,-1 }
@attribute Statistical_report { -1,1 }
@attribute Result { -1,1 }
@data
-1,1,1,1,-1,-1,-1,-1,-1,1,1,-1,1,-1,1,-1,-1,-1,0,1,1,1,1,-1,-1,-1,-1,1,1,-1,-1
1,1,1,1,1,-1,0,1,-1,1,1,-1,1,0,-1,-1,1,1,0,1,1,1,1,-1,-1,0,-1,1,1,1,-1
・・・以下省略・・・
ARFF 形式で説明変数(これまでみてきたフィッシングサイトの30個の特徴)と目的変数 Result(フィッシングサイトかどうか)の値定義があり、@data
以降は各サイトについて調べた説明変数・目的変数の値の CSV データが続く。
CSVデータの各行が具体的にどのサイトのことなのかはわからないが、30個の特徴を数値化しているのがわかる。
素晴らしい!
機械学習による評価
フィッシングサイトかどうかの二値分類問題なので単純に「ロジスティック回帰分析」を使った。
一応、Rust 修行中の身なので Python は使わず Rust を使い smartcore というクレイトを利用した。
ホールドアウト検証
ロジスティック回帰分析を用いたホールドアウト検証の実装例を示す。
// main.rs
const DATASET_FILE:&str = "dataset.arff";
use smartcore::linalg::basic::matrix::DenseMatrix;
use smartcore::linear::logistic_regression::LogisticRegression;
use smartcore::model_selection::train_test_split;
use smartcore::metrics::{accuracy, precision, recall, f1};
use smartcore::linalg::basic::arrays::Array;
use rand::Rng;
use phishweb::err::MyResult;
use phishweb::dataset::{Dataset, load_dataset};
fn main() {
if let Err(e) = process() {
eprintln!("[Error] {}", e)
}
}
fn process() -> MyResult<()> {
// データセットの読み出し
let data: Dataset = load_dataset(DATASET_FILE)?;
let x = DenseMatrix::new(data.num_samples, data.num_features, data.data, false);
let y = data.target;
// 学習データと試験データの取得(データセットより 8:2 の割合でランダムに取得)
// 学習データ:x_train, y_train
// 試験データ:x_test, y_test
let mut rng = rand::thread_rng();
let (x_train, x_test, y_train, y_test) = train_test_split(
&x, &y, 0.2, true, Some(rng.gen::<u64>()));
println!("x_train: {:?}", x_train.shape());
println!("y_train: {:?}", y_train.shape());
println!("x_test: {:?}", x_test.shape());
println!("y_test: {:?}", y_test.shape());
// 学習
let model = LogisticRegression::fit(
&x_train,
&y_train,
Default::default(),
)?;
// 予測データの取得
let pred = model.predict(&x_test)?;
// 試験データ y_test と予測データ pred より評価
println!("accuracy: {}", accuracy(&y_test, &pred));
// F値等を求めるため f32 に変換した試験データと予測データを用意。
let y_test_f32:Vec<f32> = y_test.iter().map(|x|*x as f32).collect();
let pred_f32:Vec<f32> = pred.iter().map(|x|*x as f32).collect();
println!("precision: {}", precision(&pred_f32, &y_test_f32));
println!("recall: {}", recall(&pred_f32, &y_test_f32));
println!("f-measure: {}", f1(&pred_f32, &y_test_f32, 1.0));
Ok(())
}
以降は上の process 関数を動かすための補助的なコードである。
本質的な部分ではないので Rust 実装に興味のない方は評価結果まで読み飛ばしてもかまわない。
データセット
ARFF 形式を適切に扱えるクレイトを見つけられなかったので @data
以降の CSV データだけを読み出すという安直な実装にした。
// dataset.rs
const NUM_FEATURES:usize = 30;
use std::io::{BufRead, BufReader};
use std::str::FromStr;
use crate::err::MyError;
#[derive(Debug)]
pub struct Dataset {
pub data: Vec<f32>, // 説明変数、Row-major order
pub target: Vec<i32>, // 目的変数
pub num_samples: usize, // 標本数
pub num_features: usize, // 特徴数
// [MEMO] このデータは以下の制約を必ず満たさねばならない
// num_samples > 0
// num_features > 0
// data.len() == num_samples*num_features
// target.len() == num_samples
}
// load_dataset はデータセットを読み出す関数。
// [MEMO] 入力対象ファイルは arff 形式の @data 行以降に
// CSV形式でデータが並ぶことを想定していることに注意
pub fn load_dataset(file: &str) -> Result<Dataset, MyError> {
let mut reader = BufReader::new(std::fs::File::open(file)?);
let mut buf = String::new();
// [*] 前半の arff 形式特有の行を読み飛ばす
while reader.read_line(&mut buf)? > 0 {
// @data が出るまでスキップ
//println!("=>{}", buf);
if buf.starts_with("@data") {
break;
}
buf.clear();
}
// 入力対象ファイルがただのCSV形式の場合は上の while 文を
// コメントにすればイケる(はず)
let mut d: Vec<f32> = vec![];
let mut t: Vec<i32> = vec![];
let mut csv_reader = csv::Reader::from_reader(reader);
for record in csv_reader.records() {
let record = record?;
for (i, item) in record.iter().enumerate() {
let v = f32::from_str(item)?;
if i < NUM_FEATURES {
d.push(v);
} else {
t.push(if v> 0.0 {1} else {0});
}
}
}
let num_samples = d.len() / NUM_FEATURES;
// validation
if num_samples != t.len() {
return Err(MyError::DatasetError)
}
Ok(Dataset {
data: d,
target: t,
num_samples: num_samples,
num_features: NUM_FEATURES,
})
}
エラー定義等
ググってみつけられる smartcore のサンプルコードはすべて unwrap()
していてダサかったのと、IO エラーやら CSV エラーやら複数のエラー型を扱う必要があったので ?
でまとめてエラー処理できるよう独自エラー型と独自 Result 型を定義した。
// err.rs
#[derive(thiserror::Error, Debug)]
pub enum MyError {
#[error("SmartcoreError:{0}")]
SmartcoreError(#[from] smartcore::error::Failed),
#[error("IOError:{0}")]
IOError(#[from] std::io::Error),
#[error("CsvError:{0}")]
CsvError(#[from] csv::Error),
#[error("ParseFloatError:{0}")]
ParseFloatError(#[from] std::num::ParseFloatError),
#[error("ParseIntError:{0}")]
ParseIntError(#[from] std::num::ParseIntError),
#[error("DatasetError")]
DatasetError
}
pub type MyResult<T> = Result<T, MyError>;
// lib.rs
pub mod dataset;
pub mod err;
評価結果
以下のようなコマンドでコンパイル&実行。
cargo add smartcore@0.3.2 thiserror csv rand
cargo run
実行結果は以下のようになった。
(毎回同じ結果になるとは限らない)
x_train: (8844, 30)
y_train: 8844
x_test: (2210, 30)
y_test: 2210
accuracy: 0.9285067873303168
precision: 0.9244274809160306
recall: 0.9535433070866142
f-measure: 0.9387596899224805
学習データは 8844 件、試験データは 2210 件で検証し、精度は 92.9%、適合率は 92.4%、再現率は 95.3%、F値は 93.9% だった。
ざっくり言うと、前半でみたようなフィッシングサイトの特徴を抽出することにより 90% 超の正解率でフィッシングサイトかどうかを見分けることができる、ということを示している。