FirmAE源码粗读(五)

news2024/10/7 5:30:03

文章目录

    • 简介
    • 参考阅读
    • 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 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则通过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 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前后调用的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 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_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.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,这倒是证明了这玩意的生命力确实不错。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/704271.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

国产32位MCU低成本烟机变频风机方案

家用油烟机主要由风机系统&#xff0c;控制系统&#xff0c;滤油装置&#xff0c;外壳和悬吊装置组成&#xff0c;其中风机系统是油烟机的心脏&#xff0c;通常由电机和叶轮组成&#xff0c;一台油烟机功率的大小&#xff0c;也是由风机系统决定的&#xff0c;控制系统是烟机最…

JVM-垃圾回收器

垃圾回收器 Serial 新生代垃圾回收器&#xff0c;单线程&#xff0c;会产生STW&#xff08;Stop The World&#xff09;&#xff0c;采用拷贝算法。 它在停止线程时&#xff0c;并不是直接将线程强行停止&#xff0c;而是等线程运行到一个安全点&#xff08;Safe Point&…

超级实用!详解Node.js中的lodash模块和async模块

文章目录 7. lodash 模块安装 Lodash数组处理对象处理函数式编程 8. async 模块安装 Async异步流程控制串行执行&#xff08;Series&#xff09;并行执行&#xff08;Parallel&#xff09;循环迭代&#xff08;Each&#xff09; 控制流程和错误处理瀑布流控制&#xff08;Water…

强化学习:策略梯度法

策略梯度法的思路 之前我们是用表格的形式来表达策略&#xff0c;现在我们同样可以用函数来表达策略。之前学的所有的方法都是被称为 value-based&#xff0c;接来下学的叫 policy-based 。接下来我们来看一下 策略梯度法的思路。之前学的的策略全都是用表格来表示的&#xff0…

win 安装 C++运行环境 - MinGW

目录 一、下载二、安装四、检查是否安装成功五、参考文章 一、下载 官网地址&#xff1a;https://www.mingw-w64.org/downloads/ 1.1点击【MingW-W64-builds】 1.2点击【Github】 1.3下载 如果下载太慢&#xff0c;可以使用GitHub Proxy 代理加速 (ghproxy.com) 二、安装 …

c++为什么支持函数重载?

前言 在c语言中&#xff0c;函数名是不可以重复的&#xff0c;在同一作用域中函数名称都是唯一的。这也使得我们的函数调用充满了种种麻烦。 而c则支持通过函数重载解决了这个问题 函数重载&#xff1a;是函数的一种特殊情况&#xff0c;C允许在同一作用域中声明几个功能类似的…

如何在Microsoft Excel中移动列的位置

若要在 Excel 中移动列,请使用 shift 键或“插入剪切单元格”。你还可以神奇地更改所有列的顺序。 一、Shift 键 要在不覆盖现有数据的情况下快速移动 Excel 中的列,请按住键盘上的 shift 键。 移动单列: 1、首先,选择一列。 2、将鼠标悬停在所选内容的边界上。此时会出…

建立本地题库

建立试题库文件json 第一步&#xff1a; 按标准格式保存试题到本地,文件名为.json. import json import osimage_path os.path.join(C:\\, Users, 123, PycharmProjects, pythonProject1, test01, imges, jxn_0900.png)# 准备要保存的数据 data [{"id": 1,"…

基于深度学习的高精度动物园动物检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度动物园动物&#xff08;水牛、斑马、大象、水豚、海龟、猫、奶牛、鹿、狗、火烈鸟、长颈鹿、捷豹、袋鼠、狮子、鹦鹉、企鹅、犀牛、羊、老虎&#xff09;检测识别系统可用于日常生活中或野外来检测与定位动物园动物&#xff0c;利用深度学…

【Java可执行命令】(五)打包部署工具 jar:深入解析应用程序打包、分发和部署工具jar ~

Java可执行命令详解之jar 1️⃣ 概念2️⃣ 优势和缺点3️⃣ 使用3.1 语法格式&#xff1a;创建jar文件3.1.1 参数&#xff1a;-cf3.1.2 参数&#xff1a;-tf3.1.3 参数&#xff1a;-i3.1.4 参数&#xff1a;-v3.1.5 参数&#xff1a;-e 3.2 运行jar文件 4️⃣ 应用场景5️⃣ 注…

