【アプリ保護にも応用可能】C言語のラッパー関数をラップ対象APIの代わりに呼び出す方法(iOS)

コラム概要

■こんな方におすすめ:

C言語のラッパー関数をラップ対象の代わりに呼び出すテクニックを知りたい
実行時にアプリ自身(Mach-O形式)にパッチを当てる方法を知りたい
これらのアプリ保護への応用を知りたい

■難易度:★☆☆~★★☆

■ポイント:

  • マクロ定義を利用する方法と、その限界
  • 対象と同じシンボルを作成する方法と、アプリ保護への応用例
  • 実行時にシンボル情報のセクションをパッチする方法と、アプリ保護への応用例

1. はじめに

今回は、標準CライブラリのAPIのラッパー関数を作成・使用する上で便利なテクニックをご紹介します。
アプリのセキュリティ対策としても利用できますので、是非ご一読ください。

1.1. ラッパー関数とは

関数呼び出しを内包(=ラップ)している関数のことです。
putsのラッパー関数を例示します。

int PutsWrapper(const char *sz)
{
    printf("I am a puts wrapper.\n"); //処理追加
    return puts(sz);
}

putsを呼び出すようにPutsWrapperを呼び出すと、追加した処理がputs本来の処理の前に実行されます。

1.2. ラッパー関数を使用するメリット

インターフェースを変更できる

ラッパー関数を使用することによって、APIのインターフェースを用途に合わせて変更することができます。
例えば、引数を簡略化するなどです。
以下にpthread_createの例を示します。

pthread_t PthreadCreateWrapper(void *(*fn)(void *)) { //1引数に簡略化
    pthread_t thread;
    pthread_create(&thread, NULL, fn, NULL);          //本来は4引数
    return thread;
}

第2・第4引数がNULLであるケースについて、スレッド関数(fn)以外の引数が省略できています。

機能を追加できる

ラッパー関数を使用することによって、APIに独自の機能を追加することができます。
以下にmallocの例を示します。

#define MallocWrapper(iSize)    _MallocWrapper(iSize, __FUNCTION__)
void *_MallocWrapper(size_t iSize, char *szCaller)
{
    void *pRet = malloc(iSize);
    if (!pRet) {
        printf("ERROR: %s: malloc failed.\n", szCaller);
        return NULL;
    }

    return pRet;
}

ソースコード中のMallocWrapper呼び出しを、マクロ定義で_MallocWrapperの呼び出しに置換します。
_MallocWrapperはmallocの失敗時に呼び出し元の関数名を出力するラッパー関数です。

セキュリティの観点からは、任意のAPI呼び出し時にセキュリティ処理を実行することができます。
例えば以下のようなセキュリティ処理です。
・ラップ対象のAPIが改ざんされていないかの確認
・重要な変数が改ざんされていないかの確認
・攻撃環境で実行されていないかの確認
・数値の暗号化/復号

2. ラッパー関数をラップ対象のAPIの代わりに呼び出す方法

2.1. マクロ定義による方法

おそらくよく知られているであろう方法が、マクロ定義による置換です。
以下にmallocの例を示します。

void *_MallocWrapper(size_t iSize, const char *szCaller)    //_MallocWrapperの定義
{ ... }
#define malloc(iSize)   _MallocWrapper(iSize, __FUNCTION__) //mallocの呼び出しを_MallocWrapperの呼び出しに置換

上記のようにマクロ定義をすれば、以降の全てのmallocの呼び出しがラッパー関数の呼び出しに置換されます。
ラッパー関数内のmallocが置換されてしまわないように、ラッパー関数の後でマクロ定義を行っています。

2.2. マクロ定義による方法の限界

マクロ定義による方法には、非常に明確な限界があります。
ソースコード上の呼び出しを置換する手法なので、すでにコンパイルされている静的ライブラリ内部の呼び出しは置換できないのです。
ライブラリをリビルドしたくない、ライブラリのソースコードがない、などの場合、この方法は使えません。

2.3. 方法1:対象と同じシンボルを作成する方法

手順

ラップ対象のAPIと戻り値・名称・引数が同一のラッパー関数を作成します。
これだけで、ラップ対象のAPIを呼び出した際にこのラッパー関数が呼ばれます。
ラッパー関数からラップ対象のAPIを呼び出すために、関数内部でシンボル解決を行います。

