Android ndk开发/逆向之so文件格式基础

admin 2022年8月20日14:46:27评论84 views字数 28259阅读94分11秒阅读模式

推荐阅读

ARM64逆向基础

ELF加壳原理与实现

浅谈加解密技术原理

Android防逆向基础

安卓逆向之ARM汇编基础

安卓逆向之常用加密算法

Android逆向之分析基础

IDA静态动态逆向分析基础

Android逆向分析基础(一)

Android逆向分析基础(二)

Android安全之ELF文件格式浅析

Android安全之DEX文件格式浅析

逆向分析中加解密算法常用工具

ELF文件注入shellcode原理与实现

Android APP开发之frida hook自吐算法

Android逆向之Magisk+Edxposed刷入教程(内附资源)

Android应用安全开发之浅谈加密算法隐藏的安全风险

零基础学编程/零基础学安全/零基础学逆向实战速成培训班



Android安全逆向课程,扫码领取(过Root、过Hook、过各种反调试、无需安装证书抓Https等风控检测、堆栈分析、算法分析等)

Android ndk开发/逆向之so文件格式基础

第一、前言

不仅要研究破解之道,也要研究加密之道,因为加密和破解是相生相克的。但是在破解的过程中可能最头疼的是native层,也就是so文件的破解。

先来介绍一下elf文件的格式,因为知道Android中的so文件就是elf文件,所以需要了解so文件,必须先来了解一下elf文件的格式,对于如何详细了解一个elf文件,就是手动的写一个工具类来解析一个elf文件。

第二、准备资料

我们需要了解elf文件的格式,关于elf文件格式详解,网上已经有很多介绍资料了。这里我也不做太多的解释了。不过有两个资料还是需要介绍一下的,因为网上的内容真的很多,很杂。这两个资料是最全的,也是最好的。我就是看这两个资料来操作的:

第一个资料是非虫大哥的经典之作:

Android ndk开发/逆向之so文件格式基础

看吧,是不是超级详细?后面我们用Java代码来解析elf文件的时候,就是按照这张图来的。但是这张图有些数据结构解释的还不是很清楚,所以第二个资料来了。

第二个资料:北京大学实验室出的标准版

http://download.csdn.net/detail/jiangwei0910410003/9204051

这里就不对这个文件做详细解释了,后面在做解析工作的时候,会截图说明。

关于上面的这两个资料,这里还是多数两句:一定要仔细认真的阅读。这个是经典之作。也是后面工作的基础。

第三、工具

当然这里还需要介绍一个工具,因为这个工具在我们下面解析elf文件的时候,也非常有用,而且是检查我们解析elf文件的模板。

就是很出名的:readelf命令

不过Window下这个命令不能用,因为这个命令是Linux的,所以我们还得做个工作就是安装Cygwin。关于这个工具的安装,大家可以看看这篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/17710243

不过在下载的过程中,我担心小朋友们会遇到挫折,所以很贴心的,放到的云盘里面:

http://pan.baidu.com/s/1C1Zci

下载下来之后,需要改一个东西才能用:

Android ndk开发/逆向之so文件格式基础

该一下这个文件:

Android ndk开发/逆向之so文件格式基础

这个路径要改成你本地cygwin64中的bin目录的路径,不然运行错误的。改好之后,直接运行Cygwin.bat就可以了。

关于readelf工具我们这里不做太详细的介绍,只介绍我们要用到的命令:

1、readelf -h xxx.so

查看so文件的头部信息

Android ndk开发/逆向之so文件格式基础

2、readelf -S xxx.so

查看so文件的段(Section)头的信息

Android ndk开发/逆向之so文件格式基础

3、readelf -l xxx.so

查看so文件的程序段头信息(Program)

Android ndk开发/逆向之so文件格式基础

4、readelf -a xxx.so

查看so文件的全部内容

Android ndk开发/逆向之so文件格式基础

还有很多命令用法,这里就不在细说了,网上有很多介绍的~~

第四、实际操作解析Elf文件(Java代码&C++代码)

上面我们介绍了elf文件格式资料,elf文件的工具,那么下面我们就来实际操作一下,来用Java代码手把手的解析一个libhello-jni.so文件。关于这个libhello-jni.so文件的下载地址:

http://download.csdn.net/detail/jiangwei0910410003/9204087

1、首先定义elf文件中各个结构体内容

这个我们需要参考elf.h这个头文件的格式了。这个文件网上也是有的,这里还是给个下载链接吧:

http://download.csdn.net/detail/jiangwei0910410003/9204081

我们看看Java中定义的elf文件的数据结构类:

package com.demo.parseso;

import java.util.ArrayList;

public class ElfType32 {

    public elf32_rel rel;
    public elf32_rela rela;
    public ArrayList<Elf32_Sym> symList = new ArrayList<Elf32_Sym>();
    public elf32_hdr hdr;//elf头部信息
    public ArrayList<elf32_phdr> phdrList = new ArrayList<elf32_phdr>();//可能会有多个程序头
    public ArrayList<elf32_shdr> shdrList = new ArrayList<elf32_shdr>();//可能会有多个段头
    public ArrayList<elf32_strtb> strtbList = new ArrayList<elf32_strtb>();//可能会有多个字符串值

