最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

elf - Partial vs. full RELRO - Stack Overflow

programmeradmin1浏览0评论

I'm trying to understand the difference between partial and full RELRO when compiling ELF files. What I gather from various sources is that only under full RELRO is the entirety of the GOT protected.

Using gcc 13.3.0, I compiled a "Hello, world" program twice:

gcc -Wl,-z,relro,-z,now hello.c -o full
gcc -Wl,-z,relro,-z,lazy hello.c -o partial

checksec confirms that they have full and partial RELRO, respectively. When I run readelf -a on full, I see

...
  [21] .init_array       INIT_ARRAY      0000000000003db8 002db8 000008 08  WA  0   0  8
  [22] .fini_array       FINI_ARRAY      0000000000003dc0 002dc0 000008 08  WA  0   0  8
  [23] .dynamic          DYNAMIC         0000000000003dc8 002dc8 0001f0 10  WA  7   0  8
  [24] .got              PROGBITS        0000000000003fb8 002fb8 000048 08  WA  0   0  8
  [25] .data             PROGBITS        0000000000004000 003000 000010 00  WA  0   0  8
  [26] .bss              NOBITS          0000000000004010 003010 000008 00  WA  0   0  1
...
Program Headers:
...
  GNU_RELRO      0x002db8 0x0000000000003db8 0x0000000000003db8 0x000248 0x000248 R   0x1

This makes sense as 0x3db8 + 0x248 = 0x4000 which covers the GOT.

However, when I run readelf -a on partial, I see

...
  [21] .init_array       INIT_ARRAY      0000000000003dd0 002dd0 000008 08  WA  0   0  8
  [22] .fini_array       FINI_ARRAY      0000000000003dd8 002dd8 000008 08  WA  0   0  8
  [23] .dynamic          DYNAMIC         0000000000003de0 002de0 0001e0 10  WA  7   0  8
  [24] .got              PROGBITS        0000000000003fc0 002fc0 000028 08  WA  0   0  8
  [25] .got.plt          PROGBITS        0000000000003fe8 002fe8 000020 08  WA  0   0  8
  [26] .data             PROGBITS        0000000000004008 003008 000010 00  WA  0   0  8
  [27] .bss              NOBITS          0000000000004018 003018 000008 00  WA  0   0  1
...
Program Headers:
...
  GNU_RELRO      0x002dd0 0x0000000000003dd0 0x0000000000003dd0 0x000230 0x000230 R   0x1

While the GOT is split into two sections now, the GNU_RELRO segment still covers both of them.

I guess I'm misunderstanding how RELRO works. What is the difference between partial and full in terms of GOT protection and how can I detect by parsing the ELF?

For context, I have a library I’ve written which parses ELF files and finds GOT PLT entries. I’m trying to add a feature to detect when full RELRO is present.

I'm trying to understand the difference between partial and full RELRO when compiling ELF files. What I gather from various sources is that only under full RELRO is the entirety of the GOT protected.

Using gcc 13.3.0, I compiled a "Hello, world" program twice:

gcc -Wl,-z,relro,-z,now hello.c -o full
gcc -Wl,-z,relro,-z,lazy hello.c -o partial

checksec confirms that they have full and partial RELRO, respectively. When I run readelf -a on full, I see

...
  [21] .init_array       INIT_ARRAY      0000000000003db8 002db8 000008 08  WA  0   0  8
  [22] .fini_array       FINI_ARRAY      0000000000003dc0 002dc0 000008 08  WA  0   0  8
  [23] .dynamic          DYNAMIC         0000000000003dc8 002dc8 0001f0 10  WA  7   0  8
  [24] .got              PROGBITS        0000000000003fb8 002fb8 000048 08  WA  0   0  8
  [25] .data             PROGBITS        0000000000004000 003000 000010 00  WA  0   0  8
  [26] .bss              NOBITS          0000000000004010 003010 000008 00  WA  0   0  1
...
Program Headers:
...
  GNU_RELRO      0x002db8 0x0000000000003db8 0x0000000000003db8 0x000248 0x000248 R   0x1

This makes sense as 0x3db8 + 0x248 = 0x4000 which covers the GOT.

However, when I run readelf -a on partial, I see

