Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PLT accesses and symbol imports in ELF binaries #247

Closed
m4b opened this issue Feb 13, 2017 · 9 comments
Closed

PLT accesses and symbol imports in ELF binaries #247

m4b opened this issue Feb 13, 2017 · 9 comments
Assignees

Comments

@m4b
Copy link
Collaborator

m4b commented Feb 13, 2017

I'm wondering what we should do about PLT accesses (e.g., calling an imported symbol from another library), in particular, how to:

  1. represent them
  2. detect them

An example will be clearer, consider the following c file compiled with gcc -shared foo.c -o libfoo.so:

#include<stdio.h>
#include<math.h>

extern int foo() {
  return 0xdeadbeef;
}

static int internal1(int i) {
  return i - 0xbeef;
}

extern int bar(int i) {
  return internal1(i);
}

extern int foobar() {
  return bar(foo());
}

extern void ifoo() {
  printf("hello, world\n");
}

extern void ifoobar() {
  int result = foobar();
  printf("ifoobar: %d\n", result);
}

Currently, panopticon correctly discovers every PLT jump stub (more on that in a bit), and the local unexported (e.g., internal1 above, and func_7bb below) function.

screenshot from 2017-02-12 18-41-20

The PLT jump stubs are func_650 to func_690, and all have the same form (but reference different PLT entries):

screenshot from 2017-02-12 18-44-24

If we examine the function ifoobar in panopticon we see the following:

screenshot from 2017-02-12 18-47-06

Which are two PLT accesses to func_670 and func_680, (the PLT jump stubs for foobar and printf respectively).

For the record, the PLT jump stubs are little entries created by the linker for imported symbols, which effectively implement lazy import of symbols as follows:

  1. when a function is called, the PLT jump stub is actually called
  2. this stub simply jumps directly to whatever value is in something called the PLT GOT (the procedure linkage global offset table), which is stored in the READ+WRITE ".data" loadable area of the binary; on the programs first run, it is just the resolver function for the dynamic linker, which after resolving the function's address (hopefully), places this value in the GOT, so the next call jumps directly to the address of the imported function, instead of the resolver (thus implementing lazy loading of imported symbols)

So, the first question is: what do we want to show here? I am for maximal explicitness, especially for beginners learning to look at disassembled code, but it would also be nice to:

  1. recognize PLT jump slots,
  2. resolve them to their symbolic name
  3. display this symbolic name alongside the plt jump slot call, e.g. (printf@plt, or something similar)

gdb performs something similar when we disassemble:

Dump of assembler code for function ifoobar:
   0x000000000000080d <+0>:	push   %rbp
   0x000000000000080e <+1>:	mov    %rsp,%rbp
   0x0000000000000811 <+4>:	sub    $0x10,%rsp
   0x0000000000000815 <+8>:	mov    $0x0,%eax
   0x000000000000081a <+13>:	callq  0x670 <foobar@plt>
   0x000000000000081f <+18>:	mov    %eax,-0x4(%rbp)
   0x0000000000000822 <+21>:	mov    -0x4(%rbp),%eax
   0x0000000000000825 <+24>:	mov    %eax,%esi
   0x0000000000000827 <+26>:	lea    0x24(%rip),%rdi        # 0x852
   0x000000000000082e <+33>:	mov    $0x0,%eax
   0x0000000000000833 <+38>:	callq  0x680 <printf@plt>
   0x0000000000000838 <+43>:	nop
   0x0000000000000839 <+44>:	leaveq 
   0x000000000000083a <+45>:	retq

(to be fair, gdb is using the section headers to locate the PLT and symbols and likely the symbol table for foobar, all of which is strippable)

The second question is, how should we do it? Is this the responsibility of goblin? Iirc, the actual PLT jump stubs aren't required to be located anywhere in the binary information (since the dynamic linker doesn't even need to know about the jump slots, only the PLT location of where to place the resolved addresses), so it might be better/more sensible to:

  1. add some kind of pattern recognition w.r.t. jmp slots function stubs
  2. grab which PLT GOT reference occurs (simply RIP + value = instruction addr + value = GOT value)
  3. Since the PLT GOT location and size is known, use the value in 2, and then combine the GOT address with the dynamic relocation information to recover the import name (this information is in the dynamic array which is required if any imports occur, so even section stripped binaries are tractable to this analysis iiuc)
@flanfly
Copy link
Member

flanfly commented Feb 14, 2017

