[AOS] How to Detect libc.so Hooking

koo00·2022년 9월 19일
0

libc 라이브러리 후킹 탐지 방법에 대한 포스팅이다.
아래 코드를 응용하면 libc 라이브러리뿐만 아니라 다른 라이브러리에 대해서도 후킹 탐지가 가능하다.
상용 보안 솔루션에서 사용하는 방식도 비슷하지 않을까 싶다.
물론 함수 인라인 처리 + 난독화 = 점심 나가서 먹겠지만..


JNI_OnLoad 로 구현하기 귀찮으니까 대충 예제 앱 만들어서 기본 제공 함수인 stringFromJNI 함수 코드 안에 DetectHook 함수를 넣어줬다.

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_antilibchookexample_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    DetectHook();
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}


DetectHook 함수는 아래와 같이 작성했다.
1. 현재 메모리를 읽어 libc.so 경로를 저장
2. 방금 저장한 libc.so 경로를 읽어 .text.plt 섹션의 offsetsize 저장
3. .text.plt 섹션의 checksum 계산 후 저장
4. Loop 쓰레드 실행

void DetectHook() {
    int fd = 0;
    char buf[MAX];
    // Set LIBC_PATH from PROC_MAPS
    fd = open(PROC_MAPS, O_RDONLY);
    if (fd != 0) {
        while((read_one_line(fd, buf, MAX)) > 0) {
            if (strstr(buf, LIBC) != NULL) {
                char path[MAX] = "";
                char tmp[MAX] = "";
                char pem[5] = "";
                sscanf(buf, "%s %s %s %s %s %s", tmp, pem, tmp, tmp, tmp, path);
                size_t size = strlen(path) + 1;
                LIBC_PATH = (char*) malloc(size);
                strncpy(LIBC_PATH, path, size);
                LOGI("%s", buf);
                LOGI("---------------------------------------");
                break;
            } else {
                LOGI("LIBC_PATH Null !");
            }
        }
        close(fd);
    }
    LOGI("LIBC_PATH : %s", LIBC_PATH);
    LOGI("---------------------------------------");

    // Set Section Header from LIBC_PATH
    Elf_Ehdr ehdr;
    Elf_Shdr shdr;
    fd = open(LIBC_PATH, O_RDONLY);
    if (fd != 0) {
        read(fd, &ehdr, sizeof(Elf_Ehdr));
        lseek(fd, ehdr.e_shoff, SEEK_SET);

        for (int i = 0; i < ehdr.e_shnum; i++) {
            memset(&shdr, 0, sizeof(Elf_Shdr));
            read(fd, &shdr, sizeof(Elf_Shdr));

            // .text & .plt Section
            if (shdr.sh_flags & SHF_EXECINSTR) {
                OFFSET[COUNT] = shdr.sh_offset;
                SIZE[COUNT] = shdr.sh_size;
                LOGI("OFFSET[%d] : %lx", COUNT, OFFSET[COUNT]);
                LOGI("SIZE[%d] : %lx", COUNT, SIZE[COUNT]);
                LOGI("---------------------------------------");
                COUNT++;

                if (COUNT == 2) {
                    break;
                }
            }
        }
        // Set CHECKSUM from .text & .plt Section
        for (int i = 0; i < COUNT; i++) {
            lseek(fd, OFFSET[i], SEEK_SET);
            // uint8_t* buffer = (uint8_t*) malloc(size[i] * sizeof(uint8_t));
            unsigned char* buffer = (unsigned char*) malloc(SIZE[i]);
            read(fd, buffer, SIZE[i]);
            LOGI("buffer1 : %lx", buffer);
            LOGI("SIZE[i] : %lx", SIZE[i]);
            CHECKSUM[i] = checksum(buffer, SIZE[i]);
            LOGI("CHECKSUM[%d] : %lx", i, checksum(buffer, SIZE[i]));
            LOGI("---------------------------------------");
            free(buffer);
        }

        close(fd);
    }

    // Start Loop
    pthread_t t;
    pthread_create(&t, NULL, Loop, NULL);
}


