In dynamically linked ELF files, functions used from shared memory need to be linked into the executable at runtime. This is because the executable does not contain its own copy of the function and hence does not know its address.

Linking functions is done through the use of the Procedure Linkage Table (PLT) as well as the Global Offset Table (GOT).

The general sequence of events is:

1) On the first call to a functions that needs to be dynamically loaded, you will call the PLT stub for that function.
2) The PLT stub will then take you to the GOT table entry for that function.
3) Since it is the first time you are calling this function the GOT table will actually return you back to the PLT stub for that specific function (where you just came from).
4) Where the GOT returned you to will be an instruction to jump to the dynamic linker which sits lower in memory within the PLT (near the beginning of where the PLT section got loaded into memory).
5) The dynamic linker will then find the actual function in shared memory and then update that functions entry in the GOT table to point to the actual executable code for the linked function instead of pointing back to the PLT.
6) Now all subsequent calls to that function will go to it’s entry in the PLT just like before which will take it to the GOT just like before, but now the GOT will point to actual address to jump to in memory to execute the function.

You can view the .plt and .plt.got section of the ELF file and observe the functions that will need to be linked.

objdump -d <binary_name> -mi386:intel


Output:
... Other Disassembled Sections ...

Disassembly of section .plt:

00001020 <__libc_start_main@plt-0x10>:
    1020:	ff b3 04 00 00 00    	push   DWORD PTR [ebx+0x4]
    1026:	ff a3 08 00 00 00    	jmp    DWORD PTR [ebx+0x8]
    102c:	00 00                	add    BYTE PTR [eax],al
	...

00001030 <__libc_start_main@plt>:
    1030:	ff a3 0c 00 00 00    	jmp    DWORD PTR [ebx+0xc]
    1036:	68 00 00 00 00       	push   0x0
    103b:	e9 e0 ff ff ff       	jmp    1020 <_init+0x20>

00001040 <printf@plt>:
    1040:	ff a3 10 00 00 00    	jmp    DWORD PTR [ebx+0x10]
    1046:	68 08 00 00 00       	push   0x8
    104b:	e9 d0 ff ff ff       	jmp    1020 <_init+0x20>

00001050 <getchar@plt>:
    1050:	ff a3 14 00 00 00    	jmp    DWORD PTR [ebx+0x14]
    1056:	68 10 00 00 00       	push   0x10
    105b:	e9 c0 ff ff ff       	jmp    1020 <_init+0x20>

Disassembly of section .plt.got:

00001060 <__cxa_finalize@plt>:
    1060:	ff a3 f0 ff ff ff    	jmp    DWORD PTR [ebx-0x10]
    1066:	66 90                	xchg   ax,ax

... Other Disassembled Sections ...



The EBX register in all of the output above will be pointing to the Global Offset Table. We will be looking at printf@plt as an example. First you should notice that the memory addresses are all quite small, and this is because these values are just offsets, Relative Virtual Addresses (RVAs), once this executable is actually loaded into memory, these RVAs will be applied to the base address of the executable in memory.

You can see the first instruction inside printf@plt is to jmp to the address held at ebp+0x10. As mentioned before EBP is holding the address of the GOT and so 0x10 is just an offset into that table - presumably the offset of the printf entry that table.

To actually be able to inspect these values we need to run the executable and inspect it with gdb. It’s important to note that if we want to watch the dynamic linking process occur we need to break before the first call to the function that we want to inspect, otherwise it will already have been linked. If you were to break before the first call to printf@plt then you will be able to inspect the PLT in gdb like so:

pwndbg> plt
    Section .plt 0x565fa020-0x565fa060:
    0x565fa030: __libc_start_main@plt
    0x565fa040: printf@plt
    0x565fa050: getchar@plt


From this we can see that the printf PLT entry is at 0x56610040. If we were to inspect this address and look at the instruction in the PLT entry we would see something like:

pwndbg> x/3i 0x56610040
    0x565fa040 <printf@plt>:	jmp    DWORD PTR [ebx+0x10]
    0x565fa046 <printf@plt+6>:	push   0x8
    0x565fa04b <printf@plt+11>:	jmp    0x565fa020


This looks pretty identical to the structure we saw in the PLT displayed by objdump, however now we have proper addresses. So if someone were to call printf@plt for the first time, the first instruction after the call they would execute is:

0x565fa040 <printf@plt>:	jmp    DWORD PTR [ebx+0x10]


This is indexing into the GOT by 0x10, dereferencing that address and jumping to it. Lets break that down a little, first lets have a look at where the GOT is in memory and what it contains there. First i mentioned that EBX holds the value of the GOT, I got this information from gdb i.e.:

EBX  0x565fcff4 (_GLOBAL_OFFSET_TABLE_) ◂— 0x3ef0


If we use vmmap to look at the memory layout of our process we will see that the GOT address (EBP+0x10 = 0x565fd004) resides in the DATA segment (It isn’t color coded here so you will have to take my word for it):

