OpenCVで顔認証を実装したら、印刷した写真であっさり突破された【C++】

なりすまし判定・ライブネス検知
📌 準備: OpenCV の環境構築がまだの方はこちら → C++ 環境構築ガイドC#(OpenCvSharp)セットアップ

OpenCVで顔認証を実装したら、印刷した写真であっさり突破された【C++】

前回の記事で、OpenCV 標準の YuNet + SFace だけを使って顔認証パイプラインを組みました。登録した本人は通り、他人は弾かれる。ここまでは期待どおりに動きます。

では、カメラの前に立つのが本人ではなく、本人の顔写真を印刷した紙だったらどうなるでしょうか。

結論から書くと——通ります。コンビニのマルチコピー機で印刷した写真で、コサイン類似度は本人判定の閾値を超えてきます。スマホの画面に顔写真を表示してかざしても同じです。

筆者ら(スワローインキュベート)はなりすまし判定SDKの開発・評価の過程でこの挙動を繰り返し実測してきましたが、本記事のコードは手元でそのまま再現できるので、ぜひ自分の顔でご確認ください。


実験の構成

登録画像から特徴量を作っておき、カメラに映った顔と照合し続けるプログラムを用意します。

ケース カメラの前にあるもの
本人(実物)
本人の顔写真を印刷した紙
本人の顔写真を表示したスマホ画面

コード(C++)

#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    auto detector = cv::FaceDetectorYN::create(
        "face_detection_yunet_2023mar.onnx", "", cv::Size(320, 320));
    auto recognizer = cv::FaceRecognizerSF::create(
        "face_recognition_sface_2021dec.onnx", "");

    // 登録画像(本人の正面写真)から特徴量を作っておく
    cv::Mat enroll = cv::imread("enroll.jpg");
    if (enroll.empty()) {
        std::cerr << "enroll.jpg が読み込めません" << std::endl;
        return -1;
    }
    detector->setInputSize(enroll.size());
    cv::Mat facesEnroll;
    detector->detect(enroll, facesEnroll);
    if (facesEnroll.rows < 1) {
        std::cerr << "登録画像から顔が検出できません" << std::endl;
        return -1;
    }
    cv::Mat alignedE, featEnroll;
    recognizer->alignCrop(enroll, facesEnroll.row(0), alignedE);
    recognizer->feature(alignedE, featEnroll);
    featEnroll = featEnroll.clone();

    // カメラの前の「何か」を照合し続ける
    cv::VideoCapture cap(0);
    cv::Mat frame;
    while (cap.read(frame)) {
        detector->setInputSize(frame.size());
        cv::Mat faces;
        detector->detect(frame, faces);
        if (faces.rows > 0) {
            cv::Mat aligned, feat;
            recognizer->alignCrop(frame, faces.row(0), aligned);
            recognizer->feature(aligned, feat);
            double score = recognizer->match(featEnroll, feat,
                                             cv::FaceRecognizerSF::DisType::FR_COSINE);
            bool same = score >= 0.363;
            cv::Rect box(cv::Point((int)faces.at<float>(0, 0), (int)faces.at<float>(0, 1)),
                         cv::Size((int)faces.at<float>(0, 2), (int)faces.at<float>(0, 3)));
            cv::Scalar color = same ? cv::Scalar(0, 255, 0) : cv::Scalar(0, 0, 255);
            cv::rectangle(frame, box, color, 2);
            cv::putText(frame, same ? "MATCH" : "NO MATCH", cv::Point(20, 40),
                        cv::FONT_HERSHEY_SIMPLEX, 1.0, color, 2);
        }
        cv::imshow("face auth", frame);
        if (cv::waitKey(1) == 'q') break;
    }
    return 0;
}

結果

ケース 判定
① 本人(実物) MATCH(期待どおり)
② 印刷した顔写真 MATCH — 閾値 0.363 を超える
③ スマホ画面の顔写真 MATCH — 同上

②③とも、紙やスマホを顔の位置にかざすだけで緑の枠(MATCH)が出ます。印刷のモアレやスマホ画面の反射があっても、YuNet は問題なく顔を検出し、SFace は本人の特徴量を抽出します。


なぜ通ってしまうのか — バグではなく「答えている問いが違う」

種明かしをすると、これは実装ミスでもモデルの欠陥でもありません。

顔照合(Face Recognition)が答えている問いは「この顔は登録された人物と同じか」です。印刷した写真にもスマホ画面にも、本人の顔特徴はそのまま写っています。だから SFace は正しく「同じ人物の顔だ」と答えている——仕組みとして正しく動いた結果、認証としては突破されているわけです。

「本人がその場に実在しているか」は、顔照合とはまったく別の問いです。この問いに答える技術は ライブネス検知(Liveness Detection)/ PAD(Presentation Attack Detection・提示攻撃検知) と呼ばれ、専用の判定器が必要になります。

顔照合:        「誰の顔か」   → 写真でも本人なら一致する(正しい動作)
ライブネス検知: 「生体か偽物か」 → 写真・画面・マスクを見分ける(別の技術)

🛡 この「別の技術」を後付けできます

スワローインキュベートのなりすまし判定SDK/APIは、単一フレームの画像から生体顔かフェイク顔(写真・画面等)かを判定します。C++/C# 対応・CPU推論・オフライン動作可。本記事のようなOpenCV顔認証パイプラインにそのまま組み込めます。

→ なりすまし判定SDK/APIの詳細を見る


これは現実の脅威なのか

「わざわざ写真を印刷して認証を突破する人がいるのか?」と思うかもしれません。ですが考えてみてください。攻撃素材(顔写真)は SNS やウェブ上に本人が自分で公開していることが珍しくありません。必要な道具はプリンタかスマホだけ。つまり、印刷写真・画面表示によるなりすましは、もっとも低コストで実行できてしまう攻撃です。

だからこそ、勤怠打刻・入退室・本人確認など「なりすまされると誰かが得をする」システムに素の顔認証を置くのは危険です。攻撃にどんな類型があり、用途ごとにどこまで守るべきかは、次の記事で整理しています。

なりすまし攻撃(プレゼンテーション攻撃)の種類と、どこまでが現実的な脅威か


まとめ

  • OpenCV(YuNet + SFace)の顔認証は、本人の印刷写真・スマホ画面表示で突破できてしまう。本記事のコードで誰でも再現可能
  • これはバグではない。顔照合は「誰の顔か」に答える仕組みであり、「本人がその場にいるか」には最初から答えていない
  • 認証として運用するなら、顔照合とは別にライブネス検知(PAD)を組み込む必要がある
  • 攻撃素材(顔写真)は公開情報から手に入る時代。印刷写真・画面表示は最も低コストな攻撃として、真っ先に対策すべき対象
タイトルとURLをコピーしました