文章目录
- 简介
- 参考阅读
- 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,这倒是证明了这玩意的生命力确实不错。



















