20

I'm experimenting with the concept of pure-static-linked PIE executables on Linux, but running into the problem that the GNU binutils linker insists on adding a PT_INTERP header to the output binary when -pie is used, even when also given -static. Is there any way to inhibit this behavior? That is, is there a way to tell GNU ld specifically not to write certain headers to the output file? Perhaps with a linker script?

(Please don't answer with claims that it won't work; I'm well aware that the program still needs relocation processing - load-address-relative relocations only due to my use of -Bsymbolic - and I have special startup code in place of the standard Scrt1.o to handle this. But I can't get it to be invoked without the dynamic linker already kicking in and doing the work unless hexedit the PT_INTERP header out of the binary.)

R.. GitHub STOP HELPING ICE
  • 195,354
  • 31
  • 331
  • 669
  • let me see if I have this straight. you are specifying your own entry point, which is in turn handling some custom relocation, and you don't want the kernel to load in the standard interpreter? what if you're linking against libraries which need an initialization run via .init? in my experience if you want to do something with your executables but there's no way to generate it with some permutation of LDFLAGS then it's not a good idea. – Max DeLiso May 09 '12 at 09:23
  • 1
    If I were trying to put this in an application's build system, I would agree with you 100%. For an application to fool around with linker options like this is a horrible hack, and it wouldn't work anyway because it requires all `.a` libraries to be built as PIC. However, what I'm working on is a new toolchain option intended for use in security-oriented distributions where having the dynamic linker run for setuid binaries is an unacceptable risk. It's a lot easier to deploy if no changes are needed at the `ld` level, only at the gcc specfile and `crt` level. – R.. GitHub STOP HELPING ICE May 09 '12 at 13:39
  • it look's like you'll have to write a patch for ld and then argue with them over why it should be applied to trunk. also, that sounds like very interesting work. – Max DeLiso May 11 '12 at 01:56
  • 1
    If it's possible with a linker script, that would be less ideal than just a command line option, but much better than internal patching. An answer from somebody well-versed in linker scripts would be much appreciated. – R.. GitHub STOP HELPING ICE May 11 '12 at 02:33
  • Do you need to omit it in the first place? i.e. won't a post-build Make (or whatever you prefer) step to strip the `PT_INTERP` header suffice? – Mahmoud Al-Qudsi May 11 '12 at 03:22
  • Again that would be perfectly acceptable if I were trying to build an application. It's not acceptable since what I'm building is a way to build existing applications. Adding `-staticpie` or `-pie -static` or whatnot to `LDFLAGS` is trivial to use with nearly any build system. Running extra commands on each generated binary is absolutely not possible in a general way. – R.. GitHub STOP HELPING ICE May 11 '12 at 03:31

5 Answers5

13

Maybe I'm being naïve, but... woudn't suffice to search for the default linker script, edit it, and remove the line that links in the .interp section?

For example, in my machine the scripts are in /usr/lib/ldscripts and the line in question is interp : { *(.interp) } in the SECTIONS section.

You can dumpp the default script used running the following command:

$ ld --verbose ${YOUR_LD_FLAGS} | \
    gawk 'BEGIN { s = 0 } { if ($0 ~ /^=/) s = !s; else if (s == 1) print; }'