...
  [21] .init_array       INIT_ARRAY      0000000000003dd0 002dd0 000008 08  WA  0   0  8
  [22] .fini_array       FINI_ARRAY      0000000000003dd8 002dd8 000008 08  WA  0   0  8
  [23] .dynamic          DYNAMIC         0000000000003de0 002de0 0001e0 10  WA  7   0  8
  [24] .got              PROGBITS        0000000000003fc0 002fc0 000028 08  WA  0   0  8
  [25] .got.plt          PROGBITS        0000000000003fe8 002fe8 000020 08  WA  0   0  8
  [26] .data             PROGBITS        0000000000004008 003008 000010 00  WA  0   0  8
  [27] .bss              NOBITS          0000000000004018 003018 000008 00  WA  0   0  1
...
Program Headers:
...
  GNU_RELRO      0x002dd0 0x0000000000003dd0 0x0000000000003dd0 0x000230 0x000230 R   0x1

While the GOT is split into two sections now, the GNU_RELRO segment still covers both of them.

I guess I'm misunderstanding how RELRO works. What is the difference between partial and full in terms of GOT protection and how can I detect by parsing the ELF?

For context, I have a library I’ve written which parses ELF files and finds GOT PLT entries. I’m trying to add a feature to detect when full RELRO is present.

Share Improve this question edited 2 days ago Daniel Walker asked Feb 8 at 5:14 Daniel WalkerDaniel Walker 6,7507 gold badges24 silver badges61 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 0

man ld says about the relro option:

Create an ELF "PT_GNU_RELRO" segment header in the object. This specifies a memory segment that should be made read-only after relocation, if supported.

The real difference between partial and full RELRO comes from the now option:

When generating an executable or shared library, mark it to tell the dynamic linker to resolve all symbols when the program is started, or when the shared library is loaded by dlopen, instead of deferring function call resolution to the point when the function is first called.

That is, full RELRO means that .got.plt entries will be resolved and then marked read-only before control is passed to main (or before dlopen returns).

You can detect this in the ELF file by looking at the entries in the .dynamic section. You'll either see an entry with the DT_BIND_NOW tag or the DT_FLAGS tag with the DF_BIND_NOW flag set in the entry's value.

You've stumbled on an obscure crevice in the GNU linker. You are almost but not exactly right in saying that even in partial relro the GNU_RELRO segment covers the got.plt section in your "Hello World" linkage. In fact it doesn't cover it, but it covers the first 24 bytes of it (the first 3 entries).

This outcome is expressly crafted into the default linker script for program linkage, with the intent of negotiating an inconvenient contraint on the demarcation of the relro segment. That constraint is the fact the relro segment must end on a page boundary, because memory access attributes have page grangularity. But page size is an arbitrary number with respect to section size. Hence, when the relro segment is prescribed to protect just the sections S1,...Sn, and Sn, and the final Sn extends some N < page-size bytes into a page, the linker has a choice of either:

  1. Not protecting those N bytes of Sn
  2. Protecting some initial bytes of whatever section is Sn + 1 - at most as many as will take the relro segment to the next page boundary
  3. Pushing all of Sn + 1 beyond the pale, even though it would be cost nothing to protect some of it.

Option #1 is of course unacceptable, but the linker (for x86_64 ELF targets at least), will take option #2 in some linkages and option #3 in others. Your linkage happens to be an option #2 type.

To play it out, I'll redo your "Hello World" linkages:

$ gcc --version | head -n1
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0

$ gcc -g -Wl,-z,relro,-z,now hello.c -o full
$ gcc -g -Wl,-z,relro,-z,lazy hello.c -o partial

Inspecting full

As a baseline, see the program headers of full:

$ readelf --program-headers --wide full

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R   0x8
  INTERP         0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x000628 0x000628 R   0x1000
  LOAD           0x001000 0x0000000000001000 0x0000000000001000 0x000175 0x000175 R E 0x1000
  LOAD           0x002000 0x0000000000002000 0x0000000000002000 0x0000f4 0x0000f4 R   0x1000
  LOAD           0x002db8 0x0000000000003db8 0x0000000000003db8 0x000258 0x000260 RW  0x1000
  DYNAMIC        0x002dc8 0x0000000000003dc8 0x0000000000003dc8 0x0001f0 0x0001f0 RW  0x8
  NOTE           0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R   0x8
  NOTE           0x000368 0x0000000000000368 0x0000000000000368 0x000044 0x000044 R   0x4
  GNU_PROPERTY   0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R   0x8
  GNU_EH_FRAME   0x002014 0x0000000000002014 0x0000000000002014 0x000034 0x000034 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x002db8 0x0000000000003db8 0x0000000000003db8 0x000248 0x000248 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .plt.got .plt.sec .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .data .bss 
   06     .dynamic 
   07     .note.gnu.property 
   08     .note.gnu.build-id .note.ABI-tag 
   09     .note.gnu.property 
   10     .eh_frame_hdr 
   11     
   12     .init_array .fini_array .dynamic .got
   

