---
# System prepended metadata

title: Reversing /sbin/init
tags: [' ARM', firmadyne]

---

---
title: Reversing /sbin/init
tags: firmadyne, ARM
lang: zh_tw
---

# Reversing /sbin/init
[TOC]

# 前言
本篇嘗試 Reverse 各種 firmware 裡的 filesystem 的 /sbin/init

根據[註3](https://blog.csdn.net/u013275111/article/details/56666940)
> init进程是由内核启动的第一个也是惟一的一个用户进程，它根据配置文件决定启动哪些程序，比如执行某些脚本，启动shell，运行用户指定的程序等。

以下會先以指令架構分類，再以 firmware 型號細分

下面會盡量精簡內容，故一些詳細 Debug 過程會陸陸續續放到不同筆記
使得這篇筆記是相對精簡的，其他筆記放瑣碎過程


# ARM
## R6400-V1.0.0.14_1.0.8
:::info
- 檔案名稱
    R6400-V1.0.0.14_1.0.8.ZIP
- MD5 hash
    723a253bca512068abda65f25a5e42f1
- 廠牌
    Netgear
- Reversing /sbin/init 結果
    未分析完畢
:::

### 過程記錄
首先[下載 firmware](http://www.downloads.netgear.com/files/GDC/R6400/R6400-V1.0.0.14_1.0.8.zip)

### --- Firmadyne 步驟
以下是跑 firmadyne 官方示範步驟

1. Extract 出 FS
    ```
    ./sources/extractor/extractor.py -b Netgear -sql 127.0.0.1 -np -nk "firmwares/R6400-V1.0.0.14_1.0.8.zip" images
    ```

2. 將 ${IID} 改為正確編號，此例是 7
    ```
    IID=7
    ```

3. 獲取 arch、endian 資訊，
    ```
    ./scripts/getArch.sh ./images/${IID}.tar.gz
    ```

4. INSERT 資訊到 tabl object、object_to_image
    ```
    ./scripts/tar2db.py -i ${IID} -f ./images/${IID}.tar.gz
    ```

5. 建立 scrach/${IID}/ 
    ```
    sudo ./scripts/makeImage.sh ${IID}
    ```

6. 試圖獲取 IP
    ```
    sudo ./scripts/inferNetwork.sh ${IID}
    ```
    
### --- 取得 /sbin/init
將 FS 掛載起來
```
sudo ./scripts/mount.sh ${IID}
```

查看有無 /sbin/init
```
ls -al scratch/${IID}/image/sbin | grep init
```
輸出如下
```
lrwxrwxrwx  1 ljp ljp     12 May  6  2015 acos_init -> acos_service
lrwxrwxrwx  1 ljp ljp      2 May  6  2015 init -> rc
lrwxrwxrwx  1 ljp ljp      2 May  6  2015 preinit -> rc
```

rc? 查看一下
```
ls -al scratch/${IID}/image/sbin | grep rc
```

輸出如下
```
lrwxrwxrwx  1 ljp ljp      2 May  6  2015 erase -> rc
lrwxrwxrwx  1 ljp ljp      2 May  6  2015 hotplug -> rc
lrwxrwxrwx  1 ljp ljp      2 May  6  2015 init -> rc
lrwxrwxrwx  1 ljp ljp      2 May  6  2015 preinit -> rc
-rwxr-xr-x  1 ljp ljp 101956 May  6  2015 rc
lrwxrwxrwx  1 ljp ljp      2 May  6  2015 write -> rc
lrwxrwxrwx  1 ljp ljp      2 May  6  2015 write_cfe -> rc
```

### --- kernel zImage bug
詳細過程在 [kernel zImage bug](/sVWFoRFmSgeZVOk2ehw-3Q?view)

實際解決這個問題的筆記是 [Make ARM Kernel run successfully](/6MDrgwsGTbaGOeWARxbqHQ?view)

### --- back to analysis rc
這一篇幅也過長，以每周做的分析，來切分筆記
- [Back to analysis rc - 1](/PvEXTp5TTJmpgE5xwBA6Ig?view)
    - 學到一些 ARM 的語法
        - condition ldr
        - movw & movt
    - 了解到 rc 是個 switch case 的程式
        - 根據 link 到 rc 的 link name 判斷要進哪個 case
        - case 總共有:
            - preinit
            - rc 
            - erase
            - write
            - hotplug
    - 自己改編 kernel，使 kernel 會 call /sbin/preinit
        - /sbin/preinit 為 link, link to rc
        - 如此就會使 rc 執行 preinit 這 case 的 code
    - 發現 preinit 中 0xB074 位址處有個迴圈(就稱他 0xB074 loop)跑超久
- [Back to analysis rc - 2](/lUw41ugKRSS4wpmo8ccicg?view)
    - 再度用 ubuntu 18.04 測，發現 0xB074 loop 這次就又沒跑這麼久
    - 因為沒 output的緣故，改用 ubuntu 16.04
    - 用原版 kernel 跑
        - 有 output，且 kernel panic
        - 沒進 rc
    - 用加了執行 preinit 的 kernel 跑
        - 沒 output，且持續 running
        - 的確有進入 rc preinit case
    - 以 ubuntu 14.04 編譯 kernel-v4.1，並以 ubuntu 16.04 模擬
        - 成功有 output
        - kernel 加了執行 preinit，成功有 output

## DIR-890L 1.09.B14_WW
:::info
- 檔案名稱
    DIR-890L_REVA_FIRMWARE_1.09.B14_WW.ZIP
- MD5 hash
    8481918d2a855a925801b7d9c997b4c5
- 廠牌
    D-Link
- Reversing /sbin/init 結果
    未分析完畢
:::

### 過程記錄
首先[下載 firmware](ftp://ftp2.dlink.com/PRODUCTS/DIR-890L/REVA/DIR-890L_REVA_FIRMWARE_1.09.B14_WW.ZIP)

### --- Firmadyne 步驟
以下是跑 firmadyne 官方示範步驟

1. Extract 出 FS
    ```
    ./sources/extractor/extractor.py -b Dlink -sql 127.0.0.1 -np -nk "firmwares/DIR-890L_REVA_FIRMWARE_1.09.B14_WW.ZIP" images
    ```

2. 將 ${IID} 改為正確編號，此例是 6
    ```
    IID=6
    ```

3. 獲取 arch、endian 資訊，
    ```
    ./scripts/getArch.sh ./images/${IID}.tar.gz
    ```

4. INSERT 資訊到 tabl object、object_to_image
    ```
    ./scripts/tar2db.py -i ${IID} -f ./images/${IID}.tar.gz
    ```

5. 建立 scrach/${IID}/ 
    ```
    sudo ./scripts/makeImage.sh ${IID}
    ```

6. 試圖獲取 IP
    ```
    ./scripts/inferNetwork.sh ${IID}
    ```

### --- 取得 /sbin/init
將 FS 掛載起來
```
sudo ./scripts/mount.sh ${IID}
```

查看有無 /sbin/init
```
ls -al scratch/6/image/sbin | grep init
```
輸出如下
```
lrwxrwxrwx  1 ljp ljp      14 Jan 29  2016 init -> ../bin/busybox
```

是一個 symbolic link
查看有無 /bin/busybox
```
ls -al scratch/6/image/bin | grep busybox
```
輸出如下
```
...
lrwxrwxrwx  1 ljp ljp      7 Jan 29  2016 ash -> busybox
-rwxr-xr-x  1 ljp ljp 585020 Jan 29  2016 busybox
lrwxrwxrwx  1 ljp ljp      7 Jan 29  2016 cat -> busybox
...
```
發現有一堆 symbolic link 都是到 busybox

### --- reverse busybox

根據 busybox 的 wiki 寫的 :
> Busybox在單一的可執行檔中提供了精簡的Unix工具集
> 更常見的作法是，這些指令會以連結（使用硬連結或者符號連結）至BusyBox可執行檔，BusyBox會偵測其被連結時的名稱，並執行對應的指令。舉例來說，只要將/bin/ls連結到/bin/busybox，即可執行 /bin/ls
> 

從 wiki 下方的 reference 找到了 [busybox 提供的功能表](https://www.busybox.net/downloads/BusyBox.html)
有一個 init，但就只有寫著
> Init is the parent of all processes

查了許多文後，參考 [busybox 官方關於 init 的 source code](https://github.com/mirror/busybox/blob/master/init/init.c) 和 [註4文章](https://www.cnblogs.com/CrazyCatJack/p/6184564.html)
init 的進入點為 init_main
init_main 簡潔版為
```c=
int init_main(int argc UNUSED_PARAM, char **argv)
{
    if (argv[1]
        && (strcmp(argv[1], "single") == 0
        || strcmp(argv[1], "-s") == 0
        || LONE_CHAR(argv[1], '1'))
    )
    {
        new_init_action(RESPAWN, bb_default_login_shell, "");
    } 
    else
    {
        parse_inittab();
    }

    run_actions(SYSINIT);
    run_actions(WAIT);
    run_actions(ONCE);
    while (1)
    {
        run_actions(RESPAWN | ASKFIRST);
        while(1)
        {
            wpid = waitpid(-1, NULL, maybe_WNOHANG);
            if (wpid <= 0)
                break;
            struct init_action a = mark_terminated(wpid);
        }
    }
}
```

##### --- parse_inittab

先看一下簡潔版 parse_inittab()
```c=
#define INIT_SCRIPT "/etc/init.d/rcS"

static void parse_inittab(void)
{
    parser_t *parser = config_open2("/etc/inittab", fopen_for_read);
    if (parser == NULL)
    {
        new_init_action(SYSINIT, INIT_SCRIPT, "");
        new_init_action(ASKFIRST, bb_default_login_shell, "");
        new_init_action(ASKFIRST, bb_default_login_shell, VC_2);
        new_init_action(ASKFIRST, bb_default_login_shell, VC_3);
        new_init_action(ASKFIRST, bb_default_login_shell, VC_4);
        new_init_action(CTRLALTDEL, "reboot", "");
        new_init_action(SHUTDOWN, "umount -a -r", "");
        new_init_action(SHUTDOWN, "swapoff -a", "");
        new_init_action(RESTART, "init", "");
        return;
    }
    while (config_read(..........))
    {
        // Resolving parser file
        ...
        new_init_action(1 << action, token[3], tty);
        ...
    }
    config_close(parser);
}
```
我的理解是，若存在 /etc/inittab 就解析這個檔案 (Line 19 ~ 25)，這個 /etc/inittab 設定檔內容格式長得像這樣(從網路上抓的範例)
```
# inittab for linux
# id:runlevels:action:process
id:1:initdefault:
rc::bootwait:/etc/rc
1:1:respawn:/etc/getty 9600 tty1
2:1:respawn:/etc/getty 9600 tty2
3:1:respawn:/etc/getty 9600 tty3
4:1:respawn:/etc/getty 9600 tty4
```
像是此範例的 `1:1:respawn:/etc/getty 9600 tty1`
最終解析後，會呼叫 `new_init_action(RESPAWN, "/etc/getty 9600 tty1", tty1);`

若沒有/etc/inittab，Line 6 ~ 15 寫了預設要做的事

### --- new_init_action
總之，不管存不存在 /etc/inittab，都會用到 new_init_action()，根據[註4](https://www.cnblogs.com/CrazyCatJack/p/6184564.html)
> 这里涉及到了一个函数 new_init_action 。它实际上的工作就是把各个程序的执行时机、命令行、控制台参数分别赋值给结构体，并把这些结构体组成一个单链表。这也就是我们所说的配置。
> 
> 这三个参数不正是inittab配置文件中的配置命令吗？他们分别对应于 \<action>、\<process>、\<id>。按照\<id>:\<runlevels>:\<action>: \<process>的顺序将参数填充进去不就是我们需要的默认配置文件吗\^\_\^
>

來看看 new_init_action()

```c=
// 將創一個新的 init_action 加入到 singly linked list G.init_action_list
static void new_init_action(uint8_t action_type, const char *command, const char *cons)
{
    struct init_action *a, **nextp;

    nextp = &G.init_action_list;
    
    // 遍尋 G.init_action_list
    while ((a = *nextp) != NULL)
    {
        // 如果 command 跟 terminal 都一樣
        if (strcmp(a->command, command) == 0
         && strcmp(a->terminal, cons) == 0 )
        {
            // 將 a 脫離 linked list 
            *nextp = a->next;
            // 到 linked list 最尾端
            while (*nextp != NULL)
                nextp = &(*nextp)->next;
            a->next = NULL;
            goto append;
        }
        nextp = &a->next;
    }

    // 若都沒有重複的 init_action，分配一個新的 init_action
    a = xzalloc(sizeof(*a) + strlen(command));

append:
    // 若是有重複的狀況: 將重複的 init_action 重新加回 linked list 最尾端
    *nextp = a;
    a->action_type = action_type;
    strcpy(a->command, command);
    safe_strncpy(a->terminal, cons, sizeof(a->terminal));
    dbg_message(L_LOG | L_CONSOLE, "command='%s' action=%x tty='%s'\n",
        a->command, a->action_type, a->terminal);
}
```

### --- run_actions

回頭來看 init_main() -> run_actions()

```c=
static void run_actions(int action_type)
{
    struct init_action *a;

    for (a = G.init_action_list; a; a = a->next) {
        // 如果不是 action_type 就跳過
        if (!(a->action_type & action_type))
            continue;

        if (a->action_type & (SYSINIT | WAIT | ONCE | CTRLALTDEL | SHUTDOWN)) {
            pid_t pid = run(a);
            // 只有 action_type 是 ONCE 的不等之執行完畢
            if (a->action_type & (SYSINIT | WAIT | CTRLALTDEL | SHUTDOWN))
                waitfor(pid);
        }
        if (a->action_type & (RESPAWN | ASKFIRST)) {
            /* Only run stuff with pid == 0. If pid != 0,
             * it is already running
             */
            if (a->pid == 0)
                a->pid = run(a);
        }
    }
}
```

### --- run

而 run() 是先 fork 出 child，再讓 child call init_exec()
```c=
static pid_t run(const struct init_action *a)
{
    pid_t pid;
    if (BB_MMU && (a->action_type & ASKFIRST))
        pid = fork();
    else
        pid = vfork();
    
    if (pid < 0)
        message(L_LOG | L_CONSOLE, "can't fork");
    if (pid) {
        sigprocmask_allsigs(SIG_UNBLOCK);
        return pid; /* Parent or error */
    }
    
    /* Child */
    
    ...
    init_exec(a->command);
    ...
}
```

### --- init_exec

init_exec() 最後 call execvp 來執行 command

[include/libbb.h](https://github.com/mirror/busybox/blob/b6946084296525f0e2aa3e72df7b896cc98197d7/include/libbb.h#L1128) : 
```c=
#define BB_EXECVP(prog,cmd)     execvp(prog,cmd)
```

```c=
#include "libbb.h"

static void init_exec(const char *command)
{
    ...
    BB_EXECVP(command, cmd);
    ...
}
```

### --- 統整

小小的統整，busybox 的 init 是會執行設定在 /etc/inittab 中的其他程式
這些程式有分 action，分成
- SYSINIT
- WAIT
- ONCE
- RESPAWN
- ASKFIRST
...

這些 action 的差別分別是執行優先順序、是否要等待此程式執行完、執行時機點
若沒有 /etc/inittab 設定檔的話，還是有預設設定檔，且直接寫死在 code 中

### 實際分析此 filesystem 裡面的 busybox

但搞不好這 fs 的 busybox 是有改過的
為了驗證真的有做這些事，來 Reverse 一下 busybox

mount 起來並移動到 image 目錄中
```
sudo ./scripts/mount.sh ${IID}
cd scratch/${IID}/image
```

查看 busybox
```
file bin/busybox
```
輸出如下
```
bin/busybox: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, stripped
```

用 radare2 打開
```
r2 -a arm bin/busybox
```
![](https://i.imgur.com/wf4NaGi.png)
看來 entry 是 0x00008710

```
# 先分析一下
[0x00008710]> aa
# 可以看到程式流程圖
[0x00008710]> VV
```
![](https://i.imgur.com/BdXL0EH.jpg)

這支程式沒有 debugging symbol
無法直接用 s 跳到 init_main

我的想法是利用程式有用到的字串
像是在 init_main() 後面的 code 中有一段
```c=
if (a) {
    message(L_LOG, "process '%s' (pid %d) exited. "
            "Scheduling for restart.",
            a->command, wpid);
}
```
`"process '%s' (pid %d) exited. Scheduling for restart."` 這個字串
找到這個字串的記憶體位址後
想辦法找到使用到這個記憶體位址的程式碼
八成就找到對應這段 C code 的 assembly code

### --- find string reference

在 radare2 有看到 `str.process___s___pid__d__exited._Scheduling_for_restart.` 

```
[0x00008710]> s str.process___s___pid__d__exited._Scheduling_for_restart. 
[0x000954cc]>
```
得到這字串位址為 0x000954cc

參考[註6]，這邊有另一個找此字串位置的方式(https://reverseengineering.stackexchange.com/questions/11597/find-reference-to-string-in-radare2)，r2 中有一個指令 iz
![](https://i.imgur.com/TFywEBp.png)

iz 會秀出所有在 data sections 的字串
![](https://i.imgur.com/7Q2NHUv.png)

搭配 `~` 可以篩選感興趣的部分，有點像 grep
![](https://i.imgur.com/c2gzf7Q.png)

找到字串 `process '%s' (pid %d) exited. Scheduling for restart.` 的 Vaddr 是 0x000954cc

接下來找找有哪邊有用到 0x000954cc 這個地址
![](https://i.imgur.com/smeoVWG.png)
用 `/c` 這指令找不到

後來查到 `ax` 這系列的指令
![](https://i.imgur.com/2WfiuVD.png)
但也沒派上用場

此時我在想這些指令真的是這樣用嗎
為了測試，我自己寫了簡單的 C code
用 r2 打開，執行了分析 `aa` 後，再用以上提到的指令
證明這些指令是可行的

下了這個指令後完全沒輸出，更直接宣判找哪邊有用到字串這招沒用
```
[0x00008710]> ax~str
```
因為這支程式根本沒有引用字串(下這個指令而沒有輸出，可以這樣解釋)

真的是這樣嗎，還是 ARM 架構有什麼東西不太一樣?

### --- fail to find string reference ?

![](https://i.imgur.com/afJcTNO.png)
看來不是沒有引用字串

### --- Some details of ARM

中間小小的岔開 為何 `0x0008c10: ldr r0, [pc, 0x58]` 後半段最後算完是 `[0x8c70]` 勒
照 x86 邏輯來看，fetch 了 0x8c10 的指令後，pc 加了此指令長度，也就是 4，指向下一個指令
所以 pc = 0x8c10 + 4 = 0x8c14，所以 [pc, 0x58] 應該要等於 pc + 0x58 = 0x8c14 + 0x58 = 0x8c6c 才對

而根據[註7的文章](https://stackoverflow.com/questions/24091566/why-does-the-arm-pc-register-point-to-the-instruction-after-the-next-one-to-be-e)，簡單來說這邊需要一點計算機組織的概念
> The original ARM design had a 3-stage pipeline (fetch-decode-execute).

所以真正使用到 pc 的值時，已經過了 2 cycles，pc 已 +4 (指令長度) * 2 (cycles)

### --- Hello World of ARM

細看 main 之前，我想先自己寫一個簡單的 C
編成 arm elf32，並且觀察一下

先安裝 Cross-compiler
```
sudo apt-get install gcc-arm-linux-gnueabi
```

temp.c : 
```c=
#include <stdio.h>

void hello(char *str)                                                            
{
    printf("%s", str);
}

int main()
{
    char str[] = "Hello Ni Ho!\n";
    hello(str);
    return 0;
}
```

Compile
```
arm-linux-gnueabi-gcc temp.c -o temp
```

檢查一下
```
file temp
```
輸出如下
```
temp: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 3.2.0, BuildID[sha1]=dbb3f228c867940c16ec180ffbe2a47500613394, not stripped
```

用 r2 觀察 main:
![](https://i.imgur.com/ocfXZGO.jpg)

看來 arm 的 bl 跟 x86 的 call 應該是差不多的東東

hello:
![](https://i.imgur.com/WpC7qYU.png)

![](https://i.imgur.com/t7LTs1g.png)
這邊發現幾個 arm 的特性
- call function 是用 bl 指令(Branch with Link)
    - 參數放在 r0, r1 ...

- 取用字串時，像是 hello 中的 "%s"
    "%s" 實際的字串會存在一個地址，先稱做 A
    會另有一個地址，稱做 B，存放著 A
    組語中會寫 ldr rx, [B]，將 rx 的值改為 A
    機制跟 x86 不論是 32bits 或 64bits 都不一樣

爬了一下文，根據[Calling convention wiki](https://en.wikipedia.org/wiki/Calling_convention#ARM_(A32))、[Procedure Call Standard for the ARM® Architecture 5.1.1](http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042f/IHI0042F_aapcs.pdf)
arm call function 時，參數的傳遞是用 r0, r1, r2, r3

阿超過 4 個參數勒，實驗一下

temp2.c
```c=
#include <stdio.h>

int hello(int a, int b, int c, int d, int e, int f, int g)
{
    return a + b + c + d + e + f + g;
}

int main()
{
    int i = hello(1, 2, 3, 4, 5, 6, 7);
    printf("%d\n", i);
    return 0;
}
```
![](https://i.imgur.com/2hDRRFd.png)
跟 x86 類似，r3 後的參數都是放到 stack 中

### --- Back to analyzing busybox
總之，先嘗試用動態分析
給 QEMU 參數 -s 和 -S 用來開 gdb server 並下斷點
IDA pro 連上 gdb server
可以參考 [這篇筆記](https://hackmd.io/C9I1VflyT4O6VcQoIr8gdQ#Run-qemu-with-this-kernel)

首先他一開始位置定在 0x40000000
![](https://i.imgur.com/g4KRlmz.png)

參考 [註10](https://blog.printk.io/2019/06/arm-linux-kernel-early-startup-code-debugging/)
這邊的 code 目前還是 QEMU 的 boot loader，並不是 kernel
且這個 boot loader 相關的 code 在 [Qemu source code](https://github.com/qemu/qemu/blob/master/hw/arm/boot.c#L108) 可以看到
![](https://i.imgur.com/l5PwtE1.png)

可以看到 0x4000000C 的 `LDR PC, =loc_40010000`
(ARM 的 PC 就跟 x86 的 EIP 一樣)
待會就會跳到 0x40010000 上執行

![](https://i.imgur.com/Ev94IVM.png)
而這邊的 code 是什麼呢
![](https://i.imgur.com/eIT55pz.png)
沒錯，kernel 的 code

問題也出在這邊
kernel 的 code 被放到 0x4001**** 的位址
但是 kernel 中的 branch 跳轉都是直接寫死位址的
例如上圖裡面的 `b 0x34`
應該要跳到 0x40010034，卻直接跳到 0x34 死掉

要解決這個問題
1. 改 boot loader，將 kernel load 到 0x0000****
2. 改 kernel，將 base address 改為 0x4001****

# Reference
1. [BusyBox Wiki](https://zh.wikipedia.org/zh-tw/BusyBox)
2. [BusyBox - The Swiss Army Knife of Embedded Linux](https://www.busybox.net/downloads/BusyBox.html)
3. [Busybox init进程启动过程分析](https://blog.csdn.net/u013275111/article/details/56666940)
4. [Linux根文件系统分析之init和busybox](https://www.cnblogs.com/CrazyCatJack/p/6184564.html)
5. [BusyBox Source Code](https://github.com/mirror/busybox)
6. [Find reference to string in radare2](https://reverseengineering.stackexchange.com/questions/11597/find-reference-to-string-in-radare2)
7. [Why does the ARM PC register point to the instruction after the next one to be executed?](https://stackoverflow.com/questions/24091566/why-does-the-arm-pc-register-point-to-the-instruction-after-the-next-one-to-be-e)
8. [Calling convention ARM(A32)](https://en.wikipedia.org/wiki/Calling_convention#ARM_(A32))
9. [Procedure Call Standard for the ARM® Architecture](http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042f/IHI0042F_aapcs.pdf)
10. [ARM Linux Kernel early startup code debugging](https://blog.printk.io/2019/06/arm-linux-kernel-early-startup-code-debugging/)
11. [QEMU source code](https://github.com/qemu/qemu)
12. [firmadyne/kernel-v4.1](https://github.com/firmadyne/kernel-v4.1)
