Python pty + Frida を用いたバイナリの動的解析 (Simple Flag Checker Writeup)

Python スクリプトで pty を用いて仮想端末上でバイナリを制御し, Frida を扱うことで動的解析を行う方法の覚え書き.
題材として AlpacaHack の Simple Flag Checker を解く.
Frida の Windows での基礎的な使用方法は 前回記事 で解説.

検証環境

Frida について

インストール

uv add frida-tools
        

Simple Flag Checker の問題設定の確認

do {
    update(&update_param,input_flag[lVar3]);
    iVar2 = memcmp(&update_param,table + lVar3 * 0x10,0x10);
    bVar1 = (bool)(bVar1 & iVar2 == 0);
    lVar3 = lVar3 + 1;
} while (lVar3 != 0x31);
        
memcmp(0x7ffd398c6450, 0x64dedb131020, 16, 9)                                                    = 0
memcmp(0x7ffd398c6450, 0x64dedb131030, 16, 9)                                                    = 0
memcmp(0x7ffd398c6450, 0x64dedb131040, 16, 9)                                                    = 0
memcmp(0x7ffd398c6450, 0x64dedb131050, 16, 9)                                                    = 0
memcmp(0x7ffd398c6450, 0x64dedb131060, 16, 9)                                                    = 0
memcmp(0x7ffd398c6450, 0x64dedb131070, 16, 9)                                                    = 0
memcmp(0x7ffd398c6450, 0x64dedb131080, 16, 9)                                                    = 239
memcmp(0x7ffd398c6450, 0x64dedb131090, 16, 9)                                                    = 0xffffffd4
memcmp(0x7ffd398c6450, 0x64dedb1310a0, 16, 9)                                                    = 0xffffff74
memcmp(0x7ffd398c6450, 0x64dedb1310b0, 16, 9)                                                    = 15
memcmp(0x7ffd398c6450, 0x64dedb1310c0, 16, 9)                                                    = 0xffffff8f
memcmp(0x7ffd398c6450, 0x64dedb1310d0, 16, 9)                                                    = 209
memcmp(0x7ffd398c6450, 0x64dedb1310e0, 16, 9)                                                    = 0xffffff7a
memcmp(0x7ffd398c6450, 0x64dedb1310f0, 16, 9)                                                    = 16
memcmp(0x7ffd398c6450, 0x64dedb131100, 16, 9)                                                    = 36
memcmp(0x7ffd398c6450, 0x64dedb131110, 16, 9)                                                    = 28
memcmp(0x7ffd398c6450, 0x64dedb131120, 16, 9)                                                    = 0xffffffb2
memcmp(0x7ffd398c6450, 0x64dedb131130, 16, 9)                                                    = 34
memcmp(0x7ffd398c6450, 0x64dedb131140, 16, 9)                                                    = 0xffffffba
memcmp(0x7ffd398c6450, 0x64dedb131150, 16, 9)                                                    = 0xfffffff9
memcmp(0x7ffd398c6450, 0x64dedb131160, 16, 9)                                                    = 16
memcmp(0x7ffd398c6450, 0x64dedb131170, 16, 9)                                                    = 0xffffffe1
memcmp(0x7ffd398c6450, 0x64dedb131180, 16, 9)                                                    = 0xffffffd2
memcmp(0x7ffd398c6450, 0x64dedb131190, 16, 9)                                                    = 27
memcmp(0x7ffd398c6450, 0x64dedb1311a0, 16, 9)                                                    = 0xffffff58
memcmp(0x7ffd398c6450, 0x64dedb1311b0, 16, 9)                                                    = 18
memcmp(0x7ffd398c6450, 0x64dedb1311c0, 16, 9)                                                    = 0xffffffe8
memcmp(0x7ffd398c6450, 0x64dedb1311d0, 16, 9)                                                    = 0xffffffad
memcmp(0x7ffd398c6450, 0x64dedb1311e0, 16, 9)                                                    = 142
memcmp(0x7ffd398c6450, 0x64dedb1311f0, 16, 9)                                                    = 213
memcmp(0x7ffd398c6450, 0x64dedb131200, 16, 9)                                                    = 0xffffffa6
memcmp(0x7ffd398c6450, 0x64dedb131210, 16, 9)                                                    = 0xfffffff9
memcmp(0x7ffd398c6450, 0x64dedb131220, 16, 9)                                                    = 0xffffffd4
memcmp(0x7ffd398c6450, 0x64dedb131230, 16, 9)                                                    = 198
memcmp(0x7ffd398c6450, 0x64dedb131240, 16, 9)                                                    = 119
memcmp(0x7ffd398c6450, 0x64dedb131250, 16, 9)                                                    = 89
memcmp(0x7ffd398c6450, 0x64dedb131260, 16, 9)                                                    = 0xffffffc0
memcmp(0x7ffd398c6450, 0x64dedb131270, 16, 9)                                                    = 0xffffff9f
memcmp(0x7ffd398c6450, 0x64dedb131280, 16, 9)                                                    = 43
memcmp(0x7ffd398c6450, 0x64dedb131290, 16, 9)                                                    = 0xffffffe6
memcmp(0x7ffd398c6450, 0x64dedb1312a0, 16, 9)                                                    = 68
memcmp(0x7ffd398c6450, 0x64dedb1312b0, 16, 9)                                                    = 0xffffffdd
memcmp(0x7ffd398c6450, 0x64dedb1312c0, 16, 9)                                                    = 0xffffffcf
memcmp(0x7ffd398c6450, 0x64dedb1312d0, 16, 9)                                                    = 85
memcmp(0x7ffd398c6450, 0x64dedb1312e0, 16, 9)                                                    = 183
memcmp(0x7ffd398c6450, 0x64dedb1312f0, 16, 9)                                                    = 0xffffff81
memcmp(0x7ffd398c6450, 0x64dedb131300, 16, 9)                                                    = 24
memcmp(0x7ffd398c6450, 0x64dedb131310, 16, 9)                                                    = 0xffffffef
memcmp(0x7ffd398c6450, 0x64dedb131320, 16, 9)                                                    = 7
        

