惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
F
Fortinet All Blogs
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
小众软件
小众软件
有赞技术团队
有赞技术团队
雷峰网
雷峰网
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
AWS News Blog
AWS News Blog
C
Cisco Blogs
美团技术团队
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
W
WeLiveSecurity
D
DataBreaches.Net
博客园 - 司徒正美
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
T
The Blog of Author Tim Ferriss
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Vercel News
Vercel News
月光博客
月光博客
T
Tailwind CSS Blog
H
Help Net Security
aimingoo的专栏
aimingoo的专栏
P
Proofpoint News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Spread Privacy
Spread Privacy
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
V
V2EX
WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
Recent Announcements
Recent Announcements

网络热度

LeetCode 75 学习计划清单和参考答案 AI辅助生成的统计博客文章浏览量的网络爬虫 程序员面试题精解(5)— 素数判定 程序员面试题精解(4)— 双指针 哈佛 CS50AI 课程介绍:从零开始学习人工智能 解决不蒜子文章阅读计数错误的问题 RSA的攻与防(三) RSA的攻与防(二) 启用 TLS 1.3 提升网络应用的安全性和性能 程序员面试题精解(3)— 二分查找 巧解picoCTF的RSA挑战题Sum-O-Primes 请马上停止使用TLS 1.0和TLS 1.1! 自己动手使用树莓派搭建家用NAS和流媒体服务器 iTerm2 + Oh-My-Zsh + Powerlevel10k 打造酷炫macOS终端 AddressSanitizer — 程序员检测内存访问错误的利器 程序员面试题精解(2)— 平方根运算 思科 Catalyst Wi-Fi 6 MU-MIMO 带你畅享速度与激情 程序员面试题精解(1)— 比特位计数 C编程支持16位存取接口的存储设备
嵌入式Linux系统中uClibc标准库使用经验笔记
子曦 · 2022-12-24 · via 网络热度

uClibc是一个面向嵌入式Linux系统的小巧而精致的C标准库,在低配置的嵌入式系统及物联网设备的研发中应用广泛。这里分享最近的一些使用经验,为需要解决相似问题或需求的工程师提供便利。

To me programming is more than an important practical art. It is also a gigantic undertaking in the foundations of knowledge.
Grace Hopper(格蕾丝·赫柏,美国计算机科学家、海军准将,创造了现代第一个编译器A-0系统,以及第一个高级商用计算机程序语言“COBOL”,被誉为“COBOL之母”)

uClibc简介

uClibc(有时写成μClibc)是一个小型的C标准库,旨在为使用基于Linux内核的操作系统的嵌入式系统和移动设备提供支持。uClibc原先是为了支持不需要内存管理单元的Linux版本μClinux的开发的,因此特别适合于微控制器系统。其名称中“uC”就是微控制器英文microcontroller的缩写,u是代表微小("micro")的希腊字母μ的近似拉丁字母排版。

uClibc是在GNU Lesser GPL下授权的自由和开源软件,其库函数封装了Linux内核的系统调用。它可以在标准或无MMU的Linux系统上运行,支持i386、x86-64、ARM、MIPS,和PowerPC等众多处理器。uClibc的开发始于1999年,大部分是从零开始编写的,但也吸收了glibc和其他项目的代码。uClibc的比glibc小得多,glibc的目标是在广泛的硬件和内核平台上完全支持所有相关的C标准,而uClibc则侧重于嵌入式Linux系统。它还允许开发者根据内存空间设计要求来开启或禁用一些功能。