    public ElfType32() {
        rel = new elf32_rel();
        rela = new elf32_rela();
        hdr = new elf32_hdr();
    }

    /**
     *  typedef struct elf32_rel {
          Elf32_Addr    r_offset;
          Elf32_Word    r_info;
        } Elf32_Rel;
     *
     */

    public class elf32_rel {
        public byte[] r_offset = new byte[4];
        public byte[] r_info = new byte[4];

        @Override
        public String toString(){
            return "r_offset:"+Utils.bytes2HexString(r_offset)+";r_info:"+Utils.bytes2HexString(r_info);
        }
    }

    /**
     *  typedef struct elf32_rela{
          Elf32_Addr    r_offset;
          Elf32_Word    r_info;
          Elf32_Sword   r_addend;
        } Elf32_Rela;
     */

    public class elf32_rela{
        public byte[] r_offset = new byte[4];
        public byte[] r_info = new byte[4];
        public byte[] r_addend = new byte[4];

        @Override
        public String toString(){
            return "r_offset:"+Utils.bytes2HexString(r_offset)+";r_info:"+Utils.bytes2HexString(r_info)+";r_addend:"+Utils.bytes2HexString(r_info);
        }
    }

    /**
     * typedef struct elf32_sym{
          Elf32_Word    st_name;
          Elf32_Addr    st_value;
          Elf32_Word    st_size;
          unsigned char st_info;
          unsigned char st_other;
          Elf32_Half    st_shndx;
        } Elf32_Sym;
     */

    public static class Elf32_Sym{
        public byte[] st_name = new byte[4];
        public byte[] st_value = new byte[4];
        public byte[] st_size = new byte[4];
        public byte st_info;
        public byte st_other;
        public byte[] st_shndx = new byte[2];

        @Override
        public String toString(){
            return "st_name:"+Utils.bytes2HexString(st_name)
                    +"nst_value:"+Utils.bytes2HexString(st_value)
                    +"nst_size:"+Utils.bytes2HexString(st_size)
                    +"nst_info:"+(st_info/16)
                    +"nst_other:"+(((short)st_other) & 0xF)
                    +"nst_shndx:"+Utils.bytes2HexString(st_shndx);
        }
    }

    public void printSymList(){
        for(int i=0;i<symList.size();i++){
            System.out.println();
            System.out.println("The "+(i+1)+" Symbol Table:");
            System.out.println(symList.get(i).toString());
        }
    }

    //Bind字段==》st_info
    public static final int STB_LOCAL = 0;
    public static final int STB_GLOBAL = 1;
    public static final int STB_WEAK = 2;
    //Type字段==》st_other
    public static final int STT_NOTYPE = 0;
    public static final int STT_OBJECT = 1;
    public static final int STT_FUNC = 2;
    public static final int STT_SECTION = 3;
    public static final int STT_FILE = 4;
    /**
     * 这里需要注意的是还需要做一次转化
     *  #define ELF_ST_BIND(x)  ((x) >> 4)
        #define ELF_ST_TYPE(x)  (((unsigned int) x) & 0xf)
     */


    /**
     * typedef struct elf32_hdr{
          unsigned char e_ident[EI_NIDENT];
          Elf32_Half    e_type;
          Elf32_Half    e_machine;
          Elf32_Word    e_version;
          Elf32_Addr    e_entry;  // Entry point
          Elf32_Off e_phoff;
          Elf32_Off e_shoff;
          Elf32_Word    e_flags;
          Elf32_Half    e_ehsize;
          Elf32_Half    e_phentsize;
          Elf32_Half    e_phnum;
          Elf32_Half    e_shentsize;
          Elf32_Half    e_shnum;
          Elf32_Half    e_shstrndx;
        } Elf32_Ehdr;
     */

    public class elf32_hdr{
        public byte[] e_ident = new byte[16];
        public byte[] e_type = new byte[2];
        public byte[] e_machine = new byte[2];
        public byte[] e_version = new byte[4];
        public byte[] e_entry = new byte[4];
        public byte[] e_phoff = new byte[4];
        public byte[] e_shoff = new byte[4];
        public byte[] e_flags = new byte[4];
        public byte[] e_ehsize = new byte[2];
        public byte[] e_phentsize = new byte[2];
        public byte[] e_phnum = new byte[2];
        public byte[] e_shentsize = new byte[2];
        public byte[] e_shnum = new byte[2];
        public byte[] e_shstrndx = new byte[2];