I started to work on this in order to support dynamically liked ELF files. My idea is to add a "symbol table" to the disassembler that tells the static analysis where (symbolic) addresses to certain functions are in memory (e.g. PLT). Then, we can recover that using an Abstract Interpretation like Bounded Address Tracking. As this is a interprocedural analysis (across functions) I have to implement a lot of infrastructure. Sadly I got sidetracked by fixing the GUI issues. You can take a look at the code here: https://github.com/flanfly/panopticon/tree/bounded-addr-tracking

@m4b
Copy link
Collaborator Author

m4b commented Feb 15, 2017

  1. That branch is super exciting! I hope it gets merged soon, but all things in due time
  2. I didn't get a chance for in-depth review, but why such heavy-hitting approach to solve above?

Iiuc, I'm also not sure how exactly bounded address tracking will help panopticon know a jmp slot is actually a jump slot to an import.

Also every jmp slot actually has the value immediately computable, e.g., it is simply the next instructions address + the offset = GOT entry = inside the dynamic array, which means we know its name (now loading that symbols code up and matching that with the symbolic import name might require the above)

So I was really only talking about identifying imported functions (and not having their actual contents resolved). This will also be useful to be for a couple of other unrelated reasons, in particular for a future tool which would need this information.

Anyway, does that make any sense? Perhaps I'm missing something, or perhaps you're referring to both getting the imported functions (their name), and then also resolving their code bodies?

@flanfly
Copy link
Member

flanfly commented Feb 16, 2017

Bounded address tracking simply replaces every value in the assembly code listing with a symbolic base plus offset. For example EAX + 10 or ESP - 10. There is also a global region that is equal to 0. So the absolute value 0xdeadbeef would be global + 0xdeadbeef. I use this b/c ELF files that are dynamically linked to glibc use a scheme where they push the address of main onto the stack and call a PLT stub that pops the value off the stack and calls it [1].

I thought it would be cool to resolve this using static analysis. My idea is that the ELF loader tells Panopticon where the GOT is located and what symbols it points to. The static analysis will recognize entry n as __libc_start_main and know to look at ESP - 8 for the address of main.

Thankfully, PLT resolution is easier. This should definitely be added. I'll try to get a simple version of bounded address tracking merged the next week. Then we only need a simple pass that renames all functions that jump to a symbolic address XXX to XXX@plt.

1: https://github.com/lattera/glibc/blob/master/sysdeps/i386/start.S#L60

@m4b
Copy link
Collaborator Author

m4b commented Feb 17, 2017

Hm, its still not precisely clear to me how my example above gets resolved in the manner I'm thinking. (getting _start analyzed properly will as you say, probably require bigger firepower, as it does lots of crazy stuff, like calling dynamic linker init functions, etc.)

But to illustrate, my use case is as follows:

  1. Suppose for some function f, I want to find:
    a. every "imported" function that f calls
    b. every non-imported function that f calls
  2. Panopticon currently finds every function that f calls, but, makes no distinction between imported and non-imported.

