Skip to main content

PLT Hook 체크를 위한 Android so 파일 파싱

About 5 minAndroidJavaScriptDevOpsArticle(s)blogmeetup.nhncloud.comjsandroidso

PLT Hook 체크를 위한 Android so 파일 파싱 관련

Andriod > Article(s)

Article(s)
JavaScript > Article(s)

Article(s)

PLT Hook 체크를 위한 Android so 파일 파싱 | NHN Cloud Meetup
PLT Hook 체크를 위한 Android so 파일 파싱
[NHN클라우드] Meetup!_PLT Hook 체크를 위한 Android so 파일
[NHN클라우드] Meetup!_PLT Hook 체크를 위한 Android so 파일

들어가며

Android PLT(procedure linkage table) hook과 관련해 plt hook 당한 함수를 식별하는 방법을 공유하고자 합니다. plt hook 라이브러리로는 bytedance에서 오픈소스로 공개한 bytedance/bhookopen in new window이 많이 사용되고 있는 것으로 보입니다.

실제로 사용해 보니 쉽고, 안정적으로 후킹이 가능합니다. 다음 사진은 fopen 함수를 plt hook하여 로그를 찍은 것입니다.

제가 쉽게 할 수 있다는 건 남들도, 특히 해커들도 쉽게 할 수 있다는 거겠죠? 이러한 후킹은 어뷰징으로 이어질 수 있기 때문에 분석하는 입장에서는 plt hook 당한 함수를 찾아낼 수 있어야겠죠.


PLT hook 하면 무슨 일이 발생하는가?

so 파일은 ELF 파일 포맷을 가지고 있습니다. ELF 파일은 메모리에 로드되면 .rela.plt 섹션에 정의되어 있는 함수(심볼)의 주소값을 .got.plt 섹션에 저장합니다. 그 후 .got.plt 섹션에 저장되어 있는 주소를 참조하여 해당 함수를 호출하게 됩니다.

다음은 libart.so 파일을 로컬에서 elf 파싱(horsicq/XELFVieweropen in new window) 프로그램으로 열어 본 것입니다. .rela.plt 섹션에 0x212 라는 심볼이 정의되어 있고, r_offset 값이 0x6a6480입니다. r_offsetlibart.so 파일이 메모리에 로드되면 그 base 주소로부터 0x6a6480 위치만큼 떨어진 곳에 0x212 심볼의 주소값을 저장하겠다는 뜻입니다.

0x212 심볼은 무엇을 의미할까요? .dynsym 섹션에서 찾을 수 있습니다. 0x212는 십진수로 530입니다. 530은 .dynsym 섹션의 인덱스입니다. 즉, .dynsym 섹션에서 530번째 저장되어 있는 심볼인 것이죠. fopen이네요.

정리하면 "fopen 함수는 .rela.plt 섹션에 정의되어 있고, libart.so 파일이 메모리에 로드되면 base 주소로부터 0x6a6480 위치 떨어진 곳에 fopen 함수의 주소를 저장한다. 그곳의 위치는 .got.plt 섹션에 있다"가 되겠습니다.

만약 fopen 함수가 plt hook 당하면 .got.plt 섹션에 저장되는 fopen 함수의 주소가 다른 곳(악성 so 파일의 함수 주소)으로 바뀌게 될 것입니다.


PLT hook 당한 함수를 어떻게 식별하는가?

런타임 시에 특정 함수가 plt hook을 당하면 .got.plt 섹션에 저장되는 주소값이 바꿔치기 당할 것입니다. .got.plt 섹션에 주소값을 저장하는 심볼들은 .rela.plt 섹션에 정의되어 있습니다. .rela.plt 섹션에는 어떤 심볼 및 그 심볼의 주소값이 어느 위치에 저장되는지 나와 있습니다.

그러니, .rela.plt 섹션을 파싱해서 어떤 심볼의 주소값이 악성 so 파일에 위치하고 있으면 plt hook 당한 것으로 판단할 수 있습니다.

.rela.plt 섹션을 어떻게 파싱할까요? 그냥 open 함수로 so 파일 열어서 파싱하면 되는 것 아닐까요? 그러나 문제가 있습니다. android:extractNativeLibs="false" 옵션으로 빌드한 apk 파일의 경우 so 파일을 apk 파일에서 추출하지 않고 로드하기 때문에 open 함수로 열어볼 수가 없습니다.

다음은 어느 게임 앱의 메모리에 로드된 libil2cpp.so 파일 정보인데요. 그 경로가 split_config_arm64_v8a.apk 파일 안에 있는 것을 알 수 있습니다(! 표시가 apk 파일 안에 있는 경로를 의미함).