        @Override
        public String toString(){
            return  "magic:"+ Utils.bytes2HexString(e_ident) 
                    +"ne_type:"+Utils.bytes2HexString(e_type)
                    +"ne_machine:"+Utils.bytes2HexString(e_machine)
                    +"ne_version:"+Utils.bytes2HexString(e_version)
                    +"ne_entry:"+Utils.bytes2HexString(e_entry)
                    +"ne_phoff:"+Utils.bytes2HexString(e_phoff)
                    +"ne_shoff:"+Utils.bytes2HexString(e_shoff)
                    +"ne_flags:"+Utils.bytes2HexString(e_flags)
                    +"ne_ehsize:"+Utils.bytes2HexString(e_ehsize)
                    +"ne_phentsize:"+Utils.bytes2HexString(e_phentsize)
                    +"ne_phnum:"+Utils.bytes2HexString(e_phnum)
                    +"ne_shentsize:"+Utils.bytes2HexString(e_shentsize)
                    +"ne_shnum:"+Utils.bytes2HexString(e_shnum)
                    +"ne_shstrndx:"+Utils.bytes2HexString(e_shstrndx);
        }
    }

    /**
     * typedef struct elf32_phdr{
          Elf32_Word    p_type;
          Elf32_Off p_offset;
          Elf32_Addr    p_vaddr;
          Elf32_Addr    p_paddr;
          Elf32_Word    p_filesz;
          Elf32_Word    p_memsz;
          Elf32_Word    p_flags;
          Elf32_Word    p_align;
        } Elf32_Phdr;
     */

    public static class elf32_phdr{
        public byte[] p_type = new byte[4];
        public byte[] p_offset = new byte[4];
        public byte[] p_vaddr = new byte[4];
        public byte[] p_paddr = new byte[4];
        public byte[] p_filesz = new byte[4];
        public byte[] p_memsz = new byte[4];
        public byte[] p_flags = new byte[4];
        public byte[] p_align = new byte[4];

        @Override
        public String toString(){
            return "p_type:"+ Utils.bytes2HexString(p_type)
                    +"np_offset:"+Utils.bytes2HexString(p_offset)
                    +"np_vaddr:"+Utils.bytes2HexString(p_vaddr)
                    +"np_paddr:"+Utils.bytes2HexString(p_paddr)
                    +"np_filesz:"+Utils.bytes2HexString(p_filesz)
                    +"np_memsz:"+Utils.bytes2HexString(p_memsz)
                    +"np_flags:"+Utils.bytes2HexString(p_flags)
                    +"np_align:"+Utils.bytes2HexString(p_align);
        }
    }

    public void printPhdrList(){
        for(int i=0;i<phdrList.size();i++){
            System.out.println();
            System.out.println("The "+(i+1)+" Program Header:");
            System.out.println(phdrList.get(i).toString());
        }
    }

    /**
     * typedef struct elf32_shdr {
          Elf32_Word    sh_name;
          Elf32_Word    sh_type;
          Elf32_Word    sh_flags;
          Elf32_Addr    sh_addr;
          Elf32_Off sh_offset;
          Elf32_Word    sh_size;
          Elf32_Word    sh_link;
          Elf32_Word    sh_info;
          Elf32_Word    sh_addralign;
          Elf32_Word    sh_entsize;
        } Elf32_Shdr;
     */

    public static class elf32_shdr{
        public byte[] sh_name = new byte[4];
        public byte[] sh_type = new byte[4];
        public byte[] sh_flags = new byte[4];
        public byte[] sh_addr = new byte[4];
        public byte[] sh_offset = new byte[4];
        public byte[] sh_size = new byte[4];
        public byte[] sh_link = new byte[4];
        public byte[] sh_info = new byte[4];
        public byte[] sh_addralign = new byte[4];
        public byte[] sh_entsize = new byte[4];

        @Override
        public String toString(){
            return "sh_name:"+Utils.bytes2HexString(sh_name)/*Utils.byte2Int(sh_name)*/
                    +"nsh_type:"+Utils.bytes2HexString(sh_type)
                    +"nsh_flags:"+Utils.bytes2HexString(sh_flags)
                    +"nsh_add:"+Utils.bytes2HexString(sh_addr)
                    +"nsh_offset:"+Utils.bytes2HexString(sh_offset)
                    +"nsh_size:"+Utils.bytes2HexString(sh_size)
                    +"nsh_link:"+Utils.bytes2HexString(sh_link)
                    +"nsh_info:"+Utils.bytes2HexString(sh_info)
                    +"nsh_addralign:"+Utils.bytes2HexString(sh_addralign)
                    +"nsh_entsize:"+ Utils.bytes2HexString(sh_entsize);
        }
    }

    /****************sh_type********************/
    public static final int SHT_NULL = 0;
    public static final int SHT_PROGBITS = 1;
    public static final int SHT_SYMTAB = 2;
    public static final int SHT_STRTAB = 3;
    public static final int SHT_RELA = 4;
    public static final int SHT_HASH = 5;
    public static final int SHT_DYNAMIC = 6;
    public static final int SHT_NOTE = 7;
    public static final int SHT_NOBITS = 8;
    public static final int SHT_REL = 9;
    public static final int SHT_SHLIB = 10;
    public static final int SHT_DYNSYM = 11;
    public static final int SHT_NUM = 12;
    public static final int SHT_LOPROC = 0x70000000;
    public static final int SHT_HIPROC = 0x7fffffff;
    public static final int SHT_LOUSER = 0x80000000;
    public static final int SHT_HIUSER = 0xffffffff;
    public static final int SHT_MIPS_LIST = 0x70000000;
    public static final int SHT_MIPS_CONFLICT = 0x70000002;
    public static final int SHT_MIPS_GPTAB = 0x70000003;
    public static final int SHT_MIPS_UCODE = 0x70000004;

