Skip to main content

디스어셈블 프레임워크 Capstone-engine 활용하기

About 2 minPythonC++Article(s)blogmeetup.nhncloud.compythonpycppc++

디스어셈블 프레임워크 Capstone-engine 활용하기 관련

Python > Article(s)

Article(s)
C++ > Article(s)

Article(s)

디스어셈블 프레임워크 Capstone-engine 활용하기 | NHN Cloud Meetup
디스어셈블 프레임워크 Capstone-engine 활용하기
NHN Cloud_meetup 2024.03
NHN Cloud_meetup 2024.03

들어가며

이 글에서는 디스어셈블 프레임워크인 Capstone-engine을 활용하는 방법에 대해 알아보고자 합니다.

홈페이지 소개글(https://www.capstone-engine.org/open in new window)에 의하면 Capstone은 Capstone-engine을 바이너리 분석과 리버싱에 있어 the ultimate disassembly engine으로 만드는 게 목표라고 합니다. 여기에서는 Python API를 이용해 디스어셈블 도구를 만들어 보고, 애플리케이션 런타임 시 이를 디스어셈블하는 데 C API를 이용해 보겠습니다.


Python API 사용법

Capstone 의 Python API를 이용하면 나만의 파이썬 디스어셈블 도구를 만들 수 있습니다.

Capstone 설치(macOS)

pip install capstone # (for intel macOS)
pip install --pre --no-binary capstone capstone # (for m1, m2 macOS)

Windows 등 다른 플랫폼의 경우 https://www.capstone-engine.org/documentation.html을 참고하여 설치하면 되겠습니다.

기본 사용법

기본적인 사용법은 다음과 같습니다.

디스어셈블하고자 하는 헥스 코드를 적어 주고, 아키텍처 및 모드를 정한 뒤 disasm_lite() 메서드로 디스어셈블해 주면 됩니다.


런타임 사용법

애플리케이션 런타임 시에 디스어셈블이 필요한 경우가 있습니다. 이때 Capstone C API를 이용하여 프로그래밍할 수 있습니다. 여기에서는 Android 앱 런타임 시에 Capstone을 이용해 디스어셈블해 보고자 합니다.

C 프로그래밍 예시 코드는 https://www.capstone-engine.org/lang_c.htmlopen in new window에 나와 있는데요. 모바일에서 어떻게 사용하는지는 상세히 나와 있지 않습니다. 다행히 frida-gum(https://github.com/frida/frida-gum)에서 Capstone API를 제공해 주고 있습니다.

arm64용 frida-gum static library는 https://github.com/frida/frida/releases/download/16.1.11/frida-gum-devkit-16.1.11-android-arm64.tar.xzopen in new window에서 다운로드할 수 있습니다.

기본 사용법

Android 네이티브 프로젝트에서 기본적인 사용법은 다음과 같습니다. 디스어셈블하고자 하는 헥스 코드를 적어 주고, 아키텍처 및 모드를 정한 뒤 cs_disasm() 함수로 디스어셈블해 주면 됩니다.

다음은 전체 소스 코드입니다.

#include <jni.h>
#include <string>
#include <stdio.h>
#include <inttypes.h>
#include <android/log.h>
#include "include/frida-gum.h"

#ifndef LOG_TAG
#define LOG_TAG    "[nhncloud]"
#endif
#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

static const uint8_t CODE[] = {0xc8, 0x02, 0x40, 0xf9, 0x08, 0x01, 0x00, 0xb5, 0x08, 0x1c, 0xa0, 0x4e, 0xe0, 0x03, 0x15, 0xaa};

void disassemble() {
    csh capstone;
    cs_insn * insn;
    size_t count;

    cs_open (CS_ARCH_ARM64, CS_MODE_ARM, &capstone);
    cs_option (capstone, CS_OPT_DETAIL, CS_OPT_ON);
    insn = cs_malloc (capstone);
    count = cs_disasm(capstone, CODE, sizeof(CODE), 0x1000, 0, &insn);
    if (count > 0) {
        size_t j;
        for (j = 0; j < count; j++) {
            LOGD("0x%" PRIx64":\t%s\t\t%s\n", insn[j].address, insn[j].mnemonic,
                 insn[j].op_str);
        }
        cs_free(insn, count);
    } else {
        LOGD("ERROR: Failed to disassemble given code!\n");
    }

    cs_close (&capstone);
}

extern "C" JNIEXPORT jstring

JNICALL
Java_com_nhncloud_capstonetest_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";

    disassemble();

    return env->NewStringUTF(hello.c_str());
}

응용 사용법

