FirmAE源码粗读(五)

news/2025/1/15 23:38:50/

文章目录

    • 简介
    • 参考阅读
    • libnvram
      • sem系列函数
        • sem_get
        • sem_lock
        • sem_unlock
      • nvram_init系列函数
        • nvram_init
        • nvram_set_default
    • 感言

简介

在这里插入图片描述
鸽王来咯
这一篇写的是FirmAE中源于firmadynelibnvram(经过了一定修改),不得不说这一部分是很有意义的工作,放到今天来看也是很有启发意义的。

甚至在写的过程中还找到了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.halias.h中提供的都是函数原型,至于nvram.c中出现的大写字母常量和宏可以在config.h中找到。

另一点值得一提的就是libnvram中实际上提供了一个在运行时读取键值的接口(OVERRIDE_POINT),在nvram_initnvram_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则通过setmntentgetmntent_rstrncmp等函数二次检查nvram值是否初始化。已初始化则使用sem_get(即上面那个函数)获取信号量,再用semop加锁(这里sembuf中的.sem_op为-1,对应即P(等待)操作);未初始化则调用nvram_initnvram进行初始化。这里在注释中强调了需要在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前后调用的lockunlock真的不会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_builtinnvram_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,这倒是证明了这玩意的生命力确实不错。


http://www.ppmy.cn/news/663527.html

相关文章

Flink消费kafka出现空指针异常

文章目录 出现场景&#xff1a;表现&#xff1a;问题&#xff1a;解决&#xff1a; tombstone : Kafka中提供了一个墓碑消息&#xff08;tombstone&#xff09;的概念&#xff0c;如果一条消息的key不为null&#xff0c;但是其value为null&#xff0c;那么此消息就是墓碑消息. …

查看p10文件内容

1.通过openssl查看 [rootahdjg ~]#openssl req -in cerseq.csr -text #cerseq.csr存放的是p10的base64格式数据 unable to load X509 request 140239849273232:error:0906D06C:PEM routines:PEM_read_bio:no start line:pem_lib.c:707:Expecting: CERTIFICATE REQUEST 但是…

mac 配置 gradle 、gradle-wrapper.properties文件 distributionUrl 指定本地gradle.zip

mac配置 gradle https://blog.csdn.net/RreamigOfGirls/article/details/126300196 gradle-wrapper.properties 文件&#xff1a; distributionUrl 指定本地gradle.zip 注意 如果是从官网下载&#xff0c;用的是https #Thu May 10 21:25:29 CST 2018 distributionBaseGRADLE…

Pyside6-QtCharts+psutil实战-绘制一个CPU监测工具

今天是实战篇章&#xff0c;我们结合可以快速提升我们开发效率的工具一起开实战一波实时读取系统CPU使用情况的折线图。 使用的开发工具Qt Designer来开发UI界面。 十分便捷。使用起来也算比较的简单了&#xff0c;虽然也存在不少的BUG。 ❝ 对所需要的控件进行拖拽式&#xff…

最强神作!Crysis深度剖析与优化指南(18-25)

http://bak2.beareyes.com.cn/2/lib/200711/06/20071106421_18.htm 第18页&#xff1a;无限创造力&#xff1a;Sandbox 2编辑器概览 1 集成的CryENGINE Sandbox2编辑器 运行时间引擎是完全集成在CryENGINE Sandbox2编辑器给了设计者“所见既所玩”特性。 开发者在开发过程中可以…

代理服务器之 squid、lvs、nginx、haproxy之间的区别

代理服务器之 squid、lvs、nginx、haproxy之间的区别 代理服务可简单的分为正向代理和反向代理 1、正向代理 正向代理服务器&#xff1a;squid 用于代理内部网络对 Internet 的连接请求(如 VPN/NAT)&#xff0c;客户端指定代理服务器,并将本来要直接发送给目标 Web 服务器的 HT…

Stage模型HarmonyOS服务卡片开发ArkTS卡片相关模块

图1 ArkTS卡片相关模块 FormExtensionAbility&#xff1a;卡片扩展模块&#xff0c;提供卡片创建、销毁、刷新等生命周期回调。 FormExtensionContext&#xff1a;FormExtensionAbility的上下文环境&#xff0c;提供FormExtensionAbility具有的接口和能力。 formProvider&…

如何在网上挣钱,这几个项目让你月入过万元

在我国互联网高速发达的时代&#xff0c;上网也已经非常普及。互联网给我们的日常生活带了了无比的便利&#xff0c;也为很多创业者提供了不少的创业机会。互联网可以让你的产品得到更为广阔的销售市场&#xff0c;也可以让你在网络上购买到你所需要的产品。那么如何在网上挣钱…