    /*****************sh_flag***********************/
    public static final int SHF_WRITE = 0x1;
    public static final int SHF_ALLOC = 0x2;
    public static final int SHF_EXECINSTR = 0x4;
    public static final int SHF_MASKPROC = 0xf0000000;
    public static final int SHF_MIPS_GPREL = 0x10000000;

    public void printShdrList(){
        for(int i=0;i<shdrList.size();i++){
            System.out.println();
            System.out.println("The "+(i+1)+" Section Header:");
            System.out.println(shdrList.get(i));
        }
    }


    public static class elf32_strtb{
        public byte[] str_name;
        public int len;

        @Override
        public String toString(){
            return "str_name:"+str_name
                    +"len:"+len;
        }
    }
}

这个没什么问题,也没难度,就是在看elf.h文件中定义的数据结构的时候,要记得每个字段的占用字节数就可以了。

有了结构定义,下面就来看看如何解析吧。

在解析之前我们需要将so文件读取到byte[]中,定义一个数据结构类型

public static ElfType32 type_32 = new ElfType32();

byte[] fileByteArys = Utils.readFile("so/libhello-jni.so");
if(fileByteArys == null){
    System.out.println("read file byte failed...");
    return;
}

2、解析elf文件的头部信息

Android ndk开发/逆向之so文件格式基础

关于这些字段的解释,要看上面提到的那个pdf文件中的描述

这里我们介绍几个重要的字段,也是我们后面修改so文件的时候也会用到:

1)、e_phoff

这个字段是程序头(Program Header)内容在整个文件的偏移值,我们可以用这个偏移值来定位程序头的开始位置,用于解析程序头信息

2)、e_shoff

这个字段是段头(Section Header)内容在这个文件的偏移值,我们可以用这个偏移值来定位段头的开始位置,用于解析段头信息

3)、e_phnum

这个字段是程序头的个数,用于解析程序头信息

4)、e_shnum

这个字段是段头的个数,用于解析段头信息

5)、e_shstrndx

这个字段是String段在整个段列表中的索引值,这个用于后面定位String段的位置

按照上面的图我们就可以很容易的解析

/**
 * 解析Elf的头部信息
 * @param header
 */

private static void  parseHeader(byte[] header, int offset){
    if(header == null){
        System.out.println("header is null");
        return;
    }
    /**
     *  public byte[] e_ident = new byte[16];
            public short e_type;
            public short e_machine;
            public int e_version;
            public int e_entry;
            public int e_phoff;
            public int e_shoff;
            public int e_flags;
            public short e_ehsize;
            public short e_phentsize;
            public short e_phnum;
            public short e_shentsize;
            public short e_shnum;
            public short e_shstrndx;
     */

    type_32.hdr.e_ident = Utils.copyBytes(header, 016);//魔数
    type_32.hdr.e_type = Utils.copyBytes(header, 162);
    type_32.hdr.e_machine = Utils.copyBytes(header, 182);
    type_32.hdr.e_version = Utils.copyBytes(header, 204);
    type_32.hdr.e_entry = Utils.copyBytes(header, 244);
    type_32.hdr.e_phoff = Utils.copyBytes(header, 284);
    type_32.hdr.e_shoff = Utils.copyBytes(header, 324);
    type_32.hdr.e_flags = Utils.copyBytes(header, 364);
    type_32.hdr.e_ehsize = Utils.copyBytes(header, 402);
    type_32.hdr.e_phentsize = Utils.copyBytes(header, 422);
    type_32.hdr.e_phnum = Utils.copyBytes(header, 44,2);
    type_32.hdr.e_shentsize = Utils.copyBytes(header, 46,2);
    type_32.hdr.e_shnum = Utils.copyBytes(header, 482);
    type_32.hdr.e_shstrndx = Utils.copyBytes(header, 502);
}

按照对应的每个字段的字节个数,读取byte就可以了。

3、解析段头(Section Header)信息

Android ndk开发/逆向之so文件格式基础

这个结构中字段见pdf中的描述吧,这里就不做解释了。后面我们会手动的构造这样的一个数据结构,到时候在详细说明每个字段含义。

按照这个结构。我们解析也简单了:

这里需要注意的是,我们看到的Section Header一般都是多个的,这里用一个List来保存

/**
 * 解析段头信息内容
 */

public static void parseSectionHeaderList(byte[] header, int offset){
    int header_size = 40;//40个字节
    int header_count = Utils.byte2Short(type_32.hdr.e_shnum);//头部的个数
    byte[] des = new byte[header_size];
    for(int i=0;i<header_count;i++){
        System.arraycopy(header, i*header_size + offset, des, 0, header_size);
        type_32.shdrList.add(parseSectionHeader(des));
    }
}