The GNU_RELRO #12 segment starts at the same address, 0x3db8 as the LOAD segment #5. It extends 0x248 memory bytes into load segment #5, which is 0x18 bytes less than the memory size, 0x260, of load segment #5. The relro segment isn't a load segment, or a distinct segment. It's an mprotect-ed initial sub-segment of a load segment.

Per the Section to Segment mappings, the relro segment covers the first 4 sections of load segment #5, ending with the .got. It leaves out .data and .bss, so the sum of their sizes by rights should be 0x18, and so it is:

$ readelf --sections --wide full | egrep \(data\|bss\)  
  [18] .rodata           PROGBITS        0000000000002000 002000 000011 00   A  0   0  4
  [25] .data             PROGBITS        0000000000004000 003000 000010 00  WA  0   0  8
  [26] .bss              NOBITS          0000000000004010 003010 000008 00  WA  0   0  1

And the relro segment ends on a page boundary: 0x3db8 + 0x248 = 0x4000

Inspecting partial

Next, compare the program headers of partial:

$ readelf --program-headers --wide partial

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R   0x8
  INTERP         0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x000628 0x000628 R   0x1000
  LOAD           0x001000 0x0000000000001000 0x0000000000001000 0x000175 0x000175 R E 0x1000
  LOAD           0x002000 0x0000000000002000 0x0000000000002000 0x0000f4 0x0000f4 R   0x1000
  LOAD           0x002dd0 0x0000000000003dd0 0x0000000000003dd0 0x000248 0x000250 RW  0x1000
  DYNAMIC        0x002de0 0x0000000000003de0 0x0000000000003de0 0x0001e0 0x0001e0 RW  0x8
  NOTE           0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R   0x8
  NOTE           0x000368 0x0000000000000368 0x0000000000000368 0x000044 0x000044 R   0x4
  GNU_PROPERTY   0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R   0x8
  GNU_EH_FRAME   0x002014 0x0000000000002014 0x0000000000002014 0x000034 0x000034 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x002dd0 0x0000000000003dd0 0x0000000000003dd0 0x000230 0x000230 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .plt.got .plt.sec .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss 
   06     .dynamic 
   07     .note.gnu.property 
   08     .note.gnu.build-id .note.ABI-tag 
   09     .note.gnu.property 
   10     .eh_frame_hdr 
   11     
   12     .init_array .fini_array .dynamic .got 

As you noted, we've now got an new .got.plt section in addition to .got. "Officially", the protected sections in load section #5:

   12     .init_array .fini_array .dynamic .got
   

are the same as before, and .got.plt is added to the unprotected sections:

   05     ..................................... .got.plt .data .bss
   

But the numbers don't quite agree. Now load segment #5 extends 0x250 memory bytes from address 0x3dd0 and the relro segment extends 0x230 bytes from the same base address. That makes the relro segment end on the very same page boundary as in full: 0x3dd0 + 0x230 = 0x4000. But per the section stats:

$ readelf --sections --wide partial | grep got.plt
  [25] .got.plt          PROGBITS        0000000000003fe8 002fe8 000020 08  WA  0   0  8
  

.got.plt starts at address 0x3fe8 and extends 0x20 memory bytes, making it end at 0x3fe8 + 0x20 = 0x4008. So in fact the first 0x18 bytes of .got.plt are within the relro segment and the last 0x8 bytes are not.

How so?

The source of this outcome is a fragment of the default linker script that drove the linkage. You can snip the script out from the Wl,--verbose output of the linkage. The fragment is:

 .got            : { *(.got) *(.igot) }
  . = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .);
  .got.plt        : { *(.got.plt) *(.igot.plt) }
  