Qt实现电子商城系统

用Qt实现的电子商城系统&#xff1a; 1.功能包括&#xff1a; 1)管理员账户 2)用户管理 3)用户登录 4)商品管理 5)商品出售 6)软件打包 2.商品包括&#xff1a;程序源码、开发教程和程序讲解&#xff1b;也可以根据需求进行功能更改 3.试用链接 链接&#xff1a;https://pan.…

华硕无畏14pro设置指纹登录

正常的流程如下 打开一个文件夹&#xff0c;在左侧找到此电脑&#xff0c;点击右键&#xff0c;找到属性 在搜索框中搜索&#xff1a;设置指纹登录 找到指纹识别&#xff0c;左键点击即可展开&#xff0c;点击&#xff1a;添加手指 点击&#xff1a;开始 输入PIN&#xff08;开…

Oracle Linux 迷途知返

Oracle Linux 6.9 发布了。Oracle 已经宣布发布 Oracle Linux 6 发行版的更新了。 新版本 Oracle Linux 6 Update 9&#xff0c;包括多个已更新的内核&#xff0c;以及两个新的 "Unbreakable Enterprise Kernel" 包和一个 "Red Hat Compatible Kernel" 包…

SAP 后台配置之FM基金管理篇

SAP FM基金管理后台配置及应用 1 二话不说先上后台配置&#xff0c;能跑通为先1.1 基础设置1.1.1 维护财务管理区1.1.2 分配财务管理区1.1.3 激活全局基金管理功能1.1.4 定义全局参数1.1.5 定义编号区间编号并分配1.1.6 创建更改层次变式1.1.3 激活科目分配元素1.1.4 设置允许空…

3 款适合您手机或平板电脑的最佳 Android 和 iOS 修复工具

让我们面对现实&#xff1a;技术不可能总是完美的&#xff0c;您的智能手机也是如此&#xff0c;尽管更容易爱上它。毕竟&#xff0c;它只是一台机器&#xff01; 无论您使用的是 Android 还是 iOS 系统的智能手机或平板电脑&#xff0c;当您第一次带回家时&#xff0c;它都能…

搭建帮助中心需要用到哪些工具

随着企业的发展和客户需求的增加&#xff0c;为了提供更好的客户支持和服务&#xff0c;许多企业决定搭建帮助中心。一个完善的帮助中心可以为客户提供详细的产品信息、常见问题解答和使用指南等&#xff0c;帮助他们快速解决问题并提高客户满意度。在本文中&#xff0c;我们将…

优秀的测试工程师养成记,庸碌四年的点工,“我“要进阶了...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 怎么才能成为优秀…

运动耳机哪种戴着舒服、值得推荐的五款运动耳机

在运动中享受最佳声音和舒适感受。这些耳机不仅具备防汗、抗水、稳定舒适的特点&#xff0c;而且还拥有出色的音质和智能功能&#xff0c;帮助你充分释放潜能&#xff0c;突破自我。无论你是专业运动员还是热爱户外活动的人&#xff0c;这些运动耳机将成为你的最佳伙伴&#xf…

Spring AOP 源码探索 之 链式调用中的 ExposeInvocationInterceptor拦截器作用

文章目录 ExposeInvocationInterceptor 示例代码源码分析 extendAdvisorsmakeAdvisorChainAspectJCapableIfNecessary添加扩展拦截器的调用链作用示例总结相关学习路线 JAVA资深架构师成长路线->开源框架解读->Spring框架源码解读 ExposeInvocationInterceptor 从英文名字…

【Linux系列P5】gccg++与【动静态库】的美妙邂逅

前言 大家好吖&#xff0c;欢迎来到 YY 滴 Linux系列 &#xff0c;热烈欢迎&#xff01;本章主要内容面向接触过Linux的老铁&#xff0c;主要内容含 欢迎订阅 YY 滴Linux专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; 订阅专栏阅读&#xff1a;YY的《…