Java解析ELF檔案:使用Java讀取檔案頭部、節區頭部表、程式頭部表

柴月和岐月發表於2018-01-07

    /*            名稱     大小 對齊     目的
        Elf32_Addr    4   4  無符號程式地址
        Elf32_Half    2   2  無符號中等整數
        Elf32_Off     4   4  無符號檔案偏移
        Elf32_SWord   4   4  有符號大整數
        Elf32_Word    4   4  無符號大整數
        unsigned char 1   1  無符號小整數*/
    public static final int Elf32_Addr = 4;
    public static final int Elf32_Half = 2;
    public static final int Elf32_Off = 4;
    public static final int Elf32_SWord = 4;
    public static final int Elf32_Word = 4;
    public static final int unsigned_char = 1;

本篇只講如何讀取,對讀取到的二進位制資料如何解析,有興趣的可以看下我寫的程式碼,然後自己根據上一篇部落格提供的PDF檔案自己讀取後進行解析。同時在讀取後,可以用010Edtitor對比檢視自己讀取的是否正確。

1.讀取檔案頭部

    /**
     * 檔案開始處是一個 ELF 頭部(ELF Header),用來描述整個檔案的組織。
     * 檔案的最開始幾個位元組給出如何解釋檔案的提示資訊。
     * 這些資訊獨立於處理器,也獨立於檔案中的其餘內容。(也就是說e_ident這裡的格式一直都是按大端模式讀取的?)
     * #define EI_NIDENT 16
     * typedef struct{
     * unsigned char  e_ident[EI_NIDENT];
     * Elf32_Half e_type;
     * Elf32_Half e_machine;
     * Elf32_Word e_version;
     * Elf32_Addr e_entry;
     * 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 static class Elf32_Ehdr {

        public byte[] e_ident = new byte[ELF32define.unsigned_char * ELF32define.EI_NIDENT];
        public byte[] e_type = new byte[ELF32define.Elf32_Half];
        public byte[] e_machine = new byte[ELF32define.Elf32_Half];
        public byte[] e_version = new byte[ELF32define.Elf32_Word];
        public byte[] e_entry = new byte[ELF32define.Elf32_Addr];
        public byte[] e_phoff = new byte[ELF32define.Elf32_Off];
        public byte[] e_shoff = new byte[ELF32define.Elf32_Off];
        public byte[] e_flags = new byte[ELF32define.Elf32_Word];
        public byte[] e_ehsize = new byte[ELF32define.Elf32_Half];
        public byte[] e_phentsize = new byte[ELF32define.Elf32_Half];
        public byte[] e_phnum = new byte[ELF32define.Elf32_Half];
        public byte[] e_shentsize = new byte[ELF32define.Elf32_Half];
        public byte[] e_shnum = new byte[ELF32define.Elf32_Half];
        public byte[] e_shstrndx = new byte[ELF32define.Elf32_Half];
檔案頭部在ELF檔案最開始的位置,因此我們只需要按序讀(按序填充位元組然後自己判斷)就是

        public Elf32_Ehdr(InputStream inputStream) throws IOException {
            inputStream.read(e_ident);
            inputStream.read(e_type);
            inputStream.read(e_machine);
            inputStream.read(e_version);
            inputStream.read(e_entry);
            inputStream.read(e_phoff);
            inputStream.read(e_shoff);
            inputStream.read(e_flags);
            inputStream.read(e_ehsize);
            inputStream.read(e_phentsize);
            inputStream.read(e_phnum);
            inputStream.read(e_shentsize);
            inputStream.read(e_shnum);
            inputStream.read(e_shstrndx);
        }
構造一個簡單讀取資料的建構函式,然後讀取

    public static void main(String[] args) {
        String path = "C:\\Users\\悅\\Desktop\\123.so";
        try {
            FileInputStream inputStream = new FileInputStream(path);
            try {
                ELFType32.Elf32_Ehdr ehdr = new ELFType32.Elf32_Ehdr(inputStream);
                System.out.println(ehdr);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
對得到的二進位制資料進行處理和判斷後(如何判斷的後面會提供原始碼,或者也可以在ELF檔案格式分析這個PDF裡找到判斷方式),最終結果如下:
{檔案標識=0x7f E L F, 檔案類=32位目標, 資料編碼=小端模式, 檔案版本=1, 補齊位元組開始處=00 00 00 00 00 00 00 00 00}
{目標檔案型別=共享目標檔案}
{給出檔案的目標體系結構型別=Intel 80386}
{目標檔案版本=當前版本}
{程式入口的虛擬地址=0}
{程式頭部表格(Program Header Table)的偏移量(按位元組計算)=52}
{節區頭部表格(Section Header Table)的偏移量(按位元組計算)=4476}

{儲存與檔案相關的,特定於處理器的標誌=0}
{頭部的大小(以位元組計算)=52}
{程式頭部表格的表項大小(按位元組計算)=32}
{程式頭部表格的表項數目(可以為 0)=8}
{節區頭部表格的表項大小(按位元組計算)=40}
{節區頭部表格的表項數目=25}
{區頭部表格中與節區名稱字串表相關的表項的索引=24}

這裡我們需要的資訊是,程式頭部表偏移為52,表內數量為8;節區頭部表偏移為4476,數量為25;節區名字儲存在第24個節區裡面。後面我們就利用這些資料在相應位置讀取
注:
1.偏移相對的是整個檔案
2.在進行計算的時候,要注意的是是用幾個位元組表示這個數字的,同時和它在記憶體中的儲存方式。
如程式頭部偏移為4個位元組,假如表示的偏移為666,二進位制則為00000000 000000000 00000010 10011010
對應16進位制則為0x00 0x00 0x02 0x9a
大端儲存則為上面的格式,而小端儲存則為0x9a 0x02 0x00 0x00,所以如果是自己寫解析器的時候,需要注意這一點
(記憶體中多為小端儲存,同時標頭檔案裡也會提供一個單位元組給出此檔案的儲存方式)

2.讀取節區頭部表

    /**
     * 節區頭部表(Section Heade Table)包含了描述檔案節區的資訊,每個節區在表中
     * 都有一項,每一項給出諸如節區名稱、節區大小這類資訊。用於連結的目標檔案必須包
     * 含節區頭部表,其他目標檔案可以有,也可以沒有這個表。
     * 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;
     */
    public static class Elf32_Shdr {

        public byte[] sh_name = new byte[ELF32define.Elf32_Word];
        public byte[] sh_type = new byte[ELF32define.Elf32_Word];
        public byte[] sh_flags = new byte[ELF32define.Elf32_Word];
        public byte[] sh_addr = new byte[ELF32define.Elf32_Addr];
        public byte[] sh_offset = new byte[ELF32define.Elf32_Off];
        public byte[] sh_size = new byte[ELF32define.Elf32_Word];
        public byte[] sh_link = new byte[ELF32define.Elf32_Word];
        public byte[] sh_info = new byte[ELF32define.Elf32_Word];
        public byte[] sh_addralign = new byte[ELF32define.Elf32_Word];
        public byte[] sh_entsize = new byte[ELF32define.Elf32_Word];
我們從上面知道節區頭部表的偏移為4476,數量為25,則
    public static void main(String[] args) {
        String path = "C:\\Users\\悅\\Desktop\\123.so";
        try {
            FileInputStream inputStream = new FileInputStream(path);
            try {
                inputStream.skip(4476);
                ELFType32.Elf32_Shdr shdr;
                for (int i = 0; i < 25; i++) {
                    System.out.println("       " + i);
                    shdr = new ELFType32.Elf32_Shdr(inputStream);
                    System.out.println(shdr);
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
對得到的二進位制資料進行處理和判斷後,這裡我們以上面說的儲存節區名字的第24區為例:
       24
{名稱索引=1}
{活動型別=STRTAB, 型別=3}
{標誌=無此定義}
{節區如果出現在程式的記憶體映像中的第一個位元組位置=0}
{檔案偏移=4232, 16進位制檔案偏移=1088}
{尺寸大小=241, 16進位制尺寸大小=f1}

{其他型別 sh_link:SHN_UNDEF=0}
{其他型別 sh_info:0=0}
{對齊要求=1}
{表項長度位元組數=0}

STRTAB是專門儲存節區名字的節區,字串之間插入數量不等的‘\0’來分割節。區頭部表的第一個屬性:名稱索引就是指向這個節區內字串的位置索引
所以這裡我們直接列印這個節區內所有的字串。
    public static void main(String[] args) {
        String path = "C:\\Users\\悅\\Desktop\\123.so";
        try {
            FileInputStream inputStream = new FileInputStream(path);
            try {
                inputStream.skip(4232);
                byte[] asd=new byte[241];
                inputStream.read(asd);
                System.out.println(new String(asd));
            }catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
結果如下:
.shstrtab .note.gnu.build-id .dynsym .dynstr .hash .gnu.version .gnu.version_d .gnu.version_r .rel.dyn等等

3.讀取程式頭部表

    /**
     * 可執行檔案或者共享目標檔案的程式頭部是一個結構陣列,每個結構描述了一個段或者系統準備程式執行所必需的其它資訊。
     * 目標檔案的“段”包含一個或者多個“節區”,也就是“段內容(Segment Contents)”。
     * 程式頭部僅對於可執行檔案和共享目標檔案有意義。
     * 可執行目標檔案在ELF頭部的e_phentsize和e_phnum成員中給出其自身程式頭部的大小。
     * 程式頭部的資料結構如下圖:
     * typedef struct {
     * 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;
     * 其中各個欄位說明如下:
     * p_type   此陣列元素描述的段的型別,或者如何解釋此陣列元素的資訊。
     * p_offset 此成員給出從檔案頭到該段第一個位元組的偏移。
     * p_vaddr  此成員給出段的第一個位元組將被放到記憶體中的虛擬地址。
     * p_paddr  此成員僅用於與實體地址相關的系統中。
     * 因為System V 忽略所有應用程式的實體地址資訊,此欄位對與可執行檔案和共享目標檔案而言具體內容是未指定的。
     * p_filesz 此成員給出段在檔案映像中所佔的位元組數。可以為0。
     * p_memsz  此成員給出段在記憶體映像中佔用的位元組數。可以為0。
     * p_flags  此成員給出與段相關的標誌。
     * p_align  可載入的程式段的 p_vaddr 和 p_offset 取值必須合適,相對於對頁面大小的取模而言。
     * 此成員給出段在檔案中和記憶體中如何 對齊。數值 0 和 1 表示不需要對齊。
     * 否則 p_align 應該是個 正整數,並且是 2 的冪次數,p_vaddr 和 p_offset 對 p_align 取模後應該相等。
     */
    public static class Elf32_phdr {
        public byte[] p_type = new byte[ELF32define.Elf32_Word];
        public byte[] p_offset = new byte[ELF32define.Elf32_Off];
        public byte[] p_vaddr = new byte[ELF32define.Elf32_Addr];
        public byte[] p_paddr = new byte[ELF32define.Elf32_Addr];
        public byte[] p_filesz = new byte[ELF32define.Elf32_Word];
        public byte[] p_memsz = new byte[ELF32define.Elf32_Word];
        public byte[] p_flags = new byte[ELF32define.Elf32_Word];
        public byte[] p_align = new byte[ELF32define.Elf32_Word];
從檔案頭部得到偏移為52,數量為8
public static void main(String[] args) {
            String path = "C:\\Users\\悅\\Desktop\\123.so";
            try {
                FileInputStream inputStream = new FileInputStream(path);
                try {
                    inputStream.skip(52);
                    for (int i = 0; i < 8; i++) {
                        ELFType32.Elf32_phdr elf32_sym = new ELFType32.Elf32_phdr(inputStream);
                        System.out.println("           " + i);
                        System.out.println(elf32_sym);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
結果如下:

           0
{型別=6}
{檔案頭到該段第一個位元組的偏移=52}
{段的第一個位元組將被放到記憶體中的虛擬地址=52}
{實體地址=52}
{檔案映像中所佔的位元組數=256}
{記憶體映像中佔用的位元組數=256}
{與段相關的標誌=4}
{段在檔案中和記憶體中如何對齊=4}

4.最後

1.010Edtitor最右面的檢查器可以檢視這個位元組或者位元組陣列的10進位制,16進位制等不同結果

2.滕啟明的ELF檔案格式分析距今已經15年了,內容相較於現在使用的規範不免少了一些內容。當年ELF檔案規範裡的保留位現在都用上了不少,如果你用readelf命令讀取的話,就會會發現多了好一些新名詞,而這些是PDF裡沒有的。

原始碼下載

相關文章