1. 程式人生 > >從Android裝置中提取核心和逆向分析

從Android裝置中提取核心和逆向分析

一、手機裝置環境

Model number: Nexus 5
OS Version: Android 4.4.4 KTU84P
Kernel Version: 3.4.0-gd59db4e


二、Android核心提取

adb shell
su
cd /dev/block/platform/msm_sdcc.1/by-name
ls -l boot


boot 是個系統符號軟連結,/dev/block/mmcblk0p19 就是boot分割槽

用 dd 將其dump到Nexus 5手機的sdcard資料夾下:

dd if=/dev/block/mmcblk0p19 of=/sdcard/boot.img
adb pull 將dump出來的boot.img檔案匯出到 /home/androidcode/AndroidDevlop/Nexus5Boot 資料夾下
adb pull /sdcard/boot.img /home/androidcode/AndroidDevlop/Nexus5Boot

用 Binwalk 工具分析boot.img檔案

安裝Binwalk工具並分析boot.img檔案

cd /home/androidcode/AndroidDevlop/Nexus5Boot/binwalk-master

# 按照binwalk工具的說明安裝binwalk
sudo python setup.py install

# 分析boot.img檔案
sudo binwalk ../boot.img >log
分析的結果截圖:



boot.img檔案跳過2k的檔案頭之後,包括有兩個gz包,一個是boot.img-kernel.gz即Linux核心,一個是boot.img-ramdisk.cpio.gz,
大概的組成結構如下圖,詳細的資訊可以參考Android原始碼的 android/platform/system/core/master/mkbootimg/bootimg.h

檔案,線上檢視 booting.h 檔案地址:https://android.googlesource.com/platform/system/core/+/master/mkbootimg/bootimg.h

/* tools/mkbootimg/bootimg.h
**
** Copyright 2007, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License"); 
** you may not use this file except in compliance with the License. 
** You may obtain a copy of the License at 
**
**     http://www.apache.org/licenses/LICENSE-2.0 
**
** Unless required by applicable law or agreed to in writing, software 
** distributed under the License is distributed on an "AS IS" BASIS, 
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
** See the License for the specific language governing permissions and 
** limitations under the License.
*/
#include <stdint.h>
#ifndef _BOOT_IMAGE_H_
#define _BOOT_IMAGE_H_
typedef struct boot_img_hdr boot_img_hdr;
#define BOOT_MAGIC "ANDROID!"
#define BOOT_MAGIC_SIZE 8
#define BOOT_NAME_SIZE 16
#define BOOT_ARGS_SIZE 512
#define BOOT_EXTRA_ARGS_SIZE 1024
struct boot_img_hdr
{
    uint8_t magic[BOOT_MAGIC_SIZE];
    uint32_t kernel_size;  /* size in bytes */
    uint32_t kernel_addr;  /* physical load addr */
    uint32_t ramdisk_size; /* size in bytes */
    uint32_t ramdisk_addr; /* physical load addr */
    uint32_t second_size;  /* size in bytes */
    uint32_t second_addr;  /* physical load addr */
    uint32_t tags_addr;    /* physical addr for kernel tags */
    uint32_t page_size;    /* flash page size we assume */
    uint32_t unused;       /* reserved for future expansion: MUST be 0 */
    /* operating system version and security patch level; for
     * version "A.B.C" and patch level "Y-M-D":
     * ver = A << 14 | B << 7 | C         (7 bits for each of A, B, C)
     * lvl = ((Y - 2000) & 127) << 4 | M  (7 bits for Y, 4 bits for M)
     * os_version = ver << 11 | lvl */
    uint32_t os_version;
    uint8_t name[BOOT_NAME_SIZE]; /* asciiz product name */
    uint8_t cmdline[BOOT_ARGS_SIZE];
    uint32_t id[8]; /* timestamp / checksum / sha1 / etc */
    /* Supplemental command line data; kept here to maintain
     * binary compatibility with older versions of mkbootimg */
    uint8_t extra_cmdline[BOOT_EXTRA_ARGS_SIZE];
} __attribute__((packed));
/*
** +-----------------+ 
** | boot header     | 1 page
** +-----------------+
** | kernel          | n pages  
** +-----------------+
** | ramdisk         | m pages  
** +-----------------+
** | second stage    | o pages
** +-----------------+
**
** n = (kernel_size + page_size - 1) / page_size
** m = (ramdisk_size + page_size - 1) / page_size
** o = (second_size + page_size - 1) / page_size
**
** 0. all entities are page_size aligned in flash
** 1. kernel and ramdisk are required (size != 0)
** 2. second is optional (second_size == 0 -> no second)
** 3. load each element (kernel, ramdisk, second) at
**    the specified physical address (kernel_addr, etc)
** 4. prepare tags at tag_addr.  kernel_args[] is
**    appended to the kernel commandline in the tags.
** 5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr
** 6. if second_size != 0: jump to second_addr
**    else: jump to kernel_addr
*/
#if 0
typedef struct ptentry ptentry;
struct ptentry {
    char name[16];      /* asciiz partition name    */
    unsigned start;     /* starting block number    */
    unsigned length;    /* length in blocks         */
    unsigned flags;     /* set to zero              */
};
/* MSM Partition Table ATAG
**
** length: 2 + 7 * n
** atag:   0x4d534d70
**         <ptentry> x n
*/
#endif
#endif

