文章目录
- 简介
- 参考阅读
- libnvram
- sem系列函数
- sem_get
- sem_lock
- sem_unlock
- nvram_init系列函数
- nvram_init
- nvram_set_default
- 感言
简介
鸽王来咯
这一篇写的是FirmAE
中源于firmadyne
的libnvram
(经过了一定修改),不得不说这一部分是很有意义的工作,放到今天来看也是很有启发意义的。
甚至在写的过程中还找到了libnvram
的bug
参考阅读
信号量函数介绍1
信号量函数介绍2
信号量函数介绍3
信号量函数介绍4(重要)
ftok函数介绍
libnvram
firmdyne
论文里面提出并实现的重量级内容,不过代码似乎没有那么复杂。
本质上是写了一堆nvram
相关的原子操作,然后再用这个原子操作去实现更复杂的nvram
读写函数。
需要注意的是libnvram
里的函数都是出现在实际固件中的真实nvram
操作函数,libnvram
是通过LD_PRELOAD
的方式实现了同名函数的hook(参见Readme,这一部分写在了调整过的内核里)。
这里也需要特意提一下libnvram
的hook方式。firmadyne
论文中的描述如下(详细可以参考论文第四节IMPLEMENTATION
中的C. Emulation
部分):
原文:Since the ELF loader uses a global symbol lookup scope during resolution [12], we were able to compile our NVRAM library with the -nostdlib compiler flag, delaying resolution of external symbols until after the calling process had already loaded the system C runtime library. Effectively, this allowed our shared library to appear as a static binary while dynamically utilizing functions made available by the calling process,including the standard C runtime library.
翻译:因为ELF加载器在解析符号时使用了全局符号查找范围,我们可以使用-nostdlib编译参数来编译我们的NVRAM库,将外部符号的解析推迟到(固件)进程加载完系统C运行时库之后。这样,就使得我们的共享库可以像静态(链接的)二进制文件一样发挥作用,但同时可以使用(固件)进程自身加载的(该平台上的)库中的函数。
简单来说就是让libnvram
可以使用固件自带的标准运行库中的函数,从而实现了抽象与多平台适配。
其原理可以参考这个,应该是由于指定了LD_PRELOAD
使libnvram
在一开始就加载了(此时标准库未加载),但如果按正常编译方式的话,会添加额外的依赖标准运行库的指令,导致在加载libnvram
时直接出错;而-nostdlib
参数会去掉这些依赖性指令。考虑到nvram
系列函数调用顺序肯定比标准库函数靠后,故等到nvram
系列函数调用时,已经加载完了固件自带的标准运行库,可以利用global symbol lookup scope
使用其中的函数了。
nvram.h
与alias.h
中提供的都是函数原型,至于nvram.c
中出现的大写字母常量和宏可以在config.h
中找到。
另一点值得一提的就是libnvram
中实际上提供了一个在运行时读取键值的接口(OVERRIDE_POINT
),在nvram_init
、nvram_reset
等系列函数中使用,所以实际上并不需要修改与重编译libnvram
,只需要在特定目录下(/firmadyne/libnvram.override/
)提供键值对就可以实现增加nvram
键值对的效果。
重要的几个原子函数如下,在其实现中大量使用了信号量函数,应该是考虑到了对nvram
的多进程/线程操作。
sem系列函数
sem_get
主要是用来获取信号量的一个函数。如上所述,这里的semget
等信号量函数均为标准库函数,注意区分。
先通过ftok
函数获取IPC key
键值,再用semget
函数以该key
创建对应的信号量(IPC
键值和IPC
标识符的概念见ftok函数介绍),0666应该是权限标识。
如果正常获取到信号量,则使用semop
函数解锁该信号量(解锁应该是因为semget
创建的信号量默认是锁着的;注意看sembuf
结构中.sem_op
被置1,这意味着这里的semop
函数会执行+1,即V(发送信号、解锁)操作。)。解锁失败则直接用semctl
给信号量删了,返回-1。
如果该信号量已经被占用(也就是被创建过了),就尝试以non-exclusive mode
(非独占模式)打开该信号量,如果成功再一边等待timeout
一边尝试用semctl
获取信号量,获取成功后返回;打开失败则直接返回报错。
总的来说是实现了一个类似互斥锁的结构?不过这里加的锁却是针对整个MOUNT_POINT
,也就是libnvram
目录。
static int sem_get() {int key, semid = 0;unsigned int timeout = 0;struct semid_ds seminfo;union semun {int val;struct semid_ds *buf;unsigned short *array;struct seminfo *__buf;} semun;struct sembuf sembuf = {.sem_num = 0,.sem_op = 1,.sem_flg = 0,};// Generate key for semaphore based on the mount pointif (!ftok || (key = ftok(MOUNT_POINT, IPC_KEY)) == -1) {PRINT_MSG("%s\n", "Unable to get semaphore key! Utilize altenative key.. by SR");return -1;}PRINT_MSG("Key: %x\n", key);// Get the semaphore using the keyif ((semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666)) >= 0) {semun.val = 1;// Unlock the semaphore and set the sem_otime fieldif (semop(semid, &sembuf, 1) == -1) {PRINT_MSG("%s\n", "Unable to initialize semaphore!");// Clean up semaphoresemctl(semid, 0, IPC_RMID);semid = -1;}}else if (errno == EEXIST) {// Get the semaphore in non-exclusive modeif ((semid = semget(key, 1, 0)) < 0) {PRINT_MSG("%s\n", "Unable to get semaphore non-exclusively!");return semid;}semun.buf = &seminfo;// Wait for the semaphore to be initializedwhile (timeout++ < IPC_TIMEOUT) {semctl(semid, 0, IPC_STAT, semun);if (semun.buf && semun.buf->sem_otime != 0) {break;}}if (timeout >= IPC_TIMEOUT)PRINT_MSG("Waiting for semaphore timeout (Key: %x, Semaphore: %x)...\n", key, semid);}return (timeout < IPC_TIMEOUT) ? semid : -1;
}
sem_lock
给信号量加锁,外带初始化。
先检查init
参数,为0则通过setmntent
、getmntent_r
、strncmp
等函数二次检查nvram
值是否初始化。已初始化则使用sem_get
(即上面那个函数)获取信号量,再用semop
加锁(这里sembuf
中的.sem_op
为-1,对应即P(等待)操作);未初始化则调用nvram_init
对nvram
进行初始化。这里在注释中强调了需要在nvram
初始化完成后再获取信号量,因为ftok
获取的IPC key
键值会因为tmpfs
挂载发生变化。
(libnvam
虚拟化出的nvram
实际上是一个挂载在MOUNT_POINT
下的tmpfs
,具体可见nvram_init
系列函数,故这里直接通过比较挂载路径是否存在判断是否初始化)
static void sem_lock() {int semid;struct sembuf sembuf = {.sem_num = 0,.sem_op = -1,.sem_flg = SEM_UNDO,};struct mntent entry, *ent;FILE *mnt = NULL;// If not initialized, check for existing mount before triggering NVRAM initif (!init) {if ((mnt = setmntent("/proc/mounts", "r"))) {while ((ent = getmntent_r(mnt, &entry, temp, BUFFER_SIZE))) {if (!strncmp(ent->mnt_dir, MOUNT_POINT, sizeof(MOUNT_POINT) - 2)) {init = 1;PRINT_MSG("%s\n", "Already initialized!");endmntent(mnt);goto cont;}}endmntent(mnt);}PRINT_MSG("%s\n", "Triggering NVRAM initialization!");nvram_init();}cont:// Must get sempahore after NVRAM initialization, mounting will change IDif ((semid = sem_get()) == -1) {PRINT_MSG("%s\n", "Unable to get semaphore!");return;}// PRINT_MSG("%s\n", "Locking semaphore...");if (semop(semid, &sembuf, 1) == -1) {PRINT_MSG("%s\n", "Unable to lock semaphore!");}return;
}
sem_unlock
直接解锁,没啥好说的。SEM_UNDO
参数的解释:
当操作信号量(semop)时,sem_flg可以设置SEM_UNDO标识;SEM_UNDO用于将修改的信号量值在进程正常退出(调用exit退出或main执行完)或异常退出(如段异常、除0异常、收到KILL信号等)时归还给信号量。
如信号量初始值是20,进程以SEM_UNDO方式操作信号量减2,减5,加1;在进程未退出时,信号量变成20-2-5+1=14;在进程退出时,将修改的值归还给信号量,信号量变成14+2+5-1=20。
static void sem_unlock() {int semid;struct sembuf sembuf = {.sem_num = 0,.sem_op = 1,.sem_flg = SEM_UNDO,};if ((semid = sem_get(NULL)) == -1) {PRINT_MSG("%s\n", "Unable to get semaphore!");return;}// PRINT_MSG("%s\n", "Unlocking semaphore...");if (semop(semid, &sembuf, 1) == -1) {PRINT_MSG("%s\n", "Unable to unlock semaphore!");}return;
}
基本上sem系列函数就是实现一个锁功能,协调对nvram
值的竞态读写,顺便在未初始化时启动nvram
初始化。
nvram_init系列函数
nvram_init
先检查init
位,为0则置1后加锁,再将MOUNT_POINT
上挂载上tmpfs
格式的tmpfs
,创建/var/run/nvramd.pid
以适配Ralink
,再解锁并调用nvram_set_default
。
但这里在mount
前后调用的lock
与unlock
真的不会mismatch么
经过与作者确认,这是一个bug: https://github.com/firmadyne/libnvram/issues/7
int nvram_init(void) {FILE *f;PRINT_MSG("%s\n", "Initializing NVRAM...");if (init) {PRINT_MSG("%s\n", "Early termination!");return E_SUCCESS;}init = 1;sem_lock();if (mount("tmpfs", MOUNT_POINT, "tmpfs", MS_NOEXEC | MS_NOSUID | MS_SYNCHRONOUS, "") == -1) {sem_unlock();PRINT_MSG("Unable to mount tmpfs on mount point %s!\n", MOUNT_POINT);return E_FAILURE;}// Checked by certain Ralink routersif ((f = fopen("/var/run/nvramd.pid", "w+")) == NULL) {PRINT_MSG("Unable to touch Ralink PID file: %s!\n", "/var/run/nvramd.pid");}else {fclose(f);}sem_unlock();return nvram_set_default();
}
nvram_set_default
宏写在了函数里面,不过不影响。含有FirmAE
自己改过的部分,原本代码里面并没有注释下面的整个代码块,只是几个函数(nvram_set_default_builtin
、nvram_set_default_image
)的wrapper。
新加的部分主要是和FirmAE
自己加的parse_nvram_from_file
函数一起,手动设置nvram_files
文件内的键值对。
原来的两个函数主要是通过宏利用config.h
里面的默认路径搜索nvram
键值对并设置。
int nvram_set_default(void) {int ret = nvram_set_default_builtin();PRINT_MSG("Loading built-in default values = %d!\n", ret);if (!is_load_env) firmae_load_env();#define NATIVE(a, b) \if (!system(a)) { \PRINT_MSG("Executing native call to built-in function: %s (%p) = %d!\n", #b, b, b); \}#define TABLE(a) \PRINT_MSG("Checking for symbol \"%s\"...\n", #a); \if (a) { \PRINT_MSG("Loading from native built-in table: %s (%p) = %d!\n", #a, a, nvram_set_default_table(a)); \}#define PATH(a) \if (!access(a, R_OK)) { \PRINT_MSG("Loading from default configuration file: %s = %d!\n", a, foreach_nvram_from(a, (void (*)(const char *, const char *, void *)) nvram_set, NULL)); \}
#define FIRMAE_PATH(a) \if (firmae_nvram && !access(a, R_OK)) { \PRINT_MSG("Loading from default configuration file: %s = %d!\n", a, foreach_nvram_from(a, (void (*)(const char *, const char *, void *)) nvram_set, NULL)); \}
#define FIRMAE_PATH2(a) \if (firmae_nvram && !access(a, R_OK)) { \PRINT_MSG("Loading from default configuration file: %s = %d!\n", a, parse_nvram_from_file(a)); \}NVRAM_DEFAULTS_PATH
#undef FIRMAE_PATH2
#undef FIRMAE_PATH
#undef PATH
#undef NATIVE
#undef TABLE// /usr/etc/default in DGN3500-V1.1.00.30_NA.zipFILE *file;if (firmae_nvram &&!access("/firmadyne/nvram_files", R_OK) &&(file = fopen("/firmadyne/nvram_files", "r"))){char line[256];char *nvram_file;char *file_type;while (fgets(line, sizeof line, file) != NULL){line[strlen(line) - 1] = '\0';nvram_file = strtok(line, " ");file_type = strtok(NULL, " ");file_type = strtok(NULL, " ");//写了两遍,不知道为什么if (access(nvram_file, R_OK) == -1)continue;if (strstr(file_type, "ELF") == NULL)PRINT_MSG("Loading from default configuration file: %s = %d!\n", nvram_file, parse_nvram_from_file(nvram_file));}}return nvram_set_default_image();
}
剩下一些关于nvram
列表操作的函数就不再介绍了,读起来难度也不是很大。
感言
鸽到现在写的新发现就是,今年3月居然有人给这个老库提了两个pull request…
分别增加了对以RSA公钥形式存在的多行nvram值的支持,以及修复了nvram_getall
函数在处理空文件时的bug。
我自己找到的bug倒是没想到怎么修(有人给建议更好,我去提个pull然后把issus关了:)…
原作者对这个库代码的描述是“pretty old and crufty”
,从乱七八糟的宏和函数搭配中可见一瞥。
不过过了这么多年还有人提issue和pull request,这倒是证明了这玩意的生命力确实不错。