1 of 14

Psi Beta Rho

Spring 2024 - Week 5

2 of 14

πŸ“£ Announcements

  • ⏰ Cyber Academy
    • ???
    • This Monday 6-8pm @ Bolter 4760 (hopefully)
  • 🐌 Cyber Lab: Cowrie!
    • Presented by Dylan
    • This Wednesday 6-8pm @ Boelter 4760
  • 🚩 CTF IRL
    • Friday, May 10th @ 6pm on IM Field
    • RSVP Required: https://l.acmcyber.com/ctfirl
  • πŸ’» CS Town Hall
    • Monday Week 7, 5/13
    • Mong Auditorium 6-7:30
    • https://l.acmcyber.com/s24-townhall

3 of 14

glibc linker

Enzo

4 of 14

  • responsibilities
    • load shared object dependencies into memory
    • relocate local pointers dependent on these load addresses

  • loading dependencies must be performed recursively
    • relocations processed in reverse order

  • two relocation categories
    • relative
      • offset from load address of object itself
    • symbol
      • specifies a symbol string, linker looks through symbol tables of object dependencies to resolve address from a separate object

dynamic linker

5 of 14

  • PT_INTERP specifies path to linker

  • PT_DYNAMIC points to .dynamic section
    • holds list of Elf64_Dyn tag entries
    • tells the linker where sections are located and what to do

      • common section descriptors
        • .dynsym DT_SYMTAB
        • .dynstr DT_STRTAB
        • .rela.dyn DT_RELA

      • DT_NEEDED tag for each shared object dependency

elf anatomy

typedef struct {

Elf64_Xword d_tag;

union {

Elf64_Xword d_val;

Elf64_Addr d_ptr;

} d_un;

} Elf64_Dyn;

6 of 14

  • symbol resolution done at runtime, so how do external function calls or variables work?

  • GOT (global offset table) is special region in main binary’s memory populated with symbol addresses by linker

  • function calls dependent on GOT wrapped in PLT (procedure linkage table) stubs

  • two layers of PLT stubs
    • symboled stubs in .plt.sec, simply jump to corresponding entry in GOT

    • unsymboled resolver stubs in .plt for lazy resolution

plt and got

7 of 14

  • DT_FLAGS tag can specify BIND_NOW, default with modern gcc
    • implies full RELRO (relocation read-only), all symbols resolved at startup, GOT made read-only for security

  • alternative is partial RELRO, function symbols resolved at runtime only after the first time they’re called
    • GOT initially populated with local .plt resolver stubs that push a relocation index and jump to the linker’s runtime resolver function, will overwrite the GOT with correct symbol address

relro

linker only populates runtime resolver

rest of GOT points to local functions that call the resolver with appropriate relocation index

resolver writes actual address so subsequent plt calls go to the right function

8 of 14

  • full RELRO limits linker attack surface since most of its job is done during startup

  • with partial RELRO, we can corrupt linker state to hijack what symbols resolve to from runtime resolver
    • also allows overwriting GOT for easy PC control, but this doesn’t really require interaction with the linker itself

  • universal leakless linker technique: fini hijacking
    • on exit libc calls designated functions, _dl_fini will be one of them
    • _dl_fini calls functions designated in binary by DT_FINI and DT_FINI_ARRAY
      • DT_FINI for single function, array for list, both are called
      • can be exploited leaklessly with blind write into mmap relative memory using partial overwrites

attack surface

9 of 14

  • fini tags in the ELF themselves aren’t writable, but linker uses writable link_map structure to store the tag info, allowing us to corrupt it

  • looking at case of the single DT_FINI, we notice this calculation:

    • recall from tag structure, d_un.d_ptr simply grabs tag value, adds it to map->l_addr, which holds exe base
    • initially seems unexploitable leaklessly, since we would need either l_addr or DT_FINI to be a libc address

leakless fini

typedef struct {

Elf64_Xword d_tag;

union {

Elf64_Xword d_val;

Elf64_Addr d_ptr;

} d_un;

} Elf64_Dyn;

10 of 14

  • trick is that further down in exe there are GOT addresses pointing to libc, so we can partial overwrite l_info[DT_FINI] to be on top of libc address, and write l_addr with the constant offset to the libc function we want to call

  • distance between GOT and fini would require 4 bit bf from 2 byte partial overwrite, can refine to no bf if DT_DEBUG is present which will hold libc relative address

  • luckily rdi will point to linker writable memory when our hijacked fini function is called, can write to that to control argument for system and spawn shell

  • credit to pepsipu for this technique!

leakless fini

11 of 14

  • malloc(0x3000) returns mmap relative chunk so index gives us blind write into linker and libc memory

  • no file streams like stdout being used that would allow us to perform FSOP for leak

  • program will return once we supply an index of zero, which will go to libc at_exit and call _dl_fini

example chall

#include <unistd.h>

#include <stdlib.h>

long readint() {

char buf[0x11] = {};

read(0, buf, 0x10);

return atol(buf);

}

int main() {

char *p = malloc(0x3000);

for(;;) {

write(1, "idx: ", 5);

long idx = readint();

if(idx == 0)

break;

write(1, "data: ", 6);

read(0, p+idx, 8);

}

}

12 of 14

  • interesting leakless heap+linker exploit, requires partial RELRO and a call to runtime resolver after vuln

  • large heap chunks get mmap’d, due to mmap relativity can be positioned right above libc base which contains ELF symbol info and is read-only

  • corrupt size of chunk there, free will then munmap the top of libc, allocate another large chunk to reclaim that memory as writeable

  • can then forge symbol information in the ELF, doesn’t require leaks since all information is relative, will trick the runtime resolver into resolving any libc address you want

house of muney

13 of 14

Questions?

14 of 14

PBR Rahhh!