下面的记录显示在两个相似的嵌入式系统中C标准库文件的列表。第一个使用glibc-2.23版,第二个集成uClibc-0.9.33.2版。总计glibc库文件为2MB多,而uClibc库文件加起来不到1MB。可见使用uClibc确实节省了不少存储空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
STM1:/
-rwxr-xr-x 1 root root 9.6K Jan 1 1970 ./lib/libanl-2.23.so
-rwxr-xr-x 1 root root 1.1M Jan 1 1970 ./lib/libc-2.23.so
-rwxr-xr-x 1 root root 177.5K Jan 1 1970 ./lib/libcidn-2.23.so
-rwxr-xr-x 1 root root 29.5K Jan 1 1970 ./lib/libcrypt-2.23.so
-rwxr-xr-x 1 root root 9.5K Jan 1 1970 ./lib/libdl-2.23.so
-rwxr-xr-x 1 root root 429.4K Jan 1 1970 ./lib/libm-2.23.so
-rwxr-xr-x 1 root root 65.8K Jan 1 1970 ./lib/libnsl-2.23.so
-rwxr-xr-x 1 root root 17.5K Jan 1 1970 ./lib/libnss_dns-2.23.so
-rwxr-xr-x 1 root root 33.6K Jan 1 1970 ./lib/libnss_files-2.23.so
-rwxr-xr-x 1 root root 90.5K Jan 1 1970 ./lib/libpthread-2.23.so
-rwxr-xr-x 1 root root 65.7K Jan 1 1970 ./lib/libresolv-2.23.so
-rwxr-xr-x 1 root root 25.9K Jan 1 1970 ./lib/librt-2.23.so
-rwxr-xr-x 1 root root 9.5K Jan 1 1970 ./lib/libutil-2.23.so

STM2:/
-rwxr-xr-x 1 root root 28.0K Jan 1 1970 ./lib/ld-uClibc-0.9.33.2.so
-rwxr-xr-x 1 root root 36.1K Jan 1 1970 ./lib/libcrypt-0.9.33.2.so
-rwxr-xr-x 1 root root 16.2K Jan 1 1970 ./lib/libdl-0.9.33.2.so
-rwxr-xr-x 1 root root 72.1K Jan 1 1970 ./lib/libm-0.9.33.2.so
-rwxr-xr-x 1 root root 116.4K Jan 1 1970 ./lib/libpthread-0.9.33.2.so
-rwxr-xr-x 1 root root 16.2K Jan 1 1970 ./lib/librt-0.9.33.2.so
-rwxr-xr-x 1 root root 28.3K Jan 1 1970 ./lib/libthread_db-0.9.33.2.so
-rwxr-xr-x 1 root root 621.4K Jan 1 1970 ./lib/libuClibc-0.9.33.2.so
-rwxr-xr-x 1 root root 8.1K Jan 1 1970 ./lib/libubacktrace-0.9.33.2.so
-rwxr-xr-x 1 root root 4.1K Jan 1 1970 ./lib/libutil-0.9.33.2.so

IPv6和接口API

随着IPv6的使用量稳步增长,为嵌入式系统添加IPv6协议栈支持已经成为必需。在一个为使用uClibc的设备加入IPv4/IPv6双栈功能的软件项目中,发现出现应用程序链接错误——undefined reference to getifaddrsgetifaddrs()是一个非常有用的函数,我们可以调用它得到系统所有的网络接口的地址信息。查询Linux编程手册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SYNOPSIS
#include <sys/types.h>
#include <ifaddrs.h>

int getifaddrs(struct ifaddrs **ifap);
...

DESCRIPTION
The getifaddrs() function creates a linked list of structures
describing the network interfaces of the local system, and stores
the address of the first item of the list in *ifap.
...

VERSIONS
The getifaddrs() function first appeared in glibc 2.3, but before
glibc 2.3.3, the implementation supported only IPv4 addresses;
IPv6 support was added in glibc 2.3.3. Support of address
families other than IPv4 is available only on kernels that
support netlink.
...

上面的最后一句话很关键:只有支持netlink的内核才能支持IPv4以外的地址系列。此系统运行的Linux内核版本是3.x, 是支持netlink的。那么,会不会是uClibc对netlink的支持出现问题导致了getifaddrs()函数没有被编译呢?