특정 메모리 패턴을 스캔하고, 해당 패턴이 발견된 메모리 주소에서 Capstone API를 이용해 디스어셈블 후 그 결과값에 대해 추가 동작(예: 후킹, 메모리 패치 등)을 고려해 볼 수 있습니다. 이렇게 하면 타깃 함수의 offset이 매 빌드마다 달라져도 문제가 없습니다. 디스어셈블한 결과값에 대해 추가 동작을 수행하는 것이고, 보통 디스어셈블한 결과값은 소스 코드가 변하지 않는 한 동일한 형태를 보이기 때문이죠.

예를 들어 'libxxx.so'라는 라이브러리의 메모리에서 AA AA AA AA BB BB BB BB CC CC CC CC DD DD DD DD 패턴을 스캔합니다. 해당 패턴이 발견된 주소에서 0xc만큼 떨어진 위치를 디스어셈블하면 항상 어떤 함수로 branch하는 b #0xabcdef입니다. 0xabcdef 함수는 불필요한 기능이므로 메모리 패치를 통해 작동하지 않도록 하고 싶습니다. 다음은 이에 대한 예시 코드입니다.

#include <jni.h>
#include <string>
#include <stdio.h>
#include <inttypes.h>
#include <android/log.h>
#include "include/frida-gum.h"

#ifndef LOG_TAG
#define LOG_TAG    "[nhncloud]"
#endif
#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

// disassemble using capstone api
static void
disassemble (const guint8* code, gsize size, guint64 address, std::string& mnemonic, std::string& op_str)
{
    csh capstone;
    cs_insn * insn;

#ifdef __aarch64__
    cs_open (CS_ARCH_ARM64, CS_MODE_ARM, &capstone);
#elif __arm__
    cs_open (CS_ARCH_ARM, static_cast<cs_mode>(CS_MODE_ARM | CS_MODE_THUMB), &capstone);
#endif
    cs_option (capstone, CS_OPT_DETAIL, CS_OPT_ON);

    insn = cs_malloc (capstone);

    while (cs_disasm_iter (capstone, &code, &size, &address, insn))
    {
        op_str = std::string(insn->op_str);
        mnemonic = std::string(insn->mnemonic);
    }

    cs_free (insn, 1);
    cs_close (&capstone);
}

int mem_scan_match_count = 0;
static gboolean
mem_scan_match_func (GumAddress address, gsize size, gpointer user_data)
{
    mem_scan_match_count++;
    if (mem_scan_match_count == 1)
    {
        unsigned char* data;
        gsize n_bytes_read;

        LOGD("match at %p", (void*)address);

        data = gum_memory_read(reinterpret_cast<gconstpointer>(address + 0xc), 4, &n_bytes_read);
        std::string op_str, mnemonic;
        disassemble(data, n_bytes_read, (address + 0xc), mnemonic, op_str);

        // op_str is "#0xabcdef"
        size_t pos = op_str.find("0x");
        std::string hex_str = op_str.substr(pos + 2);
        uint64_t value = std::stoull(hex_str, <span class="hljs-literal">nullptr, 16);

        // memory patch
        const guint8 patch[] = {0xc0, 0x03, 0x5f, 0xd6};
        gum_memory_write((void*)value, patch, 4);

        return TRUE;
    }
    else
    {
        return FALSE;
    }
}

void mem_scan_with_pattern(const GumModuleDetails* m, const char* mem_scan_pattern)
{
    GumMatchPattern* mem_pattern = gum_match_pattern_new_from_string(mem_scan_pattern);
    gum_try_mprotect(reinterpret_cast<gpointer>(m->range->base_address), m->range->size, GUM_PAGE_RWX);
    gum_memory_scan(m->range, mem_pattern, mem_scan_match_func, <span class="hljs-literal">NULL);
}

__attribute__((constructor))
void native_init(void)
{
    gum_init_embedded ();
    LOGD("frida-gum initialized!");

    uint64_t libxxx_base = gum_module_find_base_address("libxxx.so");
    const GumModuleDetails* m = gum_module_map_find(gum_module_map_new(), libxxx_base);

    // scan memory pattern
    const char* mem_scan_pattern = "AA AA AA AA BB BB BB BB CC CC CC CC DD DD DD DD";
    mem_scan_with_pattern(m, mem_scan_pattern);
}

나가며

이 글에서는 디스어셈블 도구를 만들거나 애플리케이션 런타임 시 디스어셈블하는 데 Capstone-engine을 활용해 보는 예를 다뤘습니다. 직접 사용해 보니 API도 간단하고 직관적이어서 매우 유용해 보였습니다. 앞서 소개해 드린 공식 홈페이지에서도 다양한 자료를 제공하고 있기 때문에 어려움 없이 사용해 보실 수 있을 거라 생각됩니다. 긴 글을 읽어 주셔서 감사합니다. 🙂

참고 문헌

The Ultimate Disassembly Framework – Capstone – The Ultimate Disassembler

The Ultimate Disassembler

이찬희 (MarkiiimarK)
Never Stop Learning.