checksum 함수는 인자로 받은 버퍼의 0번째 문자열부터 unsigned long 자료형으로 바꿔 버퍼의 크기만큼 계속해서 더한 값을 리턴하는 함수이다.
이때 always_inline 속성을 줘서 인라인 함수로 지정해줬다.

__attribute__((always_inline))
static inline unsigned long checksum(void* buffer, size_t len) {
    unsigned long seed = 0;
    unsigned char* buf = (unsigned char*) buffer;
    for (size_t i = 0; i < len; i++) {
        seed += (unsigned long) (*buf++);
    }
    return seed;
}


이렇게 되면 checksum 함수를 호출하는 부분에서 checksum 함수의 코드가 그대로 삽입되어 함수인지 파악하기 어렵고 해당 함수를 주소 값으로 후킹할 수 없다.

Loop 함수는 간단하다. 10초마다 현재 메모리를 열어 libc.so 문자열이 존재하면 Scan 함수를 실행시킨다.

void* Loop(void* pargs) {
    LOGI("Start Loop");
    while (1) {
        int fd = 0;
        char buf[MAX];

        fd = open(PROC_MAPS, O_RDONLY);
        if (fd != 0) {
            while (read_one_line(fd, buf, MAX) > 0) {
                if (strstr(buf, LIBC) != NULL) {
                    if (true == Scan(buf)) {
                        break;
                    }
                }
            }
            close(fd);
        }
        sleep(10);
    }
}


Scan 함수는 메모리에 로드되어 있는 libc.so 의 시작 주소에 앞서 저장해둔 .text.plt 섹션의 offset 을 더해 checksum 을 구한 뒤 저장해둔 CHECKSUM 배열의 값과 비교하는 함수이다.

static bool Scan(char* map) {
    unsigned long start, end;
    char path[MAX] = "";
    char tmp[MAX] = "";
    char pem[5] = "";

    sscanf(map, "%llx-%llx %s %s %s %s %s", &start, &end, pem, tmp, tmp, tmp, path);
    // LOGI("%s", map);
    // LOGI("START : %lx", START);
    if (pem[2] == 'x') {
        if (pem[0] == 'r') {
            unsigned char* buffer = NULL;

            buffer = (unsigned char*) start;
            for (int i = 0; i < COUNT; i++) {
                // LOGI("start : %lx", start);
                // LOGI("end : %lx", end);
                // LOGI("-------------------------------");
                if (start + OFFSET[i] + SIZE[i] > end) {
                    if (START != 0) {
                        buffer = (unsigned char*) START;
                        START = 0;
                        break;
                    }
                }
            }

            for (int i = 0; i < COUNT; i++) {
                unsigned long output = checksum(buffer + OFFSET[i], SIZE[i]);
                LOGI("output : %lx", output);
                if (output != CHECKSUM[i]) {
                    LOGI("Detect Hook !");
                    // LOGI("-------------------------------");
                }
            }

        } else {
            // Impossible
        }
        return true;
    } else {
        // Set START Pointer of LIBC
        if (pem[0] == 'r') {
            START = start;
        }
    }
    return false;
}

전체 소스 코드는 다음과 같다.

#include <jni.h>
#include <string>
#include <android/log.h>
#include <elf.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>

void DetectHook();
void* Loop(void* pargs);
static inline ssize_t read_one_line(int fd, char* buf, unsigned int max_len);
static inline unsigned long checksum(void* buffer, size_t len);
static bool Scan(char* map);

#define MAX 1024
#define TAG "KOOOO"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)

// #ifdef _32_BIT
// typedef Elf32_Ehdr Elf_Ehdr;
// typedef Elf32_Shdr Elf_Shdr;
// #elif _64_BIT
typedef Elf64_Ehdr Elf_Ehdr;
typedef Elf64_Shdr Elf_Shdr;
// #endif

