文章目录
- 简介
- 参考阅读
- 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 point
if (!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 key
if ((semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666)) >= 0) {
semun.val = 1;
// Unlock the semaphore and set the sem_otime field
if (semop(semid, &sembuf, 1) == -1) {
PRINT_MSG("%s\n", "Unable to initialize semaphore!");
// Clean up semaphore
semctl(semid, 0, IPC_RMID);
semid = -1;
}
}
else if (errno == EEXIST) {
// Get the semaphore in non-exclusive mode
if ((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 initialized
while (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 init
if (!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 ID
if ((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 routers
if ((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.zip
FILE *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,这倒是证明了这玩意的生命力确实不错。