---
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
```

看來 entry 是 0x00008710
```
# 先分析一下
[0x00008710]> aa
# 可以看到程式流程圖
[0x00008710]> VV
```

這支程式沒有 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

iz 會秀出所有在 data sections 的字串

搭配 `~` 可以篩選感興趣的部分,有點像 grep

找到字串 `process '%s' (pid %d) exited. Scheduling for restart.` 的 Vaddr 是 0x000954cc
接下來找找有哪邊有用到 0x000954cc 這個地址

用 `/c` 這指令找不到
後來查到 `ax` 這系列的指令

但也沒派上用場
此時我在想這些指令真的是這樣用嗎
為了測試,我自己寫了簡單的 C code
用 r2 打開,執行了分析 `aa` 後,再用以上提到的指令
證明這些指令是可行的
下了這個指令後完全沒輸出,更直接宣判找哪邊有用到字串這招沒用
```
[0x00008710]> ax~str
```
因為這支程式根本沒有引用字串(下這個指令而沒有輸出,可以這樣解釋)
真的是這樣嗎,還是 ARM 架構有什麼東西不太一樣?
### --- fail to find string reference ?

看來不是沒有引用字串
### --- 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:

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


這邊發現幾個 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;
}
```

跟 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

參考 [註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) 可以看到

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

而這邊的 code 是什麼呢

沒錯,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)