在Unix上启用优化时,strcpy()/ strncpy()在具有额外空间的结构成员上崩溃? [英] strcpy()/strncpy() crashes on structure member with extra space when optimization is turned on on Unix?
问题描述
在编写项目时,我遇到了一个奇怪的问题。
这是我设法编写的用于重新创建问题的最少代码。我故意将实际的字符串存储在其他地方,并分配了足够的空间。
// #include< stdio.h>
#include< stdlib.h>
#include< string.h>
#include< stdint.h>
#include< stddef.h> //对于offsetof()
typedef struct _pack {
//`c`的类型并不重要,只要它在结构内部即可。
int64_t c;
}包;
int main(){
pack * p;
char str [9] = aaaaaaaa; //输入
size_t len = offsetof(pack,c)+(strlen(str)+1);
p = malloc(len);
//版本1:崩溃
strcpy((char *)&(p-> c),str);
//版本2:崩溃
strncpy((char *)&(p-> c),str,strlen(str)+1);
//版本3:有效!
memcpy((char *)&(p-> c),str,strlen(str)+1);
// puts((char *)&(p-> c));
free(p);
返回0;
}
上面的代码使我感到困惑:
- 使用
gcc / clang -O0
,strcpy()
和memcpy ()
在Linux / WSL上工作,下面的puts()
给出了我输入的内容。 - 在OSX上使用
clang -O0
,代码会因strcpy()
而崩溃。 - 在Ubuntu /上使用
gcc / clang -O2
或-O3
Fedora / WSL ,代码崩溃 (!!)在strcpy()
,而memcpy()
效果很好。 - 在Windows上使用
gcc.exe
,代码无论优化级别如何,效果都很好。
我还发现了代码的其他一些特征:
-
(看起来)重现崩溃的最小输入为9个字节(包括零终止符),或
1 + sizeof(p-> ; c)
。使用该长度(或更长)可以保证发生崩溃(亲爱的我...)。
-
即使我分配了额外的空间(最多1MB)放在
malloc()
中,没有帮助。上面的行为完全没有改变。
-
strncpy()
的行为完全相同
-
指针似乎无关紧要。如果结构成员
char * c
更改为long long c
(或int64_t
),其行为保持不变。 (更新:已更改)。
-
崩溃消息看起来不正常。
我尝试了所有这些编译器,但它们没有什么区别:
- GCC 5.4.0(Ubuntu / Fedora / OS X / WSL,全部是64位)
- GCC 6.3.0(仅适用于Ubuntu)
- GCC 7.2.0(Android,norepro ???)(这是来自
PS最初,我想编写一个结构,其最后一项是指向动态分配空间(用于字符串)的指针。将结构写入文件时,无法编写指针。我必须写出实际的字符串。所以我想出了一个解决方案:强制将字符串存储在指针位置。
也请不要抱怨
gets()
。我不在项目中使用它,而是仅上面的示例代码。解决方案我复制了此问题在我的Ubuntu 16.10上,我发现了一些有趣的东西。
当用
gcc -O3 -o ./test ./test.c$编译时c $ c>,如果输入超过8个字节,程序将崩溃。
经过一些反转后,我发现GCC替换了
strcpy
和 memcpy_chk ,请参见此。//从IDA $ b $反编译b int __cdecl main(int argc,const char ** argv,const char ** envp)
{
int * v3; // rbx
int v4; // edx
unsigned int v5; // eax
签署了__int64 v6; // rbx
char * v7; // rax
void * v8; // r12
const char * v9; // rax
__int64 _0; // [rsp + 0h] [rbp + 0h]
unsigned __int64 vars408; // [rsp + 408h] [rbp + 408h]
vars408 = __readfsqword(0x28u);
v3 =(int *)& _0;
gets(& _0,argv,envp);
do
{
v4 = * v3;
++ v3;
v5 =〜v4& (v4-16843009)& 0x80808080;
}
而(!v5);
if(!((unsigned __int16)〜(_WORD)v4&(unsigned __int16)(v4-257)& 0x8080))
v5> == 16;
if(!((unsigned __int16)〜(_WORD)v4&(unsigned __int16)(v4-257)& 0x8080))
v3 =(int *)((char *)v3 + 2);
v6 =(char *)v3-__CFADD __((_ BYTE)v5,(_BYTE)v5)-3-(char *)& _0; // strlen
v7 =(char *)malloc(v6 + 9);
v8 = v7;
v9 =(const char *)_ memcpy_chk(v7 + 8,& _0,v6 + 1,8LL); // Forth参数为8!
puts(v9);
free(v8);
返回0;
}
您的结构包使GCC相信元素
c
恰好是8个字节。
如果复制长度为
memcpy_chk
将会失败。比第四个参数大!
所以有两种解决方案:
-
修改结构
-
使用编译选项
-D_FORTIFY_SOURCE = 0
(例如gcc test.c -O3 -D_FORTIFY_SOURCE = 0 -o ./test
)关闭强化功能。
警告:这将完全禁用整个程序中的缓冲区溢出检查!
When writing a project, I ran into a strange issue.
This is the minimal code I managed to write to recreate the issue. I am intentionally storing an actual string in the place of something else, with enough space allocated.
// #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> // For offsetof() typedef struct _pack{ // The type of `c` doesn't matter as long as it's inside of a struct. int64_t c; } pack; int main(){ pack *p; char str[9] = "aaaaaaaa"; // Input size_t len = offsetof(pack, c) + (strlen(str) + 1); p = malloc(len); // Version 1: crash strcpy((char*)&(p->c), str); // Version 2: crash strncpy((char*)&(p->c), str, strlen(str)+1); // Version 3: works! memcpy((char*)&(p->c), str, strlen(str)+1); // puts((char*)&(p->c)); free(p); return 0; }
The above code is confusing me:
- With
gcc/clang -O0
, bothstrcpy()
andmemcpy()
works on Linux/WSL, and theputs()
below gives whatever I entered. - With
clang -O0
on OSX, the code crashes withstrcpy()
. - With
gcc/clang -O2
or-O3
on Ubuntu/Fedora/WSL, the code crashes (!!) atstrcpy()
, whilememcpy()
works well. - With
gcc.exe
on Windows, the code works well whatever the optimization level is.
Also I found some other traits of the code:
(It looks like) the minimum input to reproduce the crash is 9 bytes (including zero terminator), or
1+sizeof(p->c)
. With that length (or longer) a crash is guaranteed (Dear me ...).Even if I allocate extra space (up to 1MB) in
malloc()
, it doesn't help. The above behaviors don't change at all.strncpy()
behaves exactly the same, even with the correct length supplied to its 3rd argument.The pointer does not seem to matter. If structure member
char *c
is changed intolong long c
(orint64_t
), the behavior remains the same. (Update: changed already).The crash message doesn't look regular. A lot of extra info is given along.
I tried all these compilers and they made no difference:
- GCC 5.4.0 (Ubuntu/Fedora/OS X/WSL, all are 64-bit)
- GCC 6.3.0 (Ubuntu only)
- GCC 7.2.0 (Android, norepro???) (This is the GCC from C4droid)
- Clang 5.0.0 (Ubuntu/OS X)
- MinGW GCC 6.3.0 (Windows 7/10, both x64)
Additionally, this custom string copy function, which looks exactly like the standard one, works well with any compiler configuration mentioned above:
char* my_strcpy(char *d, const char* s){ char *r = d; while (*s){ *(d++) = *(s++); } *d = '\0'; return r; }
Questions:
- Why does
strcpy()
fail? How can it? - Why does it fail only if optimization is on?
- Why doesn't
memcpy()
fail regardless of-O
level??
*If you want to discuss about struct member access violation, pleast head over here.
Part of
objdump -d
's output of a crashing executable (on WSL):
P.S. Initially I want to write a structure, the last item of which is a pointer to a dynamically allocated space (for a string). When I write the struct to file, I can't write the pointer. I must write the actual string. So I came up with this solution: force store a string in the place of a pointer.
Also please don't complain about
gets()
. I don't use it in my project, but the example code above only.解决方案I reproduced this issue on my Ubuntu 16.10 and I found something interesting.
When compiled with
gcc -O3 -o ./test ./test.c
, the program will crash if the input is longer than 8 bytes.After some reversing I found that GCC replaced
strcpy
with memcpy_chk, see this.// decompile from IDA int __cdecl main(int argc, const char **argv, const char **envp) { int *v3; // rbx int v4; // edx unsigned int v5; // eax signed __int64 v6; // rbx char *v7; // rax void *v8; // r12 const char *v9; // rax __int64 _0; // [rsp+0h] [rbp+0h] unsigned __int64 vars408; // [rsp+408h] [rbp+408h] vars408 = __readfsqword(0x28u); v3 = (int *)&_0; gets(&_0, argv, envp); do { v4 = *v3; ++v3; v5 = ~v4 & (v4 - 16843009) & 0x80808080; } while ( !v5 ); if ( !((unsigned __int16)~(_WORD)v4 & (unsigned __int16)(v4 - 257) & 0x8080) ) v5 >>= 16; if ( !((unsigned __int16)~(_WORD)v4 & (unsigned __int16)(v4 - 257) & 0x8080) ) v3 = (int *)((char *)v3 + 2); v6 = (char *)v3 - __CFADD__((_BYTE)v5, (_BYTE)v5) - 3 - (char *)&_0; // strlen v7 = (char *)malloc(v6 + 9); v8 = v7; v9 = (const char *)_memcpy_chk(v7 + 8, &_0, v6 + 1, 8LL); // Forth argument is 8!! puts(v9); free(v8); return 0; }
Your struct pack makes GCC believe that the element
c
is exactly 8 bytes long.And
memcpy_chk
will fail if the copying length is larger than the forth argument!So there are 2 solutions:
Modify your structure
Using compile options
-D_FORTIFY_SOURCE=0
(likesgcc test.c -O3 -D_FORTIFY_SOURCE=0 -o ./test
) to turn off fortify functions.Caution: This will fully disable buffer overflow checking in the whole program!!
这篇关于在Unix上启用优化时,strcpy()/ strncpy()在具有额外空间的结构成员上崩溃?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!
-