private static elf32_shdr parseSectionHeader(byte[] header){
    ElfType32.elf32_shdr shdr = new ElfType32.elf32_shdr();
    /**
     *  public byte[] sh_name = new byte[4];
            public byte[] sh_type = new byte[4];
            public byte[] sh_flags = new byte[4];
            public byte[] sh_addr = new byte[4];
            public byte[] sh_offset = new byte[4];
            public byte[] sh_size = new byte[4];
            public byte[] sh_link = new byte[4];
            public byte[] sh_info = new byte[4];
            public byte[] sh_addralign = new byte[4];
            public byte[] sh_entsize = new byte[4];
     */

    shdr.sh_name = Utils.copyBytes(header, 04);
    shdr.sh_type = Utils.copyBytes(header, 44);
    shdr.sh_flags = Utils.copyBytes(header, 84);
    shdr.sh_addr = Utils.copyBytes(header, 124);
    shdr.sh_offset = Utils.copyBytes(header, 164);
    shdr.sh_size = Utils.copyBytes(header, 204);
    shdr.sh_link = Utils.copyBytes(header, 244);
    shdr.sh_info = Utils.copyBytes(header, 284);
    shdr.sh_addralign = Utils.copyBytes(header, 324);
    shdr.sh_entsize = Utils.copyBytes(header, 364);
    return shdr;
}

4、解析程序头(Program Header)信息

Android ndk开发/逆向之so文件格式基础


这里的字段,这里也不做解释了,看pdf文档。


我们按照这个结构来进行解析:

/**
 * 解析程序头信息
 * @param header
 */

public static void parseProgramHeaderList(byte[] header, int offset){
    int header_size = 32;//32个字节
    int header_count = Utils.byte2Short(type_32.hdr.e_phnum);//头部的个数
    byte[] des = new byte[header_size];
    for(int i=0;i<header_count;i++){
        System.arraycopy(header, i*header_size + offset, des, 0, header_size);
        type_32.phdrList.add(parseProgramHeader(des));
    }
}

private static elf32_phdr parseProgramHeader(byte[] header){
    /**
     *  public int p_type;
            public int p_offset;
            public int p_vaddr;
            public int p_paddr;
            public int p_filesz;
            public int p_memsz;
            public int p_flags;
            public int p_align;
     */

    ElfType32.elf32_phdr phdr = new ElfType32.elf32_phdr();
    phdr.p_type = Utils.copyBytes(header, 04);
    phdr.p_offset = Utils.copyBytes(header, 44);
    phdr.p_vaddr = Utils.copyBytes(header, 84);
    phdr.p_paddr = Utils.copyBytes(header, 124);
    phdr.p_filesz = Utils.copyBytes(header, 164);
    phdr.p_memsz = Utils.copyBytes(header, 204);
    phdr.p_flags = Utils.copyBytes(header, 244);
    phdr.p_align = Utils.copyBytes(header, 284);
    return phdr;

}

当然还有其他结构的解析工作,这里就不在一一介绍了,因为这些结构我们在后面的介绍中不会用到,但是也是需要了解的,详细参见pdf文档。

5、验证解析结果

那么上面我们的解析工作做完了,为了验证我们的解析工作是否正确,我们需要给每个结构定义个打印函数,也就是从写toString方法即可。

Android ndk开发/逆向之so文件格式基础

然后我们在使用readelf工具来查看so文件的各个结构内容,对比就可以知道解析的是否成功了。

解析代码下载地址:https://github.com/fourbrother/parse_androidso

上面我们用的是Java代码来进行解析的,为了照顾广大程序猿,所以给出一个C++版本的解析类:

#include<iostream.h>
#include<string.h>
#include<stdio.h>
#include "elf.h"

/**
    非常重要的一个宏,功能很简单:
    P:需要对其的段地址
    ALIGNBYTES:对其的字节数
    功能:将P值补充到时ALIGNBYTES的整数倍
    这个函数也叫:页面对其函数
    eg: 0x3e45/0x1000 == >0x4000

*/

#define ALIGN(P, ALIGNBYTES)  ( ((unsigned long)P + ALIGNBYTES -1)&~(ALIGNBYTES-1) )

int addSectionFun(char*, char*, unsigned int);

int main()
{
    addSectionFun("D:libhello-jni.so"".jiangwei"0x1000);
    return 0;
}