int puts(const char *sz)
{
    int (*fn)(const char *) = dlsym(RTLD_NEXT, "puts"); //「本物の」putsのシンボルを解決
    if (fn) {
        fn("I am a puts wrapper."); //処理追加
        return fn(sz);
    }
    return EOF;
}

この方法が、静的ライブラリ内部のAPI呼び出しについても有効であることを確認できるようにします。
具体的には、内部でputsを呼び出すライブラリ関数CheckAPI_StaticLibraryを提供する静的ライブラリPutsLib.aを作成します。
実装は以下です。

void CheckAPI_StaticLibrary(char *sz)
{
    puts("CheckAPI_StaticLibrary");
    puts(sz);
}

PutsLib.aをiOSアプリにリンクします。
main関数からputs, CheckAPI_StaticLibraryを呼び出すiOSアプリを作成して、動作確認を行います。
(Swiftの場合は適宜読み替えてください。)

int main(int argc, char * argv[])
{
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }

    // 動作確認
    puts("Check: puts");
    CheckAPI_StaticLibrary("Check: CheckAPI_StaticLibrary (static library)");

    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

結果

ラッパー関数経由で標準Cライブラリのputsが呼ばれていることが分かります。

アプリ保護への応用

この方法は、アプリ保護にも利用できます。
例えば、使用しているAPIを隠蔽することが可能です。
関数内部で明示的にシンボルを解決しているからです。
以下のようにAPI名の文字列を暗号化しておけば、より強力に隠蔽することができます。

int puts(const char *sz)
{
    unsigned char szEncryptedPuts[] = {...}; //暗号化された"puts"
    int (*fn)(const char *) = dlsym(RTLD_NEXT, MyDecrypt(szEncryptedPuts)); //"puts"を復号して「本物の」putsのシンボルを解決
    if (fn) {
        return fn(sz);
    }
    return EOF;
}

2.4. 方法2:実行時にシンボル情報のセクションをパッチする方法

実用上は方法1で十分なのですが、実行時にメモリ上のアプリイメージをアプリ自身でパッチしてしまう方法もあります。
メモリ上にマップされたiOSアプリには、シンボル情報を記録するセクションがあります。
どのセクションがシンボル情報を記録するのかは、ビルド時に指定するサポート下限のiOS(Minimum Deployments)によって異なる場合があります。
今回は、Minimum Deploymentsが12.0の場合の方法、すなわち__DATAセグメントの__la_symbol_ptrセクションをパッチする方法について説明します。

手順

まずはラッパー関数を作成します。
後ほど、通常の関数呼び出しでラップ対象のAPIを呼び出した際にこのラッパー関数を呼ぶようにします。

int PutsWrapper(const char *sz)
{
    int (*fn)(const char *) = dlsym(RTLD_NEXT, "puts"); //「本物の」putsのシンボルを解決
    if (fn) {
        fn("I am a puts wrapper."); //処理追加
        return fn(sz);
    }
    return EOF;
}

アプリの実行時に__la_symbol_ptrセクションのエントリを変更してやることでputsの代わりにPutsWrapperが呼び出されるようにします。

iOSアプリがラップ対象APIのアドレスを参照すると、そのアドレスが__la_symbol_ptrセクションに記録されます。
これをラッパー関数のアドレスに変更する関数HookAPIを作成します。

ロジックの概要は以下です。

dladdr, Dl_info構造体のdli_fbaseから、アプリ自身のイメージベースを取得

getsectbynameでアプリ自身の__la_symbol_ptrセクションの情報を取得

イメージベースからのオフセットを計算して、アプリ自身の__la_symbol_ptrセクションの開始アドレスを取得

__la_symbol_ptrセクション内でラップ対象APIのアドレスを探索

ラップ対象APIのアドレスをラッパー関数のアドレスに書き換える