You can modify the gawk script slightly to remove the interp line (or just use grep -v and use that script to link your program.

rodrigo
  • 79,651
  • 7
  • 121
  • 162
  • So far this is probably the best approach. Unfortunately it requires making new versions of the linker scripts for each system, and can't just piggyback onto an existing working linker script. But if I can get the proof of concept working well, perhaps I can get it in upstream binutils. – R.. GitHub STOP HELPING ICE May 18 '12 at 02:13
  • @R.. - Well, my trick of using `strace` does not work because the script is actually compiled into `ld` and not read from disk. But you can get it using `ld --verbose` and a bit of magic in the output (see the updated answer. – rodrigo May 18 '12 at 12:59
  • From this code, you can make the ld wrapper, just as some do for [gcc](http://users.sdsc.edu/~kst/gcc-wrapper/). – alexander May 18 '12 at 18:48
12

I think I might have found a solution: simply using -shared instead of -pie to make pie binaries. You need a few extra linker options to patch up the behavior, but it seems to avoid the need for a custom linker script. Or in other words, the -shared linker script is already essentially correct for linking static pie binaries.

If I get it working with this, I'll update the answer with the exact command line I'm using.

Update: It works! Here's the command line:

gcc -shared -static-libgcc -Wl,-static -Wl,-Bsymbolic \
    -nostartfiles -fPIE Zcrt1.s Zcrt2.c /usr/lib/crti.o hello.c /usr/lib/crtn.o

where Zcrt1.s is a modified version of Scrt1.s that calls a function in Zcrt2.c before doing its normal work, and the code in Zcrt2.c processes the aux vector just past the argv and environment arrays to find the DYNAMIC section, then loops over the relocation tables and applies all the relative-type relocations (the only ones that should exist).

Now all of this can (with a little work) be wrapped up into a script or gcc specfile...

R.. GitHub STOP HELPING ICE
  • 195,354
  • 31
  • 331
  • 669
4

Expanding on my earlier note as this doesn't fit in that puny box (and this is just as an idea or discussion, please do not feel obligated to accept or reward bounty), perhaps the easiest and cleanest way of doing this is to juts add a post-build step to strip the PT_INTERP header from the resulting binary?

Even easier than manually editing the headers and potentially having to shift everything around is to just replace PT_INTERP with PT_NULL. I don't know whether you can find a way of simply patching the file via existing tools (some sort of scriptable hex find and replace) or if you'll have to write a small program to do that. I do know that libbfd (the GNU Binary File Descriptor library) might be your friend in the latter case, as it'll make that entire business a lot easier.

I guess I just don't understand why it's important to have this performed via an ld option. If available, I can see why it would be preferable; but as some (admittedly light) Googling indicates there isn't such a feature, it might be less of a hassle to just do it separately and after-the-fact. (Perhaps adding the flag to ld is easier than scripting the replacement of PT_INTERP with PT_NULL, but convincing the devs to pull it upstream is a different matter.)


Apparently (and please correct me if this is something you've already seen) you can override the behavior of ld with regards to any of the ELF headers in your linker script with the PHDRS command, and using :none to specify that a particular header type should not be included in any segment. I'm not certain of the syntax, but I presume it would look something like this:

PHDRS
{
  headers PT_PHDR PHDRS ;
  interp PT_INTERP ;
  text PT_LOAD FILEHDR PHDRS ;
  data PT_LOAD ;
  dynamic PT_DYNAMIC ;
}

SECTIONS
{
  . = SIZEOF_HEADERS;
  .interp : { } :none
  ...
}

From the ld docs you can override the linker script with --library-path:

--library-path=searchdir

Add path searchdir to the list of paths that ld will search for archive libraries and ld control scripts. You may use this option any number of times. The directories are searched in the order in which they are specified on the command line. Directories specified on the command line are searched before the default directories. All -L options apply to all -l options, regardless of the order in which the options appear. The default set of paths searched (without being specified with `-L') depends on which emulation mode ld is using, and in some cases also on how it was configured. See section Environment Variables. The paths can also be specified in a link script with the SEARCH_DIR command. Directories specified this way are searched at the point in which the linker script appears in the command line.

Also, from the section on Implicit Linker Scripts:

If you specify a linker input file which the linker can not recognize as an object file or an archive file, it will try to read the file as a linker script. If the file can not be parsed as a linker script, the linker will report an error.

Which would seem to imply values in user-defined linker scripts, in contrast with implicitly defined linker scripts, will replace values in the default scripts.

Mahmoud Al-Qudsi
  • 26,006
  • 12
  • 71
  • 118
  • 1
    This is easy to do for experimenting, and in fact it's what I did for experimenting. But it doesn't help at all when the goal is to build a toolchain whereby you can drop some extra options in `CFLAGS` and `LDFLAGS` and get any program to build as static pie. – R.. GitHub STOP HELPING ICE May 11 '12 at 03:32
  • Barring an upstream patch of ld to provide support for omitting `PT_INTERP` (assuming one doesn't already exist, that is), I don't think there's any alternative that can create a generic toolchain.. – Mahmoud Al-Qudsi May 11 '12 at 03:35
  • I've found some info on overriding the default behavior for any ELF header and updated my answer - is this applicable to you? – Mahmoud Al-Qudsi May 11 '12 at 03:52
  • 1
    This looks promising. Is there a way to chain this onto existing linker scripts ld is already using without copying/duplicating all the stuff already in them? – R.. GitHub STOP HELPING ICE May 11 '12 at 04:14
  • 1
    There's both user-defined and implicitly-defined linker scripts. I think one or the other will do what you need, because I am unsure of what the meaning of "override" in the ld docs actually is. See updated post. – Mahmoud Al-Qudsi May 11 '12 at 04:24
  • @R.. - When you add a script in the linker command, it will be appended to the default script. But AFAIK you cannot delete or modify lines from the default script without replacing it altogether (with the `-T` option). – rodrigo May 16 '12 at 19:52
  • Thanks for all the help and ideas. I eventually found a solution. – R.. GitHub STOP HELPING ICE May 28 '12 at 03:38
3

I'am not an expert in GNU ld, but I have found the following information in the documentation:

The special secname `/DISCARD/' may be used to discard input sections. Any sections which are assigned to an output section named `/DISCARD/' are not included in the final link output.

I hope this will help you.

UPDATE:

(This is the first version of the solution, which don't work because INTERP section is dropped along with the header PT_INTERP.)

main.c:

int main(int argc, char **argv)                                                                                                                               
{                                                                                                                                                             
    return 0;                                                                                                                                                 
}

main.x:

SECTIONS {                                                                                                                                                    
    /DISCARD/ : { *(.interp) }                                                                                                                                
}

build command:

$ gcc -nostdlib -pie -static -Wl,-T,main.x main.c
$ readelf -S a.out | grep .interp

build command without option -Wl,-T,main.x:

$ gcc -nostdlib -pie -static main.c 
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000000218
$ readelf -S a.out | grep .interp
  [ 1] .interp           PROGBITS        00000134 000134 000013 00   A  0   0  1

UPDATE 2:

The idea of this solution is that the original section 'INTERP' (. interp in the linker script file) is renamed to .interp1. In other words, the entire contents of the section is placed to the .interp1 section. Therefore, we can safe remove INTERP section (now empty) without fear of losing default linker script settings and hence the header INTERP_PT will be removed too.

SECTIONS {
    .interp1 : { *(.interp); } : NONE
    /DISCARD/ : { *(.interp) }
}

In order to show that the contents of the section INTERP present in the file (as .interp1), but INTERP_PT header removed, I use a combination of readelf + grep.

$ gcc -nostdlib -pie -Wl,-T,main.x main.c
$ readelf -l a.out | grep interp
   00     .note.gnu.build-id .text .interp1 .dynstr .hash .gnu.hash .dynamic .got.plt 
$ readelf -S a.out | grep interp
  [ 3] .interp1          PROGBITS        0000002e 00102e 000013 00   A  0   0  1
pts
  • 64,123
  • 15
  • 92
  • 159
alexander
  • 2,583
  • 14
  • 13
  • I tried using this with `.interp`, but couldn't get it to work, perhaps since it's created by the default linker script and not the source files. Do you have an idea how it should be written? – R.. GitHub STOP HELPING ICE May 18 '12 at 01:50
  • Well it's `-l` not `-S` you need to look at (program headers). You're right that this inhibits the `INTERP` header, but it also seems to cause a single bogus `LOAD` header to get generated instead of the correct ones (whole program is loaded read-write) so it seems to be just skipping the underlying system ld script... – R.. GitHub STOP HELPING ICE May 18 '12 at 12:48
  • Now the problem became clearer for me. Could you please try another solution. Also I found a [link](http://stackoverflow.com/a/5030518/1101537) where it is said that -static and -pie options are incompatible. – alexander May 18 '12 at 14:47
  • Anything with `-T` is going to throw away the entire default linker script and thus generate broken (or at least highly suboptimal) binaries. And it's `INTERP` (as in `PT_INTERP`, the program header), not `interp` that you want to grep for to check the output. Sections in an executable file are purely annotation/debug information; they are not used whatsoever by the program loader. – R.. GitHub STOP HELPING ICE May 18 '12 at 14:57
  • Have you tried the second solution, or not tried because you do not like the first? It doesn't throw away the entire default linker script. Or if it does, could you, please, describe how? Naturally, you need to build with other options instead of the '-nostdlib.' Options in the example are chosen so that the file was being built and not run. And, how can it be that the sections are not used by program loader? I mean, the section contains code and data. Very strange... – alexander May 18 '12 at 16:55
  • The `-T` option is *documented* to cause ld to skip/replace the default linker scripts. – R.. GitHub STOP HELPING ICE May 18 '12 at 23:05
2

-Wl,--no-dynamic-linker worked for me.

Matthias
  • 3,833
  • 11
  • 36
  • 74
Elliot
  • 41
  • 3