int addSectionFun(char *lpPath, char *szSecname, unsigned int nNewSecSize)
{
    char name[50];
    FILE *fdr, *fdw;
    char *base = NULL;
    Elf32_Ehdr *ehdr;
    Elf32_Phdr *t_phdr, *load1, *load2, *dynamic;
    Elf32_Shdr *s_hdr;
    int flag = 0;
    int i = 0;
    unsigned mapSZ = 0;
    unsigned nLoop = 0;
    unsigned int nAddInitFun = 0;
    unsigned int nNewSecAddr = 0;
    unsigned int nModuleBase = 0;
    memset(name, 0sizeof(name));
    if(nNewSecSize == 0)
    {
        return 0;
    }
    fdr = fopen(lpPath, "rb");
    strcpy(name, lpPath);
    if(strchr(name, '.'))
    {
        strcpy(strchr(name, '.'), "_new.so");
    }
    else
    {
        strcat(name, "_new");
    }
    fdw = fopen(name, "wb");
    if(fdr == NULL || fdw == NULL)
    {
        printf("Open file failed");
        return 1;
    }
    fseek(fdr, 0, SEEK_END);
    mapSZ = ftell(fdr);//源文件的长度大小
    printf("mapSZ:0x%xn", mapSZ);

    base = (char*)malloc(mapSZ * 2 + nNewSecSize);//2*源文件大小+新加的Section size
    printf("base 0x%x n", base);

    memset(base, 0, mapSZ * 2 + nNewSecSize);
    fseek(fdr, 0, SEEK_SET);
    fread(base, 1, mapSZ, fdr);//拷贝源文件内容到base
    if(base == (void*) -1)
    {
        printf("fread fd failed");
        return 2;
    }

    //判断Program Header
    ehdr = (Elf32_Ehdr*) base;
    t_phdr = (Elf32_Phdr*)(base + sizeof(Elf32_Ehdr));
    for(i=0;i<ehdr->e_phnum;i++)
    {
        if(t_phdr->p_type == PT_LOAD)
        {
            //这里的flag只是一个标志位,去除第一个LOAD的Segment的值
            if(flag == 0)
            {
                load1 = t_phdr;
                flag = 1;
                nModuleBase = load1->p_vaddr;
                printf("load1 = %p, offset = 0x%x n", load1, load1->p_offset);

            }
            else
            {
                load2 = t_phdr;
                printf("load2 = %p, offset = 0x%x n", load2, load2->p_offset);
            }
        }
        if(t_phdr->p_type == PT_DYNAMIC)
        {
            dynamic = t_phdr;
            printf("dynamic = %p, offset = 0x%x n", dynamic, dynamic->p_offset);
        }
        t_phdr ++;
    }

    //section header
    s_hdr = (Elf32_Shdr*)(base + ehdr->e_shoff);
    //获取到新加section的位置,这个是重点,需要进行页面对其操作
    printf("addr:0x%xn",load2->p_paddr);
    nNewSecAddr = ALIGN(load2->p_paddr + load2->p_memsz - nModuleBase, load2->p_align);
    printf("new section add:%x n", nNewSecAddr);

    if(load1->p_filesz < ALIGN(load2->p_paddr + load2->p_memsz, load2->p_align) )
    {
        printf("offset:%xn",(ehdr->e_shoff + sizeof(Elf32_Shdr) * ehdr->e_shnum));
        //注意这里的代码的执行条件,这里其实就是判断section header是不是在文件的末尾
        if( (ehdr->e_shoff + sizeof(Elf32_Shdr) * ehdr->e_shnum) != mapSZ)
        {
            if(mapSZ + sizeof(Elf32_Shdr) * (ehdr->e_shnum + 1) > nNewSecAddr)
            {
                printf("无法添加节n");
                return 3;
            }
            else
            {
                memcpy(base + mapSZ, base + ehdr->e_shoff, sizeof(Elf32_Shdr) * ehdr->e_shnum);//将Section Header拷贝到原来文件的末尾
                ehdr->e_shoff = mapSZ;
                mapSZ += sizeof(Elf32_Shdr) * ehdr->e_shnum;//加上Section Header的长度
                s_hdr = (Elf32_Shdr*)(base + ehdr->e_shoff);
                printf("ehdr_offset:%x",ehdr->e_shoff);
            }
        }
    }
    else
    {
        nNewSecAddr = load1->p_filesz;
    }
    printf("还可添加 %d 个节n", (nNewSecAddr - ehdr->e_shoff) / sizeof(Elf32_Shdr) - ehdr->e_shnum - 1);

    int nWriteLen = nNewSecAddr + ALIGN(strlen(szSecname) + 10x10) + nNewSecSize;//添加section之后的文件总长度:原来的长度 + section name + section size
    printf("write len %xn",nWriteLen);

    char *lpWriteBuf = (char *)malloc(nWriteLen);//nWriteLen :最后文件的总大小
    memset(lpWriteBuf, 0, nWriteLen);
    //ehdr->e_shstrndx是section name的string表在section表头中的偏移值,修改string段的大小
    s_hdr[ehdr->e_shstrndx].sh_size = nNewSecAddr - s_hdr[ehdr->e_shstrndx].sh_offset + strlen(szSecname) + 1;
    strcpy(lpWriteBuf + nNewSecAddr, szSecname);//添加section name

    //以下代码是构建一个Section Header
    Elf32_Shdr newSecShdr = {0};
    newSecShdr.sh_name = nNewSecAddr - s_hdr[ehdr->e_shstrndx].sh_offset;
    newSecShdr.sh_type = SHT_PROGBITS;
    newSecShdr.sh_flags = SHF_WRITE | SHF_ALLOC | SHF_EXECINSTR;
    nNewSecAddr += ALIGN(strlen(szSecname) + 10x10);
    newSecShdr.sh_size = nNewSecSize;
    newSecShdr.sh_offset = nNewSecAddr;
    newSecShdr.sh_addr = nNewSecAddr + nModuleBase;
    newSecShdr.sh_addralign = 4;

    //修改Program Header信息
    load1->p_filesz = nWriteLen;
    load1->p_memsz = nNewSecAddr + nNewSecSize;
    load1->p_flags = 7;     //可读 可写 可执行

    //修改Elf header中的section的count值
    ehdr->e_shnum++;
    memcpy(lpWriteBuf, base, mapSZ);//从base中拷贝mapSZ长度的字节到lpWriteBuf
    memcpy(lpWriteBuf + mapSZ, &newSecShdr, sizeof(Elf32_Shdr));//将新加的Section Header追加到lpWriteBuf末尾

    //写文件
    fseek(fdw, 0, SEEK_SET);
    fwrite(lpWriteBuf, 1, nWriteLen, fdw);
    fclose(fdw);
    fclose(fdr);
    free(base);
    free(lpWriteBuf);
    return 0;
}