#!/usr/bin/env python
# Copyright 2015, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import print_function
from sys import argv, exit, stderr
from argparse import ArgumentParser, FileType, Action
from os import fstat
from struct import pack
from hashlib import sha1
import sys
import re
def filesize(f):
    if f is None:
        return 0
    try:
        return fstat(f.fileno()).st_size
    except OSError:
        return 0
def update_sha(sha, f):
    if f:
        sha.update(f.read())
        f.seek(0)
        sha.update(pack('I', filesize(f)))
    else:
        sha.update(pack('I', 0))
def pad_file(f, padding):
    pad = (padding - (f.tell() & (padding - 1))) & (padding - 1)
    f.write(pack(str(pad) + 'x'))
def write_header(args):
    BOOT_MAGIC = 'ANDROID!'.encode()
    args.output.write(pack('8s', BOOT_MAGIC))
    args.output.write(pack('10I',
        filesize(args.kernel),                          # size in bytes
        args.base + args.kernel_offset,                 # physical load addr
        filesize(args.ramdisk),                         # size in bytes
        args.base + args.ramdisk_offset,                # physical load addr
        filesize(args.second),                          # size in bytes
        args.base + args.second_offset,                 # physical load addr
        args.base + args.tags_offset,                   # physical addr for kernel tags
        args.pagesize,                                  # flash page size we assume
        0,                                              # future expansion: MUST be 0
        (args.os_version << 11) | args.os_patch_level)) # os version and patch level
    args.output.write(pack('16s', args.board.encode())) # asciiz product name
    args.output.write(pack('512s', args.cmdline[:512].encode()))
    sha = sha1()
    update_sha(sha, args.kernel)
    update_sha(sha, args.ramdisk)
    update_sha(sha, args.second)
    img_id = pack('32s', sha.digest())
    args.output.write(img_id)
    args.output.write(pack('1024s', args.cmdline[512:].encode()))
    pad_file(args.output, args.pagesize)
    return img_id