带着这一疑问,搜索uClibc的源码目录,找到实现getifaddrs()函数的C文件:

libc/inet/ifaddrs.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...
#if __ASSUME_NETLINK_SUPPORT
#ifdef __UCLIBC_SUPPORT_AI_ADDRCONFIG__


struct ifaddrs_storage
{
struct ifaddrs ifa;
union
{


struct sockaddr sa;
struct sockaddr_ll sl;
struct sockaddr_in s4;
#ifdef __UCLIBC_HAS_IPV6__
struct sockaddr_in6 s6;
#endif
} addr, netmask, broadaddr;
char name[IF_NAMESIZE + 1];
};
#endif
...
#ifdef __UCLIBC_SUPPORT_AI_ADDRCONFIG__
...
int
getifaddrs (struct ifaddrs **ifap)
...
#endif
...
#endif

果不其然!整个函数的实现和相关数据结构ifaddrs_storage的定义,都被置于三个嵌套的宏条件编译下:

  1. __ASSUME_NETLINK_SUPPORT
  2. __UCLIBC_SUPPORT_AI_ADDRCONFIG__
  3. __UCLIBC_HAS_IPV6__

所以,只要将它们对应的配置行打开就应该能解决问题。如下改动uClibc的配置文件后,重建uClibc的动态链接库,再make应用程序就成功了:

1
2
3
4
5
6
7
8
9
10


@@ -147,7 +147,8 @@ UCLIBC_HAS_RPC=y
UCLIBC_HAS_FULL_RPC=y
-# UCLIBC_HAS_IPV6 is not set
+UCLIBC_HAS_IPV6=y
-# UCLIBC_USE_NETLINK is not set
+UCLIBC_USE_NETLINK=y
+UCLIBC_SUPPORT_AI_ADDRCONFIG=y
UCLIBC_HAS_BSD_RES_CLOSE=y

SHA-2散列函数

嵌入式系统常常需要为系统管理员提供远程SSH登录服务,这就必须创建系统用户及其密码。Linux将用户名和密码的散列值(hash)保存在 /etc/shadow 文件中。散列值的存储格式遵循称为模块化加密格式(Modular Crypt Format,简写为MCF)的事实标准,其格式如下:

1
$<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]

这里

  • id: 表示散列算法的标识符(例如 1 代表MD5,5 代表SHA-256,6 代表SHA-512)
  • param=value:散列复杂度参数(如轮数/迭代次数)及其数值
  • salt: radix-64(字符集[+/a-zA-Z0-9])编码的盐
  • hash: 密码和盐的散列值的radix-64编码结果

随着计算机算力跟随摩尔定律迅速增强,以前常用的基于 MD5 的散列方案因为太容易受到攻击已被淘汰。现在新设计的系统都换到 SHA-512 散列方案,对应于/etc/shadow 文件中所见的$6$

用户密码散列值的生成和验证都可用名为crypt的POSIX C 库函数实现。此函数的定义如下:

1
char *crypt(const char *key, const char *salt)

输入参数key指向存有用户密码的字符串,salt指向格式为$<id>$<salt>的字符串,标明所要使用的散列算法和盐。大部分的Linux发行版都使用glibc库提供的crypt函数实现。下图概括了Glibc为crypt所增加的功能:

在集成uClibc的嵌入式Linux系统中,uClibc提供对crypt函数的支持。但是在测试中发现,它居然对$6$<salt>的输入返回空指针!这是怎么回事?

谜底就在uClibc的crypt函数实现中,找到相应的C源码:

libcrypt/crypt.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <unistd.h>
#include <crypt.h>
#include "libcrypt.h"