看了C++代码解析之后,这里不得不多说两句了,看看C++中的代码多么简单,原因很简单:在做文件字节操作的时候,C++中的指针真的很牛逼的,这个也是Java望成莫及的。。

C++代码下载:http://download.csdn.net/detail/jiangwei0910410003/9204139  

第五、总结

关于Elf文件的格式,就介绍到这里,通过自己写一个解析类的话,可以很深刻的了解elf文件的格式,所以我们在以后遇到一个文件格式的了解过程中,最好的方式就是手动的写一个工具类就好了。那么这篇文章是逆向之旅的第一篇,也是以后篇章的基础,下面一篇文章我们会介绍如何来手动的在elf中添加一个段数据结构,尽情期待~~

转:https://www.jianshu.com/p/a2d87d9467cc

So文件添加Section段详细说明

简介

在逆向Android底层时,一般都或多或少的接触so文件,需要逆向so文件,一般的方法是往so文件植入我们的调试的代码;而通常都是通过添加section段来植入代码;查看本篇文章之前你需要先了解elf文件格式,so文件就是采用这种格式的

简单介绍elf文件格式

elf文件一般是由elf文件头、program header头(多个)、section段(多个)和section header头(多个组成)。

program header

程序运行时,定位文件中各个段的位置,提供创建程序映像的具体信息;一个program header指向一个segment(运行时的称呼),而一个segement可能包含多个section 段

Android ndk开发/逆向之so文件格式基础

section header

执行之前描述文件结构的,理论上来说,Linux运行期间不需要section header头的,可以删掉;一个section header头对应一个section 段,所以添加一个section段时还要添加一个section header头

section段

section段是elf文件的主题,很多内容都保存在里面,包括我们熟悉的.text、.rodata、.data等等,还有我们不熟悉的.got(全局偏移表)、.plt(过程链接表)等等,这两个表主要用于函数和全局变量的调用,可参考这个链接理解这两个表,下面是展示部分的section header头:

Android ndk开发/逆向之so文件格式基础

进入主题 -- 添加section字段

当ELF文件被加载到内存中后,系统会将多个具有相同权限(flg值)section合并一个segment。操作系统往往以页为基本单位来管理内存分配,一般页的大小为4096B,即4KB的大小。同时,内存的权限管理的粒度也是以页为单位,页内的内存是具有同样的权限等属性,并且操作系统对内存的管理往往追求高效和高利用率这样的目标。ELF文件在被映射时,是以系统的页长度为单位的,那么每个section在映射时的长度都是系统页长度的整数倍,如果section的长度不是其整数倍,则导致多余部分也将占用一个页。所以section段的位置和长度不是随便添加的,要根据program header头的align对齐方式来添加

确定添加位置

新加入的section一般是在文件末尾,但是这里不是真正的文件末尾,而是将文件加载到内存映像中的末尾,一般等于虚拟地址vaddr+占用空间大小memsiz,但是这样还不行,还要做字节对其,我们知道文件末尾不等于映像末尾,一般这个映像末尾比文件末尾大,因为操作系统对内存都是以页的粒度来操作的,所以我们添加section段要找到真正的映像末尾,操作系统会把program header的type为LOAD的段加入内存映像中,而LOAD类型的program header一般都是做升序排列,我们只需要取最后一个LOAD就可以,让vaddr+memsiz在和align做对齐操作就能够得到映像末尾,算法如下:

/**
     * 对其算法 --- 保证addr能被align整除,系统按align字节对齐的访问方式
     * 返回一个addr是align的整数倍的值
     * @param addr:vaddr + memsi
     * @param align:对其的字节数
     * @return
     */
    public static int align(int addr, int align){
        if(align > addr){    //这种情况不好弄,我也不知道怎么办
            return addr;
        }
        int offset = addr % align;
        return addr + (align-offset);
    }