static const char* PROC_MAPS = "/proc/self/maps";
static const char* LIBC = "/libc.so";
static char* LIBC_PATH = NULL;
static int COUNT = 0;
static unsigned long OFFSET[2] = {};
static unsigned long SIZE[2] = {};
static unsigned long CHECKSUM[2] = {};
static unsigned long START = 0;

void DetectHook() {
    int fd = 0;
    char buf[MAX];
    // Set LIBC_PATH from PROC_MAPS
    fd = open(PROC_MAPS, O_RDONLY);
    if (fd != 0) {
        while((read_one_line(fd, buf, MAX)) > 0) {
            if (strstr(buf, LIBC) != NULL) {
                char path[MAX] = "";
                char tmp[MAX] = "";
                char pem[5] = "";
                sscanf(buf, "%s %s %s %s %s %s", tmp, pem, tmp, tmp, tmp, path);
                size_t size = strlen(path) + 1;
                LIBC_PATH = (char*) malloc(size);
                strncpy(LIBC_PATH, path, size);
                LOGI("%s", buf);
                LOGI("---------------------------------------");
                break;
            } else {
                LOGI("LIBC_PATH Null !");
            }
        }
        close(fd);
    }
    LOGI("LIBC_PATH : %s", LIBC_PATH);
    LOGI("---------------------------------------");

    // Set Section Header from LIBC_PATH
    Elf_Ehdr ehdr;
    Elf_Shdr shdr;
    fd = open(LIBC_PATH, O_RDONLY);
    if (fd != 0) {
        read(fd, &ehdr, sizeof(Elf_Ehdr));
        lseek(fd, ehdr.e_shoff, SEEK_SET);
        /*
        LOGI("sizeof(Elf_Ehdr) : %d", sizeof(Elf_Ehdr));
        LOGI("ELF Header : %s", ehdr);
        LOGI("ELF Header Entry : %lx", ehdr.e_entry);
        LOGI("ELF Header Program Header Table Offset : %lx", ehdr.e_phoff);
        LOGI("ELF Header Section Header Table Offset : %lx", ehdr.e_shoff);
        LOGI("ELF Header Size : %lx", ehdr.e_ehsize);
        LOGI("ELF Header Program Header Table Size : %d", ehdr.e_phentsize);
        LOGI("ELF Header Program Header Table Number : %d", ehdr.e_phnum);
        LOGI("ELF Header Section Header Table Size : %lx", ehdr.e_shentsize);
        LOGI("ELF Header Section Header Table Number : %d", ehdr.e_shnum);
        */

        for (int i = 0; i < ehdr.e_shnum; i++) {
            memset(&shdr, 0, sizeof(Elf_Shdr));
            read(fd, &shdr, sizeof(Elf_Shdr));

            // .text & .plt Section
            if (shdr.sh_flags & SHF_EXECINSTR) {
                /*
                LOGI("Section Header : %s", shdr);
                LOGI("Section Header Name : %d", shdr.sh_name);
                LOGI("Section Header Address : %lx", shdr.sh_addr);
                LOGI("Section Header Offset : %lx", shdr.sh_offset);
                LOGI("Section Header Size : %lx", shdr.sh_size);
                */
                OFFSET[COUNT] = shdr.sh_offset;
                SIZE[COUNT] = shdr.sh_size;
                LOGI("OFFSET[%d] : %lx", COUNT, OFFSET[COUNT]);
                LOGI("SIZE[%d] : %lx", COUNT, SIZE[COUNT]);
                LOGI("---------------------------------------");
                COUNT++;

                if (COUNT == 2) {
                    break;
                }
            }
        }
        // Set CHECKSUM from .text & .plt Section
        for (int i = 0; i < COUNT; i++) {
            lseek(fd, OFFSET[i], SEEK_SET);
            // uint8_t* buffer = (uint8_t*) malloc(size[i] * sizeof(uint8_t));
            unsigned char* buffer = (unsigned char*) malloc(SIZE[i]);
            read(fd, buffer, SIZE[i]);
            LOGI("buffer1 : %lx", buffer);
            LOGI("SIZE[i] : %lx", SIZE[i]);
            CHECKSUM[i] = checksum(buffer, SIZE[i]);
            LOGI("CHECKSUM[%d] : %lx", i, checksum(buffer, SIZE[i]));
            LOGI("---------------------------------------");
            free(buffer);
        }

        close(fd);
    }

    // Start Loop
    pthread_t t;
    pthread_create(&t, NULL, Loop, NULL);
}