char *crypt(const char *key, const char *salt)
{
const unsigned char *ukey = (const unsigned char *)key;
const unsigned char *usalt = (const unsigned char *)salt;

if (salt[0] == '$') {
if (salt[1] && salt[2] == '$') {
if (*++salt == '1')
return __md5_crypt(ukey, usalt);
#ifdef __UCLIBC_HAS_SHA256_CRYPT_IMPL__
else if (*salt == '5')
return __sha256_crypt(ukey, usalt);
#endif
#ifdef __UCLIBC_HAS_SHA512_CRYPT_IMPL__
else if (*salt == '6')
return __sha512_crypt(ukey, usalt);
#endif
}

return NULL;
}
return __des_crypt(ukey, usalt);
}

啊哈!原来它默认只做 MD5 散列,SHA-256 和 SHA-512 的代码需要各自的条件编译宏定义。这好办,编辑uClibc的配置文件将后面两者都打开就可以了。

1
2
3
4
5
6
7
8
9


@@ -151,8 +151,8 @@ UCLIBC_HAS_REGEX_OLD=y
UCLIBC_HAS_RESOLVER_SUPPORT=y
-# UCLIBC_HAS_SHA256_CRYPT_IMPL is not set
-# UCLIBC_HAS_SHA512_CRYPT_IMPL is not set
+UCLIBC_HAS_SHA256_CRYPT_IMPL=y
+UCLIBC_HAS_SHA512_CRYPT_IMPL=y
UCLIBC_HAS_SHADOW=y

最后去看看uClibc自带的测试 SHA-512 散列算法的程序。它清楚地列出了测试代码所定义的数据结构,其包括盐、输入密码和期待的输出,以及多个测试矢量:

test/crypt/sha512c-test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static const struct
{
const char *salt;
const char *input;
const char *expected;
} tests[] =
{
{ "$6$saltstring", "Hello world!",
"$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJu"
"esI68u4OTLiBFdcbYEdFCoEOfaS35inz1" },
{ "$6$rounds=10000$saltstringsaltstring", "Hello world!",
"$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb"
"HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v." },
...
{ "$6$rounds=10$roundstoolow", "the minimum number is still observed",
"$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1x"
"hLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX." },
};

可以看到,最后一个测试用例输入定义轮次为10($6$rounds=10$roundstoolow),而输出结果显示的轮次为1000(rounds=1000)。这印证了uClibc的crypt函数实现与Glibc增加的功能相匹配 —— 为了保障安全性,如果输入指定的轮次太小,crypt将自动设定为最小轮次1000。

DNS安全补丁

2022年5月初,一家专注于为工业和关键基础设施环境提供安全解决方案的公司,Nozomi Networks,发布了他们发现的uClibc的一个新的安全漏洞 CVE-2022-30295。此漏洞存在所有版本的uClibc及其分支uClibc-ng(1.0.41版之前)的域名系统(DNS)实现中。由于该实现在进行DNS请求时使用了可预测的事务ID,因此存在DNS缓存被攻击者毒化的风险。

具体来说,应用程序常常调用gethostbyname库函数以解析某个主机名对用的网络地址。uClibc/uClibc-ng 内部实现了一个__dns_lookup函数提供实际的DNS域名请求及应答处理过程。以uClibc 最后的0.9.33.2版为例,下面的截图显示了__dns_lookup函数中问题代码:

参考第1308行,在第一次DNS请求时,local_id变量被初始化为上一次DNS请求的事务ID值(保存在静态变量last_id中)。第1319行是实际的漏洞核心,它只是简单地将local_id旧值递增1来更新。这个新值又如第1322行所示被存回last_id变量中。最后,在第1334行,local_id的值被复制到结构变量h中,它代表了DNS请求头的实际内容。这段代码在所有可获得的uClibc和1.0.41版之前的uClibc-ng中都差不多。

这种实现使得DNS请求中的事务ID变成可预测的,因为攻击者只要探察到正在的事务ID,就能估计出下一个请求中的事务ID数值。利用这个漏洞,攻击者只要制作一个包含正确源端口的DNS应答,以及在与DNS服务器返回的合法应答的竞争中获胜,就能扰乱/毒化主机的DNS缓存,使主机系统中应用程序的网络数据被导向攻击者设定的陷阱站点。