Android ndk开发/逆向之so文件格式基础

添加section header头

添加section header头的目的是把5000H的表名和5010H的内容联系起来;先看section header的结构:

typedef struct{
    Elf32_Word sh_name;
    Elf32_Word sh_type;
    Elf32_Word sh_flags;
    Elf32_Addr sh_addr;
    Elf32_Off  sh_offset;
    Elf32_Word sh_size;
    Elf32_Word sh_link;
    Elf32_Word sh_info;
    Elf32_Word sh_addralign;
    Elf32_Word sh_entsize;
}Elf32_Shdr;

sh_name : 是一个字符串表的索引偏移(5000H - 第一个字符串表的位置,第一个字符串表位置等于)

sh_addr和 sh_offset填上一个步骤得到的地址即可

sh_size:就是我们填入的section长度

sh_type:SHT_PROGBITS 1 此节区包含程序定义的信息,其格式和含义都由程序来解释。

sh_flags:

名称 取值 意义
SHF_WRITE 0x1 节区包含进程执行过程中将可写的数据
SHF_ALLOC 0x2 节区在进程执行过程中占用内存。某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为 0
SHF_EXECINSTR 0x4 节区包含可执行的机器指令
SHF_MASKPROC 0xF0000000 所有包含于此掩码中的四位都用于处理器专用的语义

其他的几个可以不用处理,以下是示例代码:

public static byte[] addSectionHeader(byte[] src){
                /**
                 *  public byte[] sh_name = new byte[4];
                 public byte[] sh_type = new byte[4];
                 public byte[] sh_flags = new byte[4];
                 public byte[] sh_addr = new byte[4];
                 public byte[] sh_offset = new byte[4];
                 public byte[] sh_size = new byte[4];
                 public byte[] sh_link = new byte[4];
                 public byte[] sh_info = new byte[4];
                 public byte[] sh_addralign = new byte[4];
                 public byte[] sh_entsize = new byte[4];
                 */

                byte[] newHeader = new byte[sectionSize];

                //构建一个New Section Header
                newHeader = Utils.replaceByteAry(newHeader, 0, Utils.int2Byte(addSectionStartAddr - stringSectionOffset));
                newHeader = Utils.replaceByteAry(newHeader, 4, Utils.int2Byte(ElfType32.SHT_PROGBITS));                         //type=PROGBITS
                newHeader = Utils.replaceByteAry(newHeader, 8, Utils.int2Byte(ElfType32.SHF_ALLOC + ElfType32.SHF_WRITE));              //改字节区可以被写入
                newHeader = Utils.replaceByteAry(newHeader, 12, Utils.int2Byte(addSectionStartAddr+0x10));                        //所代表的字节区其实地址
                newHeader = Utils.replaceByteAry(newHeader, 16, Utils.int2Byte(addSectionStartAddr+0x10));
                newHeader = Utils.replaceByteAry(newHeader, 20, Utils.int2Byte(newSectionSize));        //字节区大小
                newHeader = Utils.replaceByteAry(newHeader, 24, Utils.int2Byte(0));
                newHeader = Utils.replaceByteAry(newHeader, 28, Utils.int2Byte(0));
                newHeader = Utils.replaceByteAry(newHeader, 32, Utils.int2Byte(4));
                newHeader = Utils.replaceByteAry(newHeader, 36, Utils.int2Byte(0));

                //在末尾增加Section
                byte[] newSrc = new byte[src.length + newHeader.length];
                newSrc = Utils.replaceByteAry(newSrc, 0, src);
                newSrc = Utils.replaceByteAry(newSrc, src.length, newHeader);

                return newSrc;
        }

完善步骤

修改第一个LOAD的program header

将其filesize和memsiz改为文件的总长度

修改section header为.shstrta

将他的size在原有的基础上加16即可,因为增加了一个section name

至此,完结,如果把so文件弄回去报了这个错误  
load segment1: p_offset (0x0) + p_filesz (0x53f8) ( = 0x53f8) past end of file (0x53f8)  
这是因为你刚刚添加的段的大小超出了文件大小,因为我们修改program header的filesize和memsize文件总长度,这个长度包括啦文件结束标志0x0a,我们要修改这两个值为文件长度减去1即可

作者:jackzhoud  链接:https://www.jianshu.com/p/a2d87d9467cc  來源:简书  简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

kali渗透测试环境搭建

Web安全|docker环境搭建(2)

Android APP防作弊SDK解决方案

Web安全攻防实战零基础速成培训班

Android10系统定制|frida逆向分析实战课程

Android10系统定制之frida逆向分析速成培训班

APP逆向分析/渗透测试/安全检测/隐私合规如何选择手机机型或系统


Android ndk开发/逆向之so文件格式基础


Android ndk开发/逆向之so文件格式基础

原文始发于微信公众号(哆啦安全):Android ndk开发/逆向之so文件格式基础

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年8月20日14:46:27
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Android ndk开发/逆向之so文件格式基础http://cn-sec.com/archives/1244875.html

发表评论

匿名网友 填写信息