class ValidateStrLenAction(Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        if 'maxlen' not in kwargs:
            raise ValueError('maxlen must be set')
        self.maxlen = int(kwargs['maxlen'])
        del kwargs['maxlen']
        super(ValidateStrLenAction, self).__init__(option_strings, dest, **kwargs)
    def __call__(self, parser, namespace, values, option_string=None):
        if len(values) > self.maxlen:
            raise ValueError('String argument too long: max {0:d}, got {1:d}'.
                format(self.maxlen, len(values)))
        setattr(namespace, self.dest, values)
def write_padded_file(f_out, f_in, padding):
    if f_in is None:
        return
    f_out.write(f_in.read())
    pad_file(f_out, padding)
def parse_int(x):
    return int(x, 0)
def parse_os_version(x):
    match = re.search(r'^(\d{1,3})(?:\.(\d{1,3})(?:\.(\d{1,3}))?)?', x)
    if match:
        a = int(match.group(1))
        b = c = 0
        if match.lastindex >= 2:
            b = int(match.group(2))
        if match.lastindex == 3:
            c = int(match.group(3))
        # 7 bits allocated for each field
        assert a < 128
        assert b < 128
        assert c < 128
        return (a << 14) | (b << 7) | c
    return 0
def parse_os_patch_level(x):
    match = re.search(r'^(\d{4})-(\d{2})-(\d{2})', x)
    if match:
        y = int(match.group(1)) - 2000
        m = int(match.group(2))
        # 7 bits allocated for the year, 4 bits for the month
        assert y >= 0 and y < 128
        assert m > 0 and m <= 12
        return (y << 4) | m
    return 0
def parse_cmdline():
    parser = ArgumentParser()
    parser.add_argument('--kernel', help='path to the kernel', type=FileType('rb'),
                        required=True)
    parser.add_argument('--ramdisk', help='path to the ramdisk', type=FileType('rb'))
    parser.add_argument('--second', help='path to the 2nd bootloader', type=FileType('rb'))
    parser.add_argument('--cmdline', help='extra arguments to be passed on the '
                        'kernel command line', default='', action=ValidateStrLenAction, maxlen=1536)
    parser.add_argument('--base', help='base address', type=parse_int, default=0x10000000)
    parser.add_argument('--kernel_offset', help='kernel offset', type=parse_int, default=0x00008000)
    parser.add_argument('--ramdisk_offset', help='ramdisk offset', type=parse_int, default=0x01000000)
    parser.add_argument('--second_offset', help='2nd bootloader offset', type=parse_int,
                        default=0x00f00000)
    parser.add_argument('--os_version', help='operating system version', type=parse_os_version,
                        default=0)
    parser.add_argument('--os_patch_level', help='operating system patch level',
                        type=parse_os_patch_level, default=0)
    parser.add_argument('--tags_offset', help='tags offset', type=parse_int, default=0x00000100)
    parser.add_argument('--board', help='board name', default='', action=ValidateStrLenAction,
                        maxlen=16)
    parser.add_argument('--pagesize', help='page size', type=parse_int,
                        choices=[2**i for i in range(11,15)], default=2048)
    parser.add_argument('--id', help='print the image ID on standard output',
                        action='store_true')
    parser.add_argument('-o', '--output', help='output file name', type=FileType('wb'),
                        required=True)
    return parser.parse_args()
def write_data(args):
    write_padded_file(args.output, args.kernel, args.pagesize)
    write_padded_file(args.output, args.ramdisk, args.pagesize)
    write_padded_file(args.output, args.second, args.pagesize)
def main():
    args = parse_cmdline()
    img_id = write_header(args)
    write_data(args)
    if args.id:
        if isinstance(img_id, str):
            # Python 2's struct.pack returns a string, but py3 returns bytes.
            img_id = [ord(x) for x in img_id]
        print('0x' + ''.join('{:02x}'.format(c) for c in img_id))
if __name__ == '__main__':
    main()

根據上面的資訊,從boot.img中提取出壓縮的核心檔案:

cd ../

dd if=boot.img of=kernel.gz bs=1 skip=20660


由於Android的核心檔案經過了gzip壓縮,因此要拿到最終的Android核心檔案還需要進行解壓縮:

gzip -d kernel.gz


補充說明:關於Android的核心檔案的提取和解壓方法很多,常用的工具也比較多,也可以使用下面的幾個工具之一來進行boot.img檔案的解包和gzip的解壓縮操作。

bootimg.exe	https://github.com/cofface/android_bootimg 

bootimg-tools   https://github.com/pbatard/bootimg-tools.git  

unpackbootimg	http://bbs.pediy.com/showthread.php?t=197334

abootimg	https://github.com/ggrandou/abootimg

解壓後的Android核心檔案kernel中不包含符號資訊。所以還要從Android裝置中提取符號資訊,儘管 /proc/kallsyms 檔案中儲存了所有核心符號資訊,但是從分析的結果來看,檔案中儲存的記憶體地址值都是0,這是為了防止核心地址洩露。在dump 映象檔案boot.img的Android裝置上執行下面的命令,就會發現Android裝置上的所有核心符號都被遮蔽隱藏了。

adb shell

cat /proc/kallsyms


為了要獲取Android核心中所有的核心符號資訊,可以通過在root許可權下,修改Andriod裝置中的/proc/sys/kernel/kptr_restrict的值來實現,去掉Android核心符號的資訊遮蔽。

adb shell
su

# 檢視預設值
cat /proc/sys/kernel/kptr_restrict

# 關閉核心符號遮蔽
echo 0 > /proc/sys/kernel/kptr_restrict 

# 檢視修改後的值
cat /proc/sys/kernel/kptr_restrict

cat /proc/kallsyms


關閉Android裝置的核心符號的遮蔽以後,再次執行 cat /proc/kallsyms ,發現被隱藏的核心符號資訊都顯示出來了。

在root許可權下,將Android裝置中的核心符號資訊dump出來,匯出到 /home/androidcode/AndroidDevlop/Nexus5Boot/syms.txt檔案中。因此,Android核心檔案的核心符號資訊都儲存在syms.txt檔案中了。

# cat /proc/kallsyms > /sdcard/syms.txt

# exit
$ exit

$ adb pull /sdcard/syms.txt syms.txt


三、IDA分析匯出的Androd核心檔案

將提取出來的Android核心 kernel檔案 拖到IDA Pro 6.8中進行分析,設定處理器型別ARM Little-endian

 

ROM start addressLoading address 處填上0xc0008000,然後點選 OK 完成 。


*至於這裡為什麼要設定 ROM start address 和 Loading address的地址為 0xc0008000? 具體的可以參考 bootheader這個資料結構,在這裡需要關注其中幾個比較重要的值,這些值定義在boot/boardconfig.h中,不同的晶片對應vendor下不同的boardconfig,在這裡我們的值分別是(分別是 kernel/ramdis/tags 載入ram的實體地址):

#define PHYSICAL_DRAM_BASE   0x00200000 
#define KERNEL_ADDR          (PHYSICAL_DRAM_BASE + 0x00008000)
#define RAMDISK_ADDR         (PHYSICAL_DRAM_BASE + 0x01000000)
#define TAGS_ADDR            (PHYSICAL_DRAM_BASE + 0x00000100)
#define NEWTAGS_ADDR         (PHYSICAL_DRAM_BASE + 0x00004000)
上面這些值分中 KERNEL_ADDR 就是 ZTEXTADDR,RAMDISK_ADDR 就是 INITRD_PHYS,而 TAGS_ADDR 就是PARAMS_PHYS。bootloader會從boot.img的分割槽中將kernel和ramdisk分別讀入RAM上面定義的記憶體地址中,然後就會跳到ZTEXTADDR開始執行。

OK,現在就可以在IDA中檢視和分析Android核心檔案中的程式碼了,但是函式名稱不是很友好,很多系統函式的名稱都沒有顯示出來,只是顯示成IDA中預設的普通函式名稱。


前面我們已經將Androd核心檔案中的核心符號資訊都dump出來,這裡大有用武之地。因此,向IDA中匯入之前提取出來的核心符號資訊就可以看到對應的函式名稱了。需要用到下面的python指令碼:

ksyms = open("C:\Users\Fly2016\Desktop\Binwalk工具\Nexus5_kernel\syms.txt")
for line in ksyms:
    addr = int(line[0:8],16)
    name = line[11:]
    idaapi.set_debug_name(addr,name)
    MakeNameEx(addr,name,SN_NOWARN)
    Message("%08X:%sn"%(addr,name))

在IDA的 File->Script Command中執行上述python指令碼,之後就可以在IDA中成功新增核心符號資訊使IDA顯示出正確的系統呼叫的函式名稱來。


大功告成,現在可以愉快的分析Android核心的程式碼了:


總結:通過這種dump裝置韌體的方法,可以逆向分析沒有原始碼的韌體二進位制檔案,對於Android裝置來說又可以通過這種方法修改Android的核心檔案來進行反除錯或者其他的目的。將修改好的Android核心檔案使用boot.img等打包解包工具還原打包回到boot.img檔案中,然後 fastboot flash boot boot.img  更新Android裝置的核心檔案即可達到目的。

學習連結