在此安全漏洞公布之后,uClibc-ng的维护者反应很快。他们于2022年5月中旬就提交了修复补丁,并在当月底发布了包括此修改的1.0.41版。对于uClibc,由于此C标准库从2012年起就停止发布新版本,目前处于无人维护状态,所以需要系统研发工程师自己手工修补。下面的uClibc补丁可供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61




@@ -315,6 +315,7 @@ Domain name in a message can be represented as either:
#include <sys/utsname.h>
#include <sys/un.h>
#include <sys/stat.h>
+#include <fcntl.h>
#include <sys/param.h>
#include <bits/uClibc_mutex.h>
#include "internal/parse_config.h"
@@ -1212,6 +1213,20 @@ static int __decode_answer(const unsigned char *message, /* packet */
return i + RRFIXEDSZ + a->rdlength;
}

+uint16_t dnsrand_next(int urand_fd, int def_value) {
+ if (urand_fd == -1) return def_value;
+ uint16_t val;
+ if(read(urand_fd, &val, 2) != 2) return def_value;
+ return val;
+}
+
+int dnsrand_setup(int *urand_fd, int def_value) {
+ if (*urand_fd > 0) return dnsrand_next(*urand_fd, def_value);
+ *urand_fd = open("/dev/urandom", O_RDONLY);
+ if (*urand_fd == -1) return def_value;
+ return dnsrand_next(*urand_fd, def_value);
+}
+
/* On entry:
* a.buf(len) = auxiliary buffer for IP addresses after first one
* a.add_count = how many additional addresses are there already
@@ -1237,6 +1252,7 @@ int __dns_lookup(const char *name,
/* Protected by __resolv_lock: */
static int last_ns_num = 0;
static uint16_t last_id = 1;
+ static int urand_fd = -1;

int i, j, fd, rc;
int packet_len;
@@ -1305,7 +1321,7 @@ int __dns_lookup(const char *name,
}
/* first time? pick starting server etc */
if (local_ns_num < 0) {
- local_id = last_id;
+ local_id = dnsrand_setup(&urand_fd, last_id);
/*TODO: implement /etc/resolv.conf's "options rotate"
(a.k.a. RES_ROTATE bit in _res.options)
local_ns_num = 0;
@@ -1316,8 +1332,9 @@ int __dns_lookup(const char *name,
retries_left--;
if (local_ns_num >= __nameservers)
local_ns_num = 0;
- local_id++;
+ local_id = dnsrand_next(urand_fd, local_id++);
local_id &= 0xffff;
+ DPRINTF("local_id:0x%hx\n", local_id);
/* write new values back while still under lock */
last_id = local_id;
last_ns_num = local_ns_num;

此uClibc补丁是uClibc-ng官方修复补丁的简化版,其核心是从系统的/dev/urandom文件读取双字节的随机数,然后用它设置原来的local_id,即DNS请求的事务ID。/dev/urandom是Linux系统的一个特殊的设备文件,用作非阻塞随机数发生器,它会重复使用熵池中的数据以产生伪随机数据。

注意在上面的补丁中,dnsrand_setup函数一定要先检查urand_fd是否为正,只有不成立时才去打开/dev/urandom。否则,每一次应用程序做DNS查询时都会重新打开此文件,系统将很快达到允许使用的文件描述符数目的上限,系统将因为无法再打开任何文件而崩溃。

最后,给出一个使用uClibc的嵌入式系统在加入DNS安全补丁前后的比较。如下是两个嗅探器截获的DNS数据包,第一个无补丁的系统,DNS请求的事务ID按顺序递增,这是明显的安全漏洞;第二个是加补丁后,每一个DNS请求的事务ID都是随机值,漏洞已补上。