0%

strncat溢出杂谈

Step 1

遇到一个问题,在Debian上安装了libc-bin-dbgsym,有以下的日志打印到终端中:

1
2
3
4
5
6
7
8
9
10
11
*** buffer overflow detected ***: lxcfs terminated
======= Backtrace: =========
/lib/libc.so.6.1(+0x8cb88)[0x2000611cb88]
/lib/libc.so.6.1(__fortify_fail+0x64)[0x200061c19d4]
/lib/libc.so.6.1(__chk_fail+0x24)[0x200061be864]
/lib/libc.so.6.1(__strncat_chk+0x48)[0x200061bd678]
/usr/lib/sw_64-linux-gnu/lxcfs/liblxcfs.so(dynmem_task+0x868)[0x20004832cb8]
/lib/libpthread.so.0(+0x80fc)[0x2000776a0fc]
/lib/libc.so.6.1(+0x119864)[0x200061a9864]
...
fish: “lxcfs -l -m docker /var/lib/lxc…” terminated by signal SIGABRT (Abort)

看得出是由strncat导致的,于此有关的代码类似这样:

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char *argv[]) {
char base64[64], buf[16];

while (true) {
...
strncat(buf, base64, 12);
...
}

return 0;
}

原因是我把strcat(s1, s2)当成strcpy(dst, src)用了。strcat是将两个字符串拼接,strcpy是将src拷贝至dst中。因为s1的大小不足以容纳strlen(s1)+strlen(s2)个字符,因此发生了溢出。

但溢出并非总是发生,AMD64 CPU / GCC 7.4.1 / GLIBC 2.26下有这这样的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
./strncpy 
time: 1569841068 sha256(long): H8Zw/CnxF9d/kG10Ck7VfDMNqWADDGMsrD7Wx0wTshY=
sha256(short): H8Zw/CnxF9d/
time: 1569841069 sha256(long): uyzMNURXeyOjyCZ1ANI6Lze6lwF1bKUPjU3rHBZyTNY=
sha256(short): H8Zw/CnxF9d/uyzMNURXeyOj
time: 1569841070 sha256(long): 1zdqJgOCuILZkti92I2YUuT/KGNuqmSPOioA8rjxEeg=
sha256(short): H8Zw/CnxF9d/uyzM1zdqJgOCuILZkti92I2YUuT/KGNuqmSPOioA8rjxEeg=1zdqJgOCuILZ
time: 1569841071 sha256(long): Bvf5PF9cd0q01tccsNNvNtDjPMgUSWbMx2rNgLVFgEo=
sha256(short): H8Zw/CnxF9d/uyzMBvf5PF9cd0q01tccsNNvNtDjPMgUSWbMx2rNgLVFgEo=Bvf5PF9cd0q0
time: 1569841072 sha256(long): S/06FjYG/uc3FxAY/DQbEIxezMw+xGfEbg0i373utQs=
sha256(short): H8Zw/CnxF9d/uyzMS/06FjYG/uc3FxAY/DQbEIxezMw+xGfEbg0i373utQs=S/06FjYG/uc3
time: 1569841073 sha256(long): miGQZLfdfwozwdkunw1RUA+v+eqlx6DjO7hryjuIcQc=
sha256(short): H8Zw/CnxF9d/uyzMmiGQZLfdfwozwdkunw1RUA+v+eqlx6DjO7hryjuIcQc=miGQZLfdfwoz

这是因为base64buf的地址空间连续,并且base64&buf[16],因此才会有第三次输出之后,sha256(short)的前16个字节相同的情况。

strncat的行为类似:

1
2
3
4
5
6
7
8
9
10
11
12
char*
strncat(char *dest, const char *src, size_t n)
{
size_t dest_len = strlen(dest);
size_t i;

for (i = 0 ; i < n && src[i] != '\0' ; i++)
dest[dest_len + i] = src[i];
dest[dest_len + i] = '\0';

return dest;
}

EVP_EncodeBlock会为输出自动加上一个NUL,因此自第三次输出开始,strncat的作用是在buf[16+strlen(base64)]后面添加12个字符。

总结如下:

  1. strcat(dst, src)是将字符串src拼接在dst后面
  2. 因为buf地址在base64之前且相邻,所以buf溢出后会使用base64的空间,不一定导致buffer overflow
  3. EVP_EncodeBlock的自动添加NULL行为,strncat的行为致使第三行起在base64[44]起添加12个字节与NUL

Step 2

字符串长度的定义,有两种:1. NUL('\0')结尾;2. 带有长度标识。C采用的是第一种。

C字符处理API中代有n的大都是为了解决缓冲器溢出(buffer overflow)问题而提出。但strncpystrncat并非以NUL截止。它们的手册页]这样写着:

strncpy:
Warning: If there is no null byte among the first n bytes of src the
string placed in dest will not be null-terminated.

strncat:
src does not need to be null-terminated if it contains n or more bytes.

Why does strncpy not null terminate?是个在StackOver Flow中提到的问题。LWN: The ups and downs of strlcpy()提到,BSD派生的strlcpystrlcat保证了NUL的存在,但因为截断数据可能导致安全性问题,它并没有进入glibc,并且gcc -D_FORTIFY_SOURCE能够捕获大部分strlcpystrlcat意在解决的问题。

Note: gcc -D_FORTIFY_SOURCE也是导致产生SIGABRT的原因之一。

Step 3

memcpymemmove也是string.h的API之二。

9月3日面试的时候,面试官提到了memcpy无法用在dst与src内存区域重叠(overlap)的场景下,指出了memmove便意在解决该问题。

dst与src的内存区域分为三种情况:

  1. dst与src未发生重叠
  2. dst的末端与src的起始发生重叠
  3. dst的起始与src的末端发生重叠

据此,memmove的实现思路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void *memmove(void *dst, const void *src, size_t n) {
unsigned long int dstp = (long int) dst;
unsigned long int srcp = (long int) src;

if (dstp - srcp >= len) { /* *unsigned* compare ! */
/* 符合情况1与2 */
memcpy(dest, src, n);
} else {
/* 负荷情况3 */
for (size_t i=n-1; i>=0; i--)
((char *)dst)[i] = ((char *)src)[i];
}
return dst;
}

类型转换

关于dstp - srcp >= len为什么符合情况1与2,参考demo:

1
2
3
4
5
6
7
8
9
int main(int argc, char *argv[]) {
unsigned int a = 10, b = 20;
size_t n = 5;

printf("%s\n", a-b>=n ? "yes" : "no");
printf("%s\n", a-b>=5 ? "yes" : "no");

return 0;
}

这是因为C类型转换的原因:size_t在AMD64下大小为8字节,相当于signed long int,它与unsigned int进行运算,都会被转换成unsigned long int。因为a-b小于0,计算机使用补码表示数字。因此任意的由signed long int表示的负数转换成unsigned long int后,都会比LONG_MAX大。

Reference