void* Loop(void* pargs) {
    LOGI("Start Loop");
    while (1) {
        int fd = 0;
        char buf[MAX];

        fd = open(PROC_MAPS, O_RDONLY);
        if (fd != 0) {
            while (read_one_line(fd, buf, MAX) > 0) {
                if (strstr(buf, LIBC) != NULL) {
                    if (true == Scan(buf)) {
                        break;
                    }
                }
            }
            close(fd);
        }
        sleep(10);
    }
}

static bool Scan(char* map) {
    unsigned long start, end;
    char path[MAX] = "";
    char tmp[MAX] = "";
    char pem[5] = "";

    sscanf(map, "%llx-%llx %s %s %s %s %s", &start, &end, pem, tmp, tmp, tmp, path);
    // LOGI("%s", map);
    // LOGI("START : %lx", START);
    if (pem[2] == 'x') {
        if (pem[0] == 'r') {
            unsigned char* buffer = NULL;

            buffer = (unsigned char*) start;
            for (int i = 0; i < COUNT; i++) {
                // LOGI("start : %lx", start);
                // LOGI("end : %lx", end);
                // LOGI("-------------------------------");
                if (start + OFFSET[i] + SIZE[i] > end) {
                    if (START != 0) {
                        buffer = (unsigned char*) START;
                        START = 0;
                        break;
                    }
                }
            }

            for (int i = 0; i < COUNT; i++) {
                unsigned long output = checksum(buffer + OFFSET[i], SIZE[i]);
                LOGI("output : %lx", output);
                if (output != CHECKSUM[i]) {
                    LOGI("Detect Hook !");
                    // LOGI("-------------------------------");
                }
            }

        } else {
            // Impossible
        }
        return true;
    } else {
        // Set START Pointer of LIBC
        if (pem[0] == 'r') {
            START = start;
        }
    }
    return false;
}

__attribute__((always_inline))
static inline unsigned long checksum(void* buffer, size_t len) {
    unsigned long seed = 0;
    unsigned char* buf = (unsigned char*) buffer;
    for (size_t i = 0; i < len; i++) {
        seed += (unsigned long) (*buf++);
    }
    return seed;
}

__attribute__((always_inline))
static inline ssize_t read_one_line(int fd, char* buf, unsigned int max_len) {
    char b;
    ssize_t ret;
    ssize_t bytes_read = 0;
    memset(buf, 0, max_len);

    do {
        ret = read(fd, &b, 1);

        if (ret != 1) {
            if (bytes_read == 0) {
                return -1;
            } else {
                return bytes_read;
            }
        }

        if (b == '\n') {
            return bytes_read;
        }

        *(buf++) = b;
        bytes_read += 1;
    } while (bytes_read < max_len -1);

    return bytes_read;
}


extern "C" JNIEXPORT jstring JNICALL
Java_com_example_antilibchookexample_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    DetectHook();
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

직접 테스트해보고 주석 처리해둔 부분을 해제하여 눈으로 확인하면 좀 더 이해가 갈 것이다. 그리고 IDA 에서 확인해서 눈에 익혀둔다면 다음에 비슷한 유형을 만났을 때 알아차리지 않을까 생각한다.

profile
JFDI !

0개의 댓글