This is where the end of the relro segment is assigned to the location counter (.) by the the builtin-function DATA_SEGMENT_RELRO_END(offset,expression). The documentation of this function

DATA_SEGMENT_RELRO_END(offset, exp)

This defines the end of the PT_GNU_RELRO segment when ‘-z relro’ option is used. When ‘-z relro’ option is not present, DATA_SEGMENT_RELRO_END does nothing, otherwise DATA_SEGMENT_ALIGN is padded so that exp + offset is aligned to the commonpagesize argument given to DATA_SEGMENT_ALIGN. If present in the linker script, it must be placed between DATA_SEGMENT_ALIGN and DATA_SEGMENT_END. Evaluates to the second argument plus any padding needed at the end of the PT_GNU_RELRO segment due to section alignment.

   . = DATA_SEGMENT_RELRO_END(24, .);

is as clear as mud. I reverse-engineered the meaning with experimental linker scripts and linkages. Here we need only consider cases of the form:

. = DATA_SEGMENT_RELRO_END(offset, .);

The function resets the current segment base address, pulling it up to make . + offset be the next page boundary after all the sections in the segment, ensuring that offset additional bytes following . can fit into the relro segment, and returns the fulfilling value of . So in case offset == 0, the fulfilling . is itself that next page boundary and the end of the relro segment.

In the variation that we see in the linker script:

  . = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .);

separating the mapping of .got from that of .got.plt, the effect is to shoe the first three .got.plt entries, if there are that many, into the relro segment after the officially protected whole sections as per the program headers, and if there < 3 then to push the whole section into the unprotected next segment.

In our "Hello World" linkage, the size of .got.plt is 0x20 = 32 bytes as we've already seen. So it qualifies for its first 24 bytes to be unofficially carved off into the relro segment and the residual 8 pushed out, as we've also seen. If we redo the partial linkage requesting the mapfile (e.g. -Wl,-Map=partial.map), the relevant snippet is:

.got            0x0000000000003fc0       0x28
 *(.got)
 .got           0x0000000000003fc0       0x28 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o
 *(.igot)
                0x0000000000003fe8                . = DATA_SEGMENT_RELRO_END (., (SIZEOF (.got.plt) >= 0x18)?0x18:0x0)

.got.plt        0x0000000000003fe8       0x20
 *(.got.plt)
 .got.plt       0x0000000000003fe8       0x20 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o
                0x0000000000003fe8                _GLOBAL_OFFSET_TABLE_
 *(.igot.plt)

.data           0x0000000000004008       0x10

Here we have to discount a bug in the mapfile generation which transposes the arguments of DATA_SEGMENT_RELRO_END. But we see the 0x20 bytes of .got.plt mapped starting at 0x3fe8 within the 0x3dd0-based relro segment, right after .got, and overshooting the 0x4000 end of the relro segment by 8 bytes, after which .data is mapped in.

Without changing hello_world.c we can simulate a .got.plt that's too small to get any of its entries into the relro segment by copying the default linker script and tweaking it to say:

. = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 48 ? 48 : 0, .);

A 48 byte threshold will exclude our 32 byte .got.plt. Then after:

$ gcc -g -Wl,-z,relro,-z,lazy hello.c -o partial-expt -Wl,-Texpt.ld,-Map=expt.map

the corresponding snippet of expt.map is:

.got            0x0000000000003fd8       0x28
 *(.got)
 .got           0x0000000000003fd8       0x28 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o
 *(.igot)
                0x0000000000004000                . = DATA_SEGMENT_RELRO_END (., (SIZEOF (.got.plt) >= 0x30)?0x30:0x0)

.got.plt        0x0000000000004000       0x20

with the whole .got.plt now unprotected at 0x4000. That's an option #3 linkage.

What is the point of this 24 byte threshold?

I don't know. I've found no explanation and none of my speculations stand scrutiny.

How can you distinguish full from partial relro by parsing an ELF?

The problem case is partial. If .got.plt < 24 bytes then it will start where the relro segment ends in partial relro. If it's >= 24 bytes its first 24 bytes will be at the end of the relro segment in partial relro and the residue at the start of the next segment.

Remember that this rule is good only for x86_64 targets; other ELF targets' default linker scripts will vary. And I wouldn't even bet on it being good for x86_64 targets permanently.

发布评论

评论列表(0)

  1. 暂无评论