pty の設定

端末の作成

import pty

prime_fd, second_fd = pty.openpty()
        

プログラムの実行

import subprocess

proc = subprocess.Popen(
    ["./checker"],
    stdin=second_fd,
    stdout=second_fd,
    stderr=second_fd,
    close_fds=True,
)
        

プロンプトが出現するまで文字列を読み出す

import os

def recv_prompt(fd, prompt):
    buf = b""
    
    while True:
        tmp_buf = os.read(fd, 1)
        if not tmp_buf:
            print("end reading!")
            break
        buf += tmp_buf
        if prompt in buf:
            return buf
        

文字列の送信

os.write(prime_fd, f"Alpaca\n".encode())
        

残りの文字列をすべて読み込む

import select

def recv_all(fd, timeout=2.0):
    buf = b""
    t0 = time.time()
    
    while time.time() - t0 < timeout:
        r, _, _ = select.select([fd], [], [], 0.1) # if readable
        if r:
            tmp_buf = os.read(fd, 1) # this process is blocking I/O
            if not tmp_buf:
                break
            print(tmp_buf.decode(), end="")
            buf += tmp_buf
    return buf
        

frida の設定

基本的なスクリプトのアタッチ方法

proc = subprocess.Popen(
    ["./checker"],
    stdin=second_fd,
    stdout=second_fd,
    stderr=second_fd,
    close_fds=True,
)

session = frida.attach(proc.pid)
        
script = session.create_script($SCRIPT)
script.on("message", on_message) # add signal handler
script.load()
        

frida のスクリプト

check_script = """
console.log("script loaded");

var main = Process.enumerateModules()[0];  // main binary
main.enumerateImports().forEach(function(imp) {
    console.log("Import:", imp.name);
});
"""

session = frida.attach(proc.pid)
script = session.create_script(check_script)
script.load()
        
hook_script = """
var main = Process.enumerateModules()[0];  // main binary

main.enumerateImports().forEach(function(imp) {
    if (imp.name == "memcmp") {
        Interceptor.attach(imp.address, {
                onEnter: function(args) {
                this.s1 = args[0];
                this.s2 = args[1];
                this.n  = args[2].toInt32();
            },
            onLeave: function(retval) {
                send({
                    type: "memcmp",
                    ret: retval.toInt32(),
                    n: this.n
                });
            }
        });
    }
});
"""
        
hit_len = 0
def on_message(msg, data):
    global hit_len
    if msg["type"] != "send":
        return
    payload = msg["payload"]
    if payload["type"] == "memcmp":
        ret_val = payload["ret"]
        
        if ret_val == 0:
            hit_len += 1
        

結果・考察など

コード全文

import frida
import pty
import subprocess
import os
import time
import select
import string

def recv_prompt(fd, prompt):
    buf = b""
    
    while True:
        tmp_buf = os.read(fd, 1)
        if not tmp_buf:
            print("end reading!")
            break
        buf += tmp_buf
        if prompt in buf:
            return buf
        
def recv_all(fd, timeout=2.0):
    buf = b""
    t0 = time.time()
    
    while time.time() - t0 < timeout:
        r, _, _ = select.select([fd], [], [], 0.1) # if readable
        if r:
            tmp_buf = os.read(fd, 1) # this process is blocking I/O
            if not tmp_buf:
                break
            print(tmp_buf.decode(), end="")
            buf += tmp_buf
    return buf


hook_script = """
var main = Process.enumerateModules()[0];  // main binary

main.enumerateImports().forEach(function(imp) {
    if (imp.name == "memcmp") {
        Interceptor.attach(imp.address, {
                onEnter: function(args) {
                this.s1 = args[0];
                this.s2 = args[1];
                this.n  = args[2].toInt32();
            },
            onLeave: function(retval) {
                send({
                    type: "memcmp",
                    ret: retval.toInt32(),
                    n: this.n
                });
            }
        });
    }
});
"""

check_script = """
console.log("script loaded");

var main = Process.enumerateModules()[0];  // main binary
main.enumerateImports().forEach(function(imp) {
    console.log("Import:", imp.name);
});
"""


flag = "Alpaca"
for i in range(len(flag), 0x31):
    for char in (string.digits + r"{}_" + string.ascii_letters):
        hit_len = 0
        def on_message(msg, data):
            global hit_len
            if msg["type"] != "send":
                return
            payload = msg["payload"]
            if payload["type"] == "memcmp":
                ret_val = payload["ret"]
                
                if ret_val == 0:
                    hit_len += 1

        prime_fd, second_fd = pty.openpty()

        proc = subprocess.Popen(
            ["./checker"],
            stdin=second_fd,
            stdout=second_fd,
            stderr=second_fd,
            close_fds=True,
        )

        session = frida.attach(proc.pid)
        script = session.create_script(hook_script)
        script.on("message", on_message) # add signal handler
        script.load()

        prompt = recv_prompt(prime_fd, "flag?".encode())
        print(prompt.decode())

        os.write(prime_fd, f"{flag+char}\n".encode())

        recv_all(prime_fd, timeout=0.1)
        
        if hit_len < i+1:
            continue
        else:
            flag += char
            break
    if len(flag) < i+1:
        print("no hit...")
        print("tmp flag:", flag)
        exit()