흠...그래도 문제없죠. 어차피 so 파일은 메모리에 로드되는 것이니 메모리에서 직접 .rela.plt 섹션을 파싱하면 될 테니까요. 그러면 .rela.plt 섹션이 메모리의 어디에 위치하고 있는지 알아내면 되겠습니다. .rela.plt 섹션의 위치는 Section Header라는 곳에 정의되어 있습니다. 그 Section Header의 위치는 Elf Header(Elf_Ehdr)에 정의되어 있습니다. 로컬에서 libart.so 파일의 Elf Header 중 e_shoff 값이 0x836a98입니다. 그곳이 Section Header의 위치입니다.

자, 이제 Section Header 위치로 직접 가서 메모리를 관찰해 봐야겠습니다.

아뿔싸! Memory access violation이 발생하면서 접근이 안 됩니다...무슨 문제일까요?

런타임 시에 Seciton Header도 메모리에 로드되는 것으로 알고 접근하였는데, 찾아보니 Section Header는 메모리에 로드되지 않는다고 합니다(출처: https://stackoverflow.com/a/44854052open in new window).

난감하네요. Section Header 에 .rela.plt 등 섹션 위치가 정의되어 있는데, 이게 메모리에 로드되지 않는다면 어떻게 위치를 알아내고 파싱을 할 수 있을까요? 다행히 Program Header는 메모리에 로드가 됩니다.

이 Program Header에는  섹션의 위치 등 정보가 담겨 있습니다.
이 Program Header에는 .dynamic 섹션의 위치 등 정보가 담겨 있습니다.
그리고 이  섹션에는 파싱에 필요한 다른 섹션들의 위치 정보가 담겨 있습니다.
그리고 이 .dynamic 섹션에는 파싱에 필요한 다른 섹션들의 위치 정보가 담겨 있습니다.

이제, 섹션들의 메모리상의 위치 정보를 확보했으니 파싱만 하면 되겠군요.

다음은 어느 게임 앱의 fopen 함수를 plt hook 한 뒤, libil2cpp.so 파일의 .rela.plt 섹션을 메모리상에서 파싱한 결과입니다. 음...fopen 함수의 주소가 0x72079708e8인데, 그 위치가 libplthooktest.so 파일이네요. 원래라면 위치가 libc.so가 되어야 할 것입니다. 따라서 fopen 함수가 plt hook 당한 것으로 판단할 수 있습니다.

파싱에 사용된 frida 스크립트 공유합니다.

var magic = "464c457f"

function parseElf(base) {
    base = ptr(base)
    // Read elf header
    var elf_magic = base.readU32()
    if (parseInt(elf_magic).toString(16) != magic) {
        console.log("Wrong magic")
    }

    var arch = Process.arch
    var is32bit = arch == "arm" ? 1 : 0 // 1:32 0:64

    var size_of_Elf32_Ehdr = 0x34;
    var off_of_Elf32_Ehdr_phoff = 28;
    var off_of_Elf32_Ehdr_shoff = 32;
    var off_of_Elf32_Ehdr_phentsize = 42;
    var off_of_Elf32_Ehdr_phnum = 44;
    var off_of_Elf32_Ehdr_shentsize = 46;
    var off_of_Elf32_Ehdr_shnum = 48;
    var off_of_Elf32_Ehdr_shstrndx = 50;

    var size_of_Elf64_Ehdr = 0x40;
    var off_of_Elf64_Ehdr_phoff = 32;
    var off_of_Elf64_Ehdr_shoff = 40;
    var off_of_Elf64_Ehdr_phentsize = 54;
    var off_of_Elf64_Ehdr_phnum = 56;
    var off_of_Elf64_Ehdr_shentsize = 58;
    var off_of_Elf64_Ehdr_shnum = 60;
    var off_of_Elf64_Ehdr_shstrndx = 62;

    var got_plt_secition_addr = null;
    var dynamic_section_addr = null;
    var dynstr_section_addr = null;
    var dynsym_section_addr = null;
    var rela_plt_section_addr = null;

    // Parse Ehdr(Elf header)
    var phoff = is32bit ? size_of_Elf32_Ehdr : size_of_Elf64_Ehdr   // Program header table file offset
    var shoff = is32bit ? base.add(off_of_Elf32_Ehdr_shoff).readU32() : base.add(off_of_Elf64_Ehdr_shoff).readU64();   // Section header table file offset
    var phentsize = is32bit ? base.add(off_of_Elf32_Ehdr_phentsize).readU16() : base.add(off_of_Elf64_Ehdr_phentsize).readU16();    // Size of entries in the program header table
    if (is32bit && phentsize != 32) {  // 0x20
        console.log("[*] Wrong e_phentsize. Should be 32. Let's assume it's 32");
        phentsize = 32;
    } else if (!is32bit && phentsize != 56) {
        console.log("Wrong e_phentsize. Should be 56. Let's assume it's 56");
        phentsize = 56;
    }
    var phnum = is32bit ? base.add(off_of_Elf32_Ehdr_phnum).readU16() : base.add(off_of_Elf64_Ehdr_phnum).readU16();    // Number of entries in program header table
    if (phnum == 0) {
        console.log("phnum is 0. Let's assume it's 10. because we just need to find .dynamic section")
        phnum = 10;
    }
    var shentsize = is32bit ? base.add(off_of_Elf32_Ehdr_shentsize).readU16() : base.add(off_of_Elf64_Ehdr_shentsize).readU16();    // Size of the section header
    if (is32bit && shentsize != 40) {  // 0x28
        console.log("Wrong e_shentsize. Should be 40");
    } else if (!is32bit && shentsize != 64) {
        console.log("Wrong e_shentsize. Should be 64");
    }
    var shnum = is32bit ? base.add(off_of_Elf32_Ehdr_shnum).readU16() : base.add(off_of_Elf64_Ehdr_shnum).readU16();    // Number of entries in section header table
    var shstrndx = is32bit ? base.add(off_of_Elf32_Ehdr_shstrndx).readU16() : base.add(off_of_Elf64_Ehdr_shstrndx).readU16();  // Section header table index of the entry associated with the section name string table
    // console.log(`phoff: ${phoff}, shoff: ${shoff}, phentsize: ${phentsize}, phnum: ${phnum}, shentsize: ${shentsize}, shnum: ${shnum}, shstrndx: ${shstrndx}`)

    // Parse Phdr(Program header)
    var phdrs = base.add(phoff)
    for (var i = 0; i < phnum; i++) {
        var phdr = phdrs.add(i * phentsize);
        var p_type = phdr.readU32()
        var p_offset = is32bit ? phdr.add(0x4).readU32() : phdr.add(0x8).readU64();
        var p_vaddr = is32bit ? phdr.add(0x8).readU32() : phdr.add(0x10).readU64();
        var p_paddr = is32bit ? phdr.add(0xc).readU32() : phdr.add(0x18).readU64();
        var p_filesz = is32bit ? phdr.add(0x10).readU32() : phdr.add(0x20).readU64();
        var p_memsz = is32bit ? phdr.add(0x14).readU32() : phdr.add(0x28).readU64();
        var p_flags = is32bit ? phdr.add(0x18).readU32() : phdr.add(0x4).readU32();
        var p_align = is32bit ? phdr.add(0x1c).readU32() : phdr.add(0x30).readU64();
        // console.log(`p_type: ${p_type}, p_offset: ${p_offset}, p_vaddr: ${p_vaddr}, p_paddr: ${p_paddr}, p_filesz: ${p_filesz}, p_memsz: ${p_memsz}, p_flags: ${p_flags}, p_align: ${p_align}`);

        if (p_type == 0x2) {
            // .dynamic
            dynamic_section_addr = base.add(p_vaddr);
            var dynamic_section_indices = parseInt(p_memsz) / parseInt(p_align) * 2
            var dynamic_section_entsize = p_align * 2
            for (var i = 0; i < dynamic_section_indices; i++) {
                var d_tag = is32bit ? dynamic_section_addr.add(i * dynamic_section_entsize).readU32() : dynamic_section_addr.add(i * dynamic_section_entsize).readU64()
                if (d_tag == 0) break;
                var d_value = is32bit ? dynamic_section_addr.add(i * dynamic_section_entsize + 4).readU32() : dynamic_section_addr.add(i * dynamic_section_entsize + 8).readU64()

                if (d_tag == 0x3) {
                    // .got.plt
                    got_plt_secition_addr = base.add(d_value);
                } else if (d_tag == 0x5) {
                    // .dynstr
                    dynstr_section_addr = base.add(d_value);
                } else if (d_tag == 0x6) {
                    // .dynsym
                    dynsym_section_addr = base.add(d_value);
                } else if (d_tag == 0x17) {
                    // .rela.plt
                    rela_plt_section_addr = base.add(d_value);
                }
            }
        }
    }

    // Parse .dynsym
    var dynsym_section_entsize = is32bit ? 0x10 : 0x18;
    var dynsyms = {};
    var st_infos = [
        0x00,   // LOCAL NOTYPE
        0x03,   // LOCAL SECTION
        0x10,   // GLOBAL NOTYPE
        0x11,   // GLOBAL OBJECT
        0x12,   // GLOBAL FUNC
        0x1a,   // GLOBAL LOOS
        0x20,   // WEAK NOTYPE
        0x21,   // WEAK OBJECT
        0x22,   // WEAK FUNC
    ]
    var st_others = [
        0x0,        /* STV_DEFAULT. Default symbol visibility rules */
        0x1,        /* STV_INTERNAL. Processor specific hidden class */
        0x2,        /* STV_HIDDEN. Sym unavailable in other modules */
        0x3,        /* STV_PROTECTED. Not preemptible, not exported */
    ]
    for (var i = 0, id = 0;;i += dynsym_section_entsize, id++) {
        var dynsym_section_entaddr = dynsym_section_addr.add(i)
        var st_name = is32bit ? dynsym_section_entaddr.readU32() : dynsym_section_entaddr.readU32();
        var st_value = is32bit ? dynsym_section_entaddr.add(0x4).readU32() : dynsym_section_entaddr.add(0x8).readU64();
        var st_size = is32bit ? dynsym_section_entaddr.add(0x8).readU32() : dynsym_section_entaddr.add(0x10).readU64();
        var st_info = is32bit ? dynsym_section_entaddr.add(0xc).readU8() : dynsym_section_entaddr.add(0x4).readU8();
        if (!st_infos.includes(st_info)) {
            console.log(`st_info: ${st_info} is not a valid`)
            break;
        }
        var st_other = is32bit ? dynsym_section_entaddr.add(0xd).readU8() : dynsym_section_entaddr.add(0x5).readU8();
        if (!st_others.includes(st_other)) {
            console.log(`st_ohter: ${st_other} is not a valid`)
            break;
        }
        var st_shndx = is32bit ? dynsym_section_entaddr.add(0xe).readU16() : dynsym_section_entaddr.add(0x6).readU16();
        try {
            var symbol_name = dynstr_section_addr.add(st_name).readUtf8String();
        } catch (error) {
            break;
        }
        dynsyms[id] = {
            "symbol_name": symbol_name, 
            "st_value": st_value, 
            "st_size": st_size, 
            "st_info": st_info, 
            "st_other": st_other, 
            "st_shndx": st_shndx
        }
        // console.log(`${id}. st_name: ${st_name} --> ${symbol_name}, st_value: ${st_value}, st_size: ${st_size}, st_info: ${st_info}, st_other: ${st_other}, st_shndx: ${st_shndx}`)
    }

    // Parse .rela.plt
    var rela_plt_section_entsize = is32bit ? 0x8 : 0x18
    var R_ARM_JUMP_SLOT = 0x16  /* Create PLT entry */
    var R_AARCH64_JUMP_SLOT = 0x402
    for (var i = 0, id = 0;;i += rela_plt_section_entsize, id++) {
        var rela_plt_section_entaddr = rela_plt_section_addr.add(i);
        var r_offset = is32bit ? rela_plt_section_entaddr.readU32() : rela_plt_section_entaddr.readU64();
        var r_info_addr = is32bit ? rela_plt_section_entaddr.add(0x4) : rela_plt_section_entaddr.add(0x8);
        var reloc_type = is32bit ? r_info_addr.readU8() : r_info_addr.readU32();
        if ((is32bit && reloc_type != R_ARM_JUMP_SLOT) || (!is32bit && reloc_type != R_AARCH64_JUMP_SLOT)) {
            break;
        }

        var sym_index = is32bit ? r_info_addr.readU32() > 8 : r_info_addr.add(0x4).readU32();
        var symptr_in_got_plt = base.add(r_offset).readPointer();
        var r_addend = rela_plt_section_entaddr.add(0x10).readU64();
        var location = Process.findModuleByAddress(symptr_in_got_plt) === null ? 'None' : Process.findModuleByAddress(symptr_in_got_plt).name
        console.log(`${id}. symbol: ${dynsyms[sym_index]["symbol_name"]} --> addr: ${symptr_in_got_plt}(${location})`)
    }
}

parseElf(Module.findBaseAddress("libil2cpp.so"))

긴 글을 읽어 주셔서 감사합니다.


참고 문헌

elf.h source code [glibc/elf/elf.h] - Codebrowser

Browse the source of glibc glibc-2 using KDAB Codebrowser which provides IDE like features for browsing C, C++, Rust & Dart code in your browser
bytedance/bhook

🔥 ByteHook is an Android PLT hook library which supports armeabi-v7a, arm64-v8a, x86 and x86_64.
personal_script/Frida_script/android_hook_detect.js at master · lich4/personal_script · GitHub

Scripts useful for Vulnerability Exploit, Static Analysis, Dynamic Analysis, File Format Exploit, ...
horsicq/XELFViewer

ELF file viewer/editor for Windows, Linux and MacOS.
linux - Reading ELF header of loaded shared object during runtime - Stack Overflow

I wrote some code to search for a symbol in a shared library's ELF header. The code works if I parse the shared object file stored on my disk. Now, I wanted to use this code to parse the ELF heade...

이찬희 (MarkiiimarK)
Never Stop Learning.