#define GetAddr(p, iOffset)   (void *)(((uint64_t)p) + iOffset)
void HookAPI(void *fnTarAPI, void *fnWrapper)
{
    bool bCtrl = false;
    const struct mach_header_64 *pMHApp = NULL;
    const struct section_64 *pSecLASymPtr = NULL;
    
    Dl_info dlInfo;
    if (dladdr(HookAPI, &dlInfo)) {
        //アプリ自身のイメージについて、情報取得に成功
        pMHApp = (struct mach_header_64 *)(dlInfo.dli_fbase);  //アプリ自身のイメージベースを取得
        //アプリ自身の__la_symbol_ptrセクションの情報を取得
        pSecLASymPtr = getsectbyname("__DATA", "__la_symbol_ptr");
        if (pSecLASymPtr) {
            bCtrl = true;
        } else {
            puts("__la_symbol_ptr section not found");
        }
    }
    
    if (bCtrl) {
        bCtrl = false;
        //アプリ自身の__TEXTセグメントの情報を取得
        const struct segment_command_64 *pSCTEXT = getsegbyname("__TEXT");
        
        //__la_symbol_ptrセクションの開始・終了アドレスを取得
        uint64_t iLASymPtrStart  = (uint64_t)GetAddr(pMHApp, pSecLASymPtr->addr - pSCTEXT->vmaddr);
        uint64_t iLASymPtrEnd    = iLASymPtrStart + pSecLASymPtr->size;
        
        //__la_symbol_ptrセクション内でラップ対象APIのアドレスを探索
        for (uint64_t *piEntry = (uint64_t *)iLASymPtrStart; (uint64_t)piEntry < iLASymPtrEnd; piEntry++) {
            if (*piEntry == (uint64_t)fnTarAPI) {
                //ラップ対象APIのアドレスを発見したので、ラッパー関数のアドレスに書き換える
                *piEntry = (uint64_t)fnWrapper;
                break;
            }
        }
    }
}

HookAPI実行後には、putsの代わりにPutsWrapperを呼び出すようになります。

方法1で作成したPutsLib.aをiOSアプリにリンクします。
main関数からHookAPIとputs, CheckAPI_StaticLibraryを呼び出すiOSアプリを作成して、動作確認を行います。
(Swiftの場合は適宜読み替えてください。)

int main(int argc, char * argv[])
{
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }

    // 動作確認
    HookAPI(puts, PutsWrapper);
    puts("Check: puts");
    CheckAPI_StaticLibrary("Check: CheckAPI_StaticLibrary (static library)");

    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

結果


PutsWrapper経由で標準Cライブラリのputsが呼ばれていることが分かります。

アプリ保護への応用

この方法も、アプリ保護に応用できます。
例えば、静的解析攻撃の有効性を低下させることが可能です。
以下のようにラッパー関数内で重要な処理を行うようにすれば、静的解析結果との乖離が大きくなるからです。

int PutsWrapper(const char *sz)
{
    int (*fn)(const char *) = dlsym(RTLD_NEXT, "puts"); //「本物の」putsのシンボルを解決
    if (fn) {
        ImportantFunc();    //重要な処理追加
        return fn(sz);
    }
    return EOF;
}

void Func()
{
    puts("Func");  //静的解析攻撃者の目線では単なるAPI呼び出しだが、実際には重要な処理を実行
    //:
}

3. まとめ

iOSアプリが使用しているAPIについて、そのラッパー関数が代わりに呼び出されるようにする方法を紹介しました。
各方法の概要は以下です。

■マクロ定義を利用して、ソースコードレベルで置換してしまう方法
 -静的ライブラリ内部の呼び出しは置換できない。

■対象と同じシンボルを作成する方法
 -内部でシンボルを解決してラップ対象を呼び出す。
 -使用しているAPIの隠蔽が可能。

■実行時にシンボル情報のセクションをパッチする方法
 -内部でシンボルを解決してラップ対象を呼び出す。
 -Minimum Deploymentsの値によって、パッチするセクションが異なる場合がある。
 -静的解析結果と異なる動作をするアプリが作成できる。


当サイトでは、アプリのセキュリティについて詳しくわかる資料をご用意しています。
アプリのセキュリティやクラッキング対策についてご興味を持たれた方は、
ぜひダウンロードしていただけますと幸いです。

ダウンロード資料一覧

また、弊社では
アプリへの不正な解析・改ざん行為(クラッキング)を防ぐ対策製品
「CrackProof」の開発・販売を行っております。
詳しく話を聞いてみたいと思われた方は、
当サイトからお気軽にお問い合わせください。

お問い合わせはこちら