I'm trying to protect my application against buffer overflow exploits. Among other things, I'm using non-executable stacks and link my binaries with the noexecstack flag (by passing -Wl,-z,noexecstack to gcc).
Everything seems fine - readelf confirms that PT_GNU_STACK specifies correct permissions:
$ readelf -l target | grep -A1 GNU_STACK
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
So does execstack:
$ execstack -q target
- target
There's only one problem. All my stacks are actually executable:
root#170ubuntu16p04-64smp-1:~# cat /proc/12878/task/*/maps | grep stack
7ffcac654000-7ffcac675000 rwxp 00000000 00:00 0 [stack]
7fe540e66000-7fe541666000 rwxp 00000000 00:00 0 [stack]
7fe540665000-7fe540e65000 rwxp 00000000 00:00 0 [stack]
7fe53b800000-7fe53c000000 rwxp 00000000 00:00 0 [stack]
I've trapped allocate_stack calls and examined protection flags. In theory, they should be initialized according to PT_GNU_STACK. But in my case, it seems like PT_GNU_STACK was ignored and _dl_stack_flags was initialized with default permissions.
Does anyone know what could have caused this? Everything seems correct, but stacks are still executable.
I'm using gcc 4.8.3 / glibc 2.11.
Olaf and Employed Russian pushed me in the right direction. A third-party shared object was poisoning my stacks.
But it wasn't linked to my main executable directly. Both ldd and lddtree weren't showing any libraries with RWE stacks, so I've decided to dig deeper and wrote a script that checked all shared objects currently mapped into a process memory:
#!/bin/bash
if [ -z "$1" ]; then
echo "Usage: $0 <target>"
exit 1;
fi
kav_pid=`pidof $1`
for so in `cat /proc/$kav_pid/task/*/maps | awk '/.so$/ {print $6}' | sort | uniq`; do
stack_perms=`readelf -Wl $so | awk '/GNU_STACK/ {print $7}'`
if [ -z "$stack_perms" ]; then
echo "$so doesn't have PT_GNU_STACK"
elif [ "$stack_perms" != "RW" ]; then
echo "$so has unexpected permissions: $stack_perms"
fi
done
And it worked! I found a library with RWE permissions:
$ ./find_execstack.sh target
/target/dir/lib64/lib3rdparty.so has unexpected permissions: RWE
To make sure that it was this library that poisoned my stacks, I opened my application with gdb and set a breakpoint in dlopen. And Bingo! Here are the permissions before dlopening lib3rdparty.so:
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
And here are they right after the dlopen:
7ffffffde000-7ffffffff000 rwxp 00000000 00:00 0 [stack]
As it turned out, lib3rdparty.so was built using a different toolchain and that went unnoticed utill now.
Olaf, Employed Russian, thank you!
what could have caused this?
In addition to the main executable's PT_GNU_STACK having correct permissions, you also need to have PT_GNU_STACK with correct permissions in every directly-linked shared library.
If any one of these libraries does not have PT_GNU_STACK at all, or has one with executable permissions, it will "poison" all of your stacks with executable permission.
So run
for j in $(ldd target | grep -o '=> .* ' | sed -e 's/=> //' -e '/^ *$/d' ); do
out=$(readelf -Wl $j | grep STACK)
[[ -z "$out" ]] && echo "missing GNU_STACK in $j"
echo $out | grep -q RWE && echo "executable GNU_STACK in $j"
done
and you will likely see at least one library with missing or executable stack.
P.S. I see that Olaf has already (partially) suggested this.
Related
Can anyone explain why gcc is adding the .comment and .note.gnu.property sections in the object code, and how can I tell to gcc not to add them, is it possible?
Thanks.
why gcc is adding the .comment and .note.gnu.property sections
You can examine the contents of the .comment section with e.g. readelf -x.comment:
echo "int foo() { return 42; }" | gcc -xc - -c -o foo.o
readelf -x.comment foo.o
Hex dump of section '.comment':
0x00000000 00474343 3a202844 65626961 6e203131 .GCC: (Debian 11
0x00000010 2e322e30 2d313029 2031312e 322e3000 .2.0-10) 11.2.0.
Obviously the "why" is to make it easier to understand what compiler produced the object file.
I don't believe there is a GCC flag to suppress this, but objcopy --remove-section .comment foo.o bar.o will get rid of it.
The .note.gnu.property can be removed in similar fashion.
Here is a discussion of what it may contain.
tks makred it, objcopy -O binary -R .note -R .comment -R .note.gnu.property foo , elf format to binary, size from 129M to 4K
I have a number of object files in ELF format, with the usual .text and other common sections, and I was wondering if the gnu ld or gold could be used to link a number of ELF object files into an ELF executable, even if the architecture (an 8-bit micro with a proprietary toolchain) is not known beforehand by the linker. In essence I'm asking if the linking process is, to some extent, platform independent once you have all the required obect files, or if on the contrary I will need to roll my own linker at some point.
No, it won't work.
A major thing the linker has to do is to handle relocations. Relocations are arch-specific:
int f(){return 42;}
$ gcc -c foo.c -o foo && readelf -r foo
Relocation section '.rela.eh_frame' at offset 0x198 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
$ gcc -m32 -c foo.c -o foo && readelf -r foo
Relocation section '.rel.text' at offset 0x1d0 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00000004 00000b02 R_386_PC32 00000000 __x86.get_pc_thunk.ax
00000009 00000c0a R_386_GOTPC 00000000 _GLOBAL_OFFSET_TABLE_
Relocation section '.rel.eh_frame' at offset 0x1e0 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00000020 00000202 R_386_PC32 00000000 .text
00000040 00000502 R_386_PC32 00000000 .text.__x86.get_pc_thu
$ clang -target arm-linux-gnueabi -c foo.c -o foo && readelf -r foo
Relocation section '.rel.ARM.exidx' at offset 0x104 contains 1 entry:
Offset Info Type Sym.Value Sym. Name
00000000 0000032a R_ARM_PREL31 00000000 .text
Moreover the linker script which says how the ELF file should be generated (page size, start address, etc.) is arch-specific:
ld -m elf_x86_64 --verbose
ld -m elf_i386 --verbose
arm-linux-gnueabi-ld --verbose
If your not compiling to a static executable, the linker has to generate PLT entries as well which are native code (and thus arch-specific).
Some architecture have arch-specific segments as well (eg. .ARM.extab, .ARM.exidx).
I am doing cross compile debugging.
My build server CPU is amd64. My device CPU is MIPS.
When I am trying to do debug the elf file compiled by myself. The gdb can only show ld.so.1
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
0x7704f9c0 0x7706c490 Yes (*) /lib/ld.so.1
(*): Shared library is missing debugging information.
(gdb) q
I checked the /proc/xxxx/maps file. It showed that the shared libraries are loaded.
root#TRA:/proc/13679# cat maps
......
76549000-76d48000 rwxp 00000000 00:00 0 [stack:13682]
76d48000-76d4a000 r-xp 00000000 00:0c 5268 /usr/lib/strongswan/plugins/libstrongswan-addrblock.so
76d4a000-76d59000 ---p 00002000 00:0c 5268 /usr/lib/strongswan/plugins/libstrongswan-addrblock.so
......
If I debug the file which is installed from Debian Package server, then GDB can show all the shared libraries.
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
0x77341bc0 0x77342c80 Yes (*) /lib/mips-linux-gnu/libdl.so.2
0x771d77e0 0x772ff6f0 Yes (*) /lib/mips-linux-gnu/libc.so.6
0x773549c0 0x77371490 Yes (*) /lib/ld.so.1
(*): Shared library is missing debugging information.
(gdb)
GDB version is:
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
My questions is:
Why the GDB command 'info sharedlibrary' can't show all the libraries? How can I fix it?
(EDIT)
(Does every executable file need the library ld.so? It is missing.)
The output of command "mips-linux-gnu-readelf -d src/charon/.libs/charon"
Dynamic section at offset 0x1fc contains 33 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libstrongswan.so.0]
0x00000001 (NEEDED) Shared library: [libhydra.so.0]
0x00000001 (NEEDED) Shared library: [libcharon.so.0]
0x00000001 (NEEDED) Shared library: [libm.so.6]
0x00000001 (NEEDED) Shared library: [libpthread.so.0]
0x00000001 (NEEDED) Shared library: [libdl.so.2]
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000001d (RUNPATH) Library runpath: [/usr/lib/strongswan]
0x0000000c (INIT) 0xd00
0x0000000d (FINI) 0x2eb0
0x00000004 (HASH) 0x32c
0x00000005 (STRTAB) 0x904
0x00000006 (SYMTAB) 0x4d4
0x0000000a (STRSZ) 787 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x70000035 (MIPS_RLD_MAP_REL) 0x134dc
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x13760
0x00000011 (REL) 0xcf0
0x00000012 (RELSZ) 16 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x70000001 (MIPS_RLD_VERSION) 1
0x70000005 (MIPS_FLAGS) NOTPOT
0x70000006 (MIPS_BASE_ADDRESS) 0x0
0x7000000a (MIPS_LOCAL_GOTNO) 18
0x70000011 (MIPS_SYMTABNO) 67
0x70000012 (MIPS_UNREFEXTNO) 37
0x70000013 (MIPS_GOTSYM) 0x11
0x6ffffffb (FLAGS_1) Flags: PIE
0x6ffffffe (VERNEED) 0xca0
0x6fffffff (VERNEEDNUM) 2
0x6ffffff0 (VERSYM) 0xc18
0x00000000 (NULL) 0x0
EDIT
Debuging GDB:
The gdb query ‘qXfer:libraries-svr4:read’ returned empty library list.
Breakpoint 7, svr4_current_sos_via_xfer_libraries (list=0x7fff8be59ad0, annex=<optimized out>)
at /gdb/gdb-7.11.1/gdb/solib-svr4.c:1301
1301 result = svr4_ parse_libraries (svr4_library_document, list);
1: svr4_library_document = 0x15cd9c0 "<library-list-svr4 version=\"1.0\"/>"
(gdb)
For Debian packages which are not compiled by me, the gdb query ‘qXfer:libraries-svr4:read’ returned full shared library list.
How does gdbserver construct the reply of this query ‘qXfer:libraries-svr4:read’?
EDIT
One more clue:
The pkgs installed from debian Jessie distribute is not PIE code.
The code I compiled is PIE code.
root#TRA:/proc/14956# readelf -r /usr/lib/strongswan/charon
Relocation section '.rel.dyn' at offset 0xcf0 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00000000 00000000 R_MIPS_NONE
00013870 00000003 R_MIPS_REL32
root#TRA:/proc/14956# readelf -r /usr/bin/id
There are no relocations in this file.
root#TRA:/proc/14956#
EDIT
After debugging gdbserver, I found one strange info.
The DT_DEBUG entry of the running proc is 0. After loader relocate the code, the DT_DEBUG should not be 0.(?) Does the system not support PIE code? I am using Debian Jessie MIPS system.
gdbserver source code:
if (dyn->d_tag == DT_DEBUG && map == -1)
map = dyn->d_un.d_val;
gdbserver dbg print
(gdb) p *dyn
$19 = {d_tag = 21, d_un = {d_val = 0, d_ptr = 0}}
(gdb)
EDIT
Get some information from this link:
https://sourceware.org/ml/binutils/2015-06/msg00166.html
I installed gdbserver from Debian Jessie mips-pkg server. But it seems not support PIE. Where can I install the mips-gdbserver which can support PIE?
Or how can I disable the gcc compiler generate PIE code?
I tried these flags (-fno-pie -fPIC) in cross-compile, but it still generate PIE code.
libtool: link: mips-linux-gnu-gcc -mfp32 -fno-pie -fPIC
-ggdb -O0 -Wall -Wno-format -Wno-format-security
-Wno-pointer-sign -I/cross-mips/usr/include -I/cross-mips/usr/include/libnl3
-I/cross-mips/usr/include/mips-linux-gnu
-I/work/strongswan/src/util
-include /work/strongswan/config.h
-o .libs/charon charon.o -L/cross-mips/lib/mips-linux-gnu
- L/cross-mips -L/cross-mips/usr/lib/mips-linux-gnu
../../src/libstrongswan/.libs/libstrongswan.so
-lm -lpthread -ldl -Wl,-rpath -Wl,/usr/lib/strongswan
Check the generated code:
mips-linux-gnu-readelf -r src/charon/.libs/charon
Relocation section '.rel.dyn' at offset 0xcf0 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00000000 00000000 R_MIPS_NONE
00013870 00000003 R_MIPS_REL32
Solution
Unfortunately the reason is my compiler gcc-6 is brocken. I used 'gcc version 6.3.0 20170516 (Debian 6.3.0-18)'. It is configured with '--enable-default-pie'. And there is no way to disable PIE. And this PIE breaks static library links. I have to change my compiler to gcc5.
From the info you provided, it seems that there are two likely causes:
Either you fully strip your binary, and gdbserver requires some symbol, or
You are building a PIE binary, and gdbserver on your system doesn't support such binaries.
(It's also possible that it's the combination of 1 and 2 that causes the problem.)
Since you know that the distribution binaries work, your best bet is probably to understand the differences between them and your binary, and minimizing such differences until gdbserver starts working.
I'm trying to use this answer by writing a custom target .json file with "linker-flavor":"gcc". My full target .json file is:
{
"llvm-target": "avr-atmel-none",
"cpu": "atmega328p",
"target-endian": "little",
"target-pointer-width": "16",
"os": "none",
"target-env": "gnu",
"target-vendor": "unknown",
"arch": "avr",
"data-layout": "e-p:16:16:16-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-n8",
"executables": true,
"linker": "avr-gcc",
"linker-flavor": "gcc",
"pre-link-args": {
"gcc": ["-Os -mmcu=atmega328p"]
},
"exe-suffix": ".elf",
"post-link-args": {
"gcc": ["-Wl,--gc-sections"]
},
"no-default-libraries": false
}
Running cargo build with this finishes without any error messages:
$ cargo build --release -v
Compiling core v0.1.0 (https://github.com/gergoerdi/rust-avr-libcore-mini?rev=adda44aa91ac517aab6915447592ee4cad26564c#adda44aa)
Running `rustc --crate-name core /home/cactus/.cargo/git/checkouts/rust-avr-libcore-mini-37e279d93a70b45a/adda44a/src/lib.rs --crate-type lib --emit=dep-info,link -C opt-level=3 -C metadata=655bb622dd229da9 -C extra-filename=-655bb622dd229da9 --out-dir /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps --target avr-atmega328p -L dependency=/home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps -L dependency=/home/cactus/prog/rust/avr/chip8-avr/target/release/deps --cap-lints allow`
Compiling chip8-engine v0.1.0 (https://github.com/gergoerdi/rust-avr-chip8-engine?rev=c6f88737bae4dae0bd6c5c2bbc73737e6dfadfcd#c6f88737)
Running `rustc --crate-name chip8_engine /home/cactus/.cargo/git/checkouts/rust-avr-chip8-engine-4bce60f3f178d33a/c6f8873/src/lib.rs --crate-type lib --emit=dep-info,link -C opt-level=3 -C metadata=2197ff1f15f697c9 -C extra-filename=-2197ff1f15f697c9 --out-dir /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps --target avr-atmega328p -L dependency=/home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps -L dependency=/home/cactus/prog/rust/avr/chip8-avr/target/release/deps --extern core=/home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/libcore-655bb622dd229da9.rlib --cap-lints allow`
Compiling chip8-avr v0.1.0 (file:///home/cactus/prog/rust/avr/chip8-avr)
Running `rustc --crate-name chip8_avr src/main.rs --crate-type bin --emit=dep-info,link -C opt-level=3 -C metadata=014a8fed19cbc611 -C extra-filename=-014a8fed19cbc611 --out-dir /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps --target avr-atmega328p -L dependency=/home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps -L dependency=/home/cactus/prog/rust/avr/chip8-avr/target/release/deps --extern chip8_engine=/home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/libchip8_engine-2197ff1f15f697c9.rlib --extern core=/home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/libcore-655bb622dd229da9.rlib`
Finished release [optimized] target(s) in 15.99 secs
However, the resulting ELF file's .text section is empty:
$ avr-objdump -h target/avr-atmega328p/release/chip8-avr.elf
target/avr-atmega328p/release/chip8-avr.elf: file format elf32-avr
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 00000000 00000000 00000074 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00800060 00000000 00000074 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .stab 0000012c 00000000 00000000 00000074 2**2
CONTENTS, READONLY, DEBUGGING
3 .stabstr 0000005d 00000000 00000000 000001a0 2**0
CONTENTS, READONLY, DEBUGGING
4 .comment 00000011 00000000 00000000 000001fd 2**0
CONTENTS, READONLY
So to figure out what's going on, I thought I'd replace my avr-gcc
with a small shellscript that logs its arguments before passing it to
the real avr-gcc executable.
This shows me that rustc/cargo is trying to run the following
command line to do the linking:
/usr/bin/avr-gcc -Os -mmcu=atmega328p \
-L /home/cactus/prog/rust/rust-avr/build/build/x86_64-unknown-linux-gnu/stage1/lib/rustlib/avr-atmega328p/lib \
/home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/chip8_avr-014a8fed19cbc611.0.o \
-o /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/chip8_avr-014a8fed19cbc611.elf \
-Wl,--gc-sections \
-L /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps -L /home/cactus/prog/rust/avr/chip8-avr/target/release/deps -L /home/cactus/prog/rust/rust-avr/build/build/x86_64-unknown-linux-gnu/stage1/lib/rustlib/avr-atmega328p/lib \
-Wl,-Bstatic /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/libchip8_engine-2197ff1f15f697c9.rlib \
/home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/libcore-655bb622dd229da9.rlib \
-Wl,-Bdynamic -Wl,--gc-sections
If I run the exact same command manually, with the exact same environment variables, I get a good ELF file with
the right contents (note that its .text section is not empty):
$ /usr/bin/avr-gcc -Os -mmcu=atmega328p -L /home/cactus/prog/rust/rust-avr/build/build/x86_64-unknown-linux-gnu/stage1/lib/rustlib/avr-atmega328p/lib /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/chip8_avr-014a8fed19cbc611.0.o -o /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/chip8_avr-014a8fed19cbc611.elf -Wl,--gc-sections -L /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps -L /home/cactus/prog/rust/avr/chip8-avr/target/release/deps -L /home/cactus/prog/rust/rust-avr/build/build/x86_64-unknown-linux-gnu/stage1/lib/rustlib/avr-atmega328p/lib -Wl,-Bstatic /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/libchip8_engine-2197ff1f15f697c9.rlib /home/cactus/prog/rust/avr/chip8-avr/target/avr-atmega328p/release/deps/libcore-655bb622dd229da9.rlib -Wl,-Bdynamic -Wl,--gc-sections
$ avr-objdump -h target/avr-atmega328p/release/deps/chip8_avr-014a8fed19cbc611.elf
target/avr-atmega328p/release/deps/chip8_avr-014a8fed19cbc611.elf: file format elf32-avr
Sections:
Idx Name Size VMA LMA File off Algn
0 .data 0000020e 00800100 00001a56 00001af0 2**4
CONTENTS, ALLOC, LOAD, DATA
1 .text 00001a56 00000000 00000000 00000094 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .bss 000001fa 0080030e 0080030e 00001cfe 2**0
ALLOC
3 .stab 000007ec 00000000 00000000 00001d00 2**2
CONTENTS, READONLY, DEBUGGING
4 .stabstr 000000b0 00000000 00000000 000024ec 2**0
CONTENTS, READONLY, DEBUGGING
5 .comment 00000011 00000000 00000000 0000259c 2**0
CONTENTS, READONLY
So why does cargo silently produce a nonsensical empty ELF file, if
running the (supposedly) same command from the shell results in a valid ELF file?
This is caused by a bug in the target .json file; specifically, this part:
"pre-link-args": {
"gcc": ["-Os -mmcu=atmega328p"]
},
The arguments are passed directly as argv to the linker, so multiple arguments need to be split into multiple elements of the array here:
"pre-link-args": {
"gcc": ["-Os", "-mmcu=atmega328p"]
},
The reason this problem didn't show up when using the special logging version of avr-gcc is that the log only contained all the arguments together, so there was no difference between the two representations.
As for avr-gcc '-Os -mmcu=atmega328p' creating an empty .elf file, it seems that is simply a side-effect of not specifying any (valid) -mmcu argument.
Context: I am currently debugging an issue where the binaries generated in one machine (liked with lpthread) causes pthread related bugs when tried in another machine.
libtest.so is a shared library which seems to contain multiple versions of GLIBC_ . Is that expected ?. How it happens ? It was linked using "-shared -lpthread -fPIC -soname=xxxx" option.
$objdump -T libtest.so | grep GLIBC_
...
00000000 DF *UND* 0000008d GLIBC_2.1 popen
...
00000000 DF *UND* 0000002c GLIBC_2.0 syslog
00000000 DF *UND* 00000020 GLIBC_2.0 pthread_exit
00000000 DF *UND* 0000009f GLIBC_2.0 __xstat
00000000 DF *UND* 000000bb GLIBC_2.3.2 pthread_cond_signal
00000000 DF *UND* 000000c9 GLIBC_2.0 vsprintf
...
Each symbol has its own history.
When a symbol hasn't be modified (signature, behavor) it keeps the default version eg. GLIBC_2.0.
Symbols modified are attributed the current version of the library at that time, for example popen() behavor was modified in GLIBC_2.1, pthread_cond_signal() was modified in GLIBC_2.3.2.
Your program get linked with the latest version of each symbol. The version is recorded, and if you run your program against a newer GLIBC, your program will not use newer symbol version, but the one available at link time: this ensure to have the expected behavor at runtime: no surprise.
That is because those methods exist in different .so's. glibc is a collection of .so's. I believe pthread_exit is in libpthread.so and popen is in libc.so