0x565f9000 0x565fa000 r--p     1000      0 /root/example
0x565fa000 0x565fb000 r-xp     1000   1000 /root/example
0x565fb000 0x565fc000 r--p     1000   2000 /root/example
0x565fc000 0x565fd000 r--p     1000   2000 /root/example
**0x565fd000 0x565fe000** rw-p     1000   3000 /root/example
0x56ce6000 0x56d08000 rw-p    22000      0 [heap]
0xf7cfb000 0xf7d1d000 r--p    22000      0 /usr/lib/i386-linux-gnu/libc.so.6
0xf7d1d000 0xf7e96000 r-xp   179000  22000 /usr/lib/i386-linux-gnu/libc.so.6
0xf7e96000 0xf7f16000 r--p    80000 19b000 /usr/lib/i386-linux-gnu/libc.so.6
0xf7f16000 0xf7f18000 r--p     2000 21b000 /usr/lib/i386-linux-gnu/libc.so.6
0xf7f18000 0xf7f19000 rw-p     1000 21d000 /usr/lib/i386-linux-gnu/libc.so.6
0xf7f19000 0xf7f23000 rw-p     a000      0 [anon_f7f19]
0xf7f2f000 0xf7f31000 rw-p     2000      0 [anon_f7f2f]
0xf7f31000 0xf7f35000 r--p     4000      0 [vvar]
0xf7f35000 0xf7f37000 r-xp     2000      0 [vdso]
0xf7f37000 0xf7f38000 r--p     1000      0 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7f38000 0xf7f5b000 r-xp    23000   1000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7f5b000 0xf7f69000 r--p     e000  24000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7f69000 0xf7f6b000 r--p     2000  31000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xf7f6b000 0xf7f6c000 rw-p     1000  33000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xff984000 0xff9a5000 rw-p    21000      0 [stack]


There isn’t instruction in the DATA section, so instead of using x/i to view the instructions like we did the in the PLT we will just use x/a to view the addresses stored in the GOT:

pwndbg> x/a $ebx+0x10
    0x565fd004 <printf@got.plt>:	0x565fa046


Notice how GDB has labeled that the address we are inspecting is the address of printf@got.plt i.e. printf’s entry in the GOT table. This address hold a another address ‘0x565fa046’. If we look back earlier in the post, the instruction in the printf@plt stub was to jump to whatever value as stored in memory at $EBP+0x10, so it wants us to jump to 0x565fa046 but what is there?

pwndbg> x/3i 0x565fa046
    0x565fa046 <printf@plt+6>:	push   0x8
    0x565fa04b <printf@plt+11>:	jmp    0x565fa020
    0x565fa050 <getchar@plt>:	jmp    DWORD PTR [ebx+0x14]


Its actually just jumping right back to where we came from, this is because this is the first call to printf and it has not been linked yet. It will push 0x8 onto the stack and then it will jump to 0x565fa020 which just happens to be:

pwndbg> x/10i 0x565fa020
    0x565fa020:	push   DWORD PTR [ebx+0x4]  
    0x565fa026:	jmp    DWORD PTR [ebx+0x8]
    0x565fa02c:	add    BYTE PTR [eax],al
    0x565fa02e:	add    BYTE PTR [eax],al
    0x565fa030 <__libc_start_main@plt>:	jmp    DWORD PTR [ebx+0xc]
    0x565fa036 <__libc_start_main@plt+6>:	push   0x0
    0x565fa03b <__libc_start_main@plt+11>:	jmp    0x565fa020
    0x565fa040 <printf@plt>:	jmp    DWORD PTR [ebx+0x10]
    0x565fa046 <printf@plt+6>:	push   0x8
    0x565fa04b <printf@plt+11>:	jmp    0x565fa020


just slightly further up in the PLT. Near the beginning of the PLT you will have your dynamic linker which all PLT stubs will have to go through to link their respective functions.

0x565fa020:	push   DWORD PTR [ebx+0x4]  : Pushes the ELF magic bytes onto the 
                                        ; stack presumably as a argument for the
                                        ; linker to use.


Now we are jumping to ebx+0x8, we know ebx is the GOT, we know that ebx+0x10 is the offset into the GOT for the printf entry, what about an offset of 0x8?

pwndbg> x/a $ebx+0x8
    0x565fcffc:	0xf7f498c0
pwndbg> x/i 0xf7f498c0
    0xf7f498c0 <_dl_runtime_resolve>:	push   eax


At $ebx+0x8 it stores the address 0xf7f498c0, which is jumped to, and we can see from next line, that 0xf7f498c0 is the address of the dynamic runtime linker runtime resolver. This function is what will actually go and find the address of printf and then update the value of the GOT with the address of the printf function for subsequent calls. If we were to break at the second use of printf and inspect where its jumping to we will see a similar PLT but a different GOT for example, this is right before out second call to printf:

Section .plt 0x565fa020-0x565fa060:
    0x565fa030: __libc_start_main@plt
    0x565fa040: printf@plt
    0x565fa050: getchar@plt
pwndbg> x/3i 0x565fa040
    0x565fa040 <printf@plt>:	jmp    DWORD PTR [ebx+0x10]
    0x565fa046 <printf@plt+6>:	push   0x8
    0x565fa04b <printf@plt+11>:	jmp    0x565fa020


We can see that the PLT looks the same, what about when we inspect what value is stored at ebx+0x10?

pwndbg> x/a $ebx+0x10
    0x565fd004 <printf@got.plt>:	0xf7d4ee40
pwndbg> x/3i 0xf7d4ee40
    0xf7d4ee40 <__printf>:	call   0xf7e69d4d <__x86.get_pc_thunk.ax>
    0xf7d4ee45 <__printf+5>:	add    eax,0x1c91af
    0xf7d4ee4a <__printf+10>:	sub    esp,0xc


We can see that ebx+0x10 still points to the printf entry in the GOT, but now it is storing a different value, instead of jumping back to the PLT like it did the first time, it now points to 0xf7d4ee40 which is the first instruction in printf.