What I mean by 2 is that it returns a list of every call; but some of those calls are "plt jump stubs" (but they're valid function calls nonetheless).

It looks something like:

f -> jump_stub -> lazy init machinery -> <import in other address space>

Consequently, even if bounded address tracking were to collapse the plt jump stub to:

f -> jump_stub + lazy_init machinery -> <import>

I still think f would show the call to the jump stub + lazy_init machinery, which (and this is my point), will I believe require some kind of pattern recognition to notice that "oh hey, this style of call is actually an import call, so we need to look up its name in the GOT now and display that to the user"

Unless you're saying that bounded address tracking when run on function targets would:

  1. know about PLT GOT members, which the loader provides (and hence symbolic names)
  2. whenever a PLT GOT member gets resolved as as a target (in the kset or whatever), then that whole function is marked as potentially an import

?

So ideally, I'm thinking for my simple use case, calling panopticon on f can in principle return to me:

  1. list of called functions, split by "is_import"/"is_locally_defined"

(also the initial call site in the function for the import will also be important, e.g. at what address it occurs in the function, but I suspect that will be possible once its implemented)

@flanfly
Copy link
Member

flanfly commented Feb 17, 2017

I want to use the dynamic relocation information to skip the lazy initialization completly and pretend it already has been run. So in in libfoo.so we have a setup like this:

0000000000000690 <bar@plt>:
jmp    QWORD PTR [rip+0x20098a] # 201020 <_GLOBAL_OFFSET_TABLE_+0x20>
push   0x1
jmp    670 <_init+0x20>

and dynamic relocation info:

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE 
...
0000000000201020 R_X86_64_JUMP_SLOT  bar
...

The ELF loader will give us a Region and a map HashMap<Range<u64>,String> that contains the dynamic relocations. For bar this would be (201020..201028) -> "bar". The disassembler sees jmp [rip+0x20098a] (i.e. jmp [0x201020]) and first checks the map. It realizes that the dynamic loader writes (after lazy init) the address of bar at address 0x201020. When bar is defined in the same ELF file we can simple look up where it's located and jump there. If not, we can insert a call edge to the symbol bar. A second pass will simply rename all functions that consist of a single jump to a unresolved symbol.

@m4b
Copy link
Collaborator Author

m4b commented Feb 17, 2017

Ah 👌, I see now. Yea this is a good idea, and it was the second pass I was worried about.

It should be fairly simple to add a pltgot + got hashmap directly in the Elf struct. Iirc this should also be directly constructable from the information already present in the struct. I can work on updating it with this information, so you'll have the available map when needed.

Incidentally, once this gets implemented might be nice for the functions "called functions" method to return list of local and imported functions, or have the function Enum contain this distinction.

@m4b
Copy link
Collaborator Author

m4b commented Aug 12, 2017

I'd like to nominate this as a crucial next step, and make this a priority.

I keep bumping up against this issue all over the place, and I think it needs to get solved asap.

Basically, besides from correctly determining which functions are called, almost all shared libraries will route local function calls through the PLT; glibc avoids this by some hackery I don't understand, but it involves messing with linker flags, and function visibility annotations, which most libraries and gcc wont' do by default.

For example, from libsnappy.so:

   0x0000000000003cce <+1502>:	callq  0x24e0 <_ZN6snappy18SnappyDecompressor9RefillTagEv@plt>

That function _ZN6snappy18SnappyDecompressor9RefillTagEv is a global, exported function; but it's called by _ZN6snappy20RawUncompressToIOVecEPNS_6SourceEPKNS_5iovecEm, through the plt table:

m4b@efrit ::  [ ~/projects/bingrep ] bingrep /usr/lib/libsnappy.so.1.3.1  | grep _ZN6snappy18SnappyDecompressor9RefillTagEv
            2d70 GLOBAL   FUNC      _ZN6snappy18SnappyDecompressor9RefillTagEv st_size: 0x1d3 st_other: 0x0 st_shndx: 0xc
          207090 X86_64_JUMP_SLOT _ZN6snappy18SnappyDecompressor9RefillTagEv

Consequently, panopticon only sees the callq to the plt stub.

We need to fix this and teach panopticon to:

  1. Resolve and collapse plt stubs to their corresponding entries
  2. Determine if the function called is actually an import, or local (this is easy once 1 is done)

@flanfly How do you think we should do this? I remember you were working on something maybe, but not sure?

I'd like to see this implemented sooner rather than later, as its a blocker on another project which is trying to use panopticon for analysis

EDIT

A blocker to this, as I would have implemented it already since the import symbol infrastructure is essentially in place is that I cannot get the value from the plt stub, because its RIP relative and I don't know how to access the value:

   0x0000000000002580 <+0>:	jmpq   *0x204b5a(%rip)        # 0x2070e0

You'll note in the screenshot that the value after jmp is all pale yellow, which means the entire yellow portion is considered by panopticon as a variable.

screenshot from 2017-08-12 14-01-39

That is however just the mnemonics; I think I need to access and interpret the statement vector for the plt stub, but I'd need some pointers how to do this

@m4b
Copy link
Collaborator Author

m4b commented Aug 12, 2017

Minimal example demonstrating the problem

#[no_mangle]
pub fn _print_rust() {
    println!("deadbeef: {:#x}", 0xdeadbeefu64);
}

rustc --crate-type=staticlib -C lto foo.rs

extern void _print_rust(void);

void foo () {
  _print_rust();
}

gcc -shared -L. foo.c libfoo.a -o libfoo.so

As far as I know, cannot even do symbol vis and alias hackery to fix this, as this requires the alias to be in the same compilation unit, which it is not here.

@m4b m4b self-assigned this Aug 14, 2017
m4b added a commit that referenced this issue Aug 14, 2017
1. Adds function::Kind, now a field of functions
2. Adds update_plt to program, called in analysis, to rewrite candidate function stubs as their plt name, name@plt with kind updated
3. fixes bug in elf loader when pltrela symbol type was NO_TYPE (but is actually a function, because it's in the plt and its called like a function, see `pthread_mutexattr_destroy`)
@m4b
Copy link
Collaborator Author

m4b commented Aug 21, 2017

Oh whoops this got implemented 😎

Also it works for mach too :)

@m4b m4b closed this as completed Aug 21, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants