Invocando funciones externas al binario
En linux, los ejecutables invocan funciones externas mediante dos mecanismos:
- directamente al kernel de linux través de un system call: por ejemplo, invocando write
- a traves de una librería como libc: por ejemplo, puts, write, scanf
¿Cuáles son algunas de las funciones que provee el linux kernel (los llamados system calls?) ?
-
write: escribir a algun file descriptor (recuerda que en linux tanto los archivos como los I/O devices se consideran files). Un ejemplo desensamblado de un binario:
4000b0: b8 01 00 00 00 mov $0x1,%eax 4000b5: bf 01 00 00 00 mov $0x1,%edi 4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi 4000c1: 00 00 00 4000c4: ba 0c 00 00 00 mov $0xc,%edx 4000c9: 0f 05 syscall
Desde assembly x64, se realizan los system calls usando la instrucción
syscall
. Los valores de los registros establecen:- eax: ¿cuál system call? por ejemplo cuando eax = 1 queremos invocar a write
- rdi: primer parámetro del system call. En el caso de write: el file descriptor
- rsi: segundo parámetro al system call. En el caso de write: la dirección del string
- rdx: tercer parámetro al system call. En el caso de write: el largo del string.
-
read: leer de algun file descriptor. A continuación un ejemplo de una llamada a read que espefica leer del stdin.
4000b0: b8 00 00 00 00 mov $0x0,%eax 4000b5: bf 00 00 00 00 mov $0x0,%edi 4000ba: 48 be 02 01 60 00 00 movabs $0x600102,%rsi 4000c1: 00 00 00 4000c4: ba 04 00 00 00 mov $0x4,%edx 4000c9: 0f 05 syscall
-
adivina cuál es esta llamada (hint busca en https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ )
4000e6: b8 3c 00 00 00 mov $0x3c,%eax 4000eb: bf 00 00 00 00 mov $0x0,%edi 4000f0: 0f 05 syscall
-
y esta?
4000cb: b8 23 00 00 00 mov eax,0x23 4000d0: bf 05 00 00 00 mov edi,0x5 4000d5: 48 89 7c 24 f8 mov QWORD PTR [rsp-0x8],rdi 4000da: 48 8d 7c 24 f8 lea rdi,[rsp-0x8] 4000df: be 00 00 00 00 mov esi,0x0 4000e4: 0f 05 syscall
Invocaciones a funciones de librerías compartidas
Un programa que realizará invocaciones a funciones de una librería compartida muestra esas funciones en su sección de símbolos. Esto es parte de lo obtenido con readelf -s hw
, donde hw es un programa que invoca a printf
. Nota que el símbolo printf@@GLIBC_2.2.5
dice UND
(undefined). Esto quiere decir que la función printf
no está implementada en nuestro programa, i.e. es una función externa a nuestro programa y su implementación será leida de la librería compartida libc
durante la ejecución.
50: 0000000000601038 0 NOTYPE GLOBAL DEFAULT 25 _edata
51: 00000000004005b4 0 FUNC GLOBAL DEFAULT 15 _fini
52: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
53: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
Otra forma de desplegar los símbolos de un programa es usando el comando nm
:
$nm ./hw
0000000000601038 B __bss_start
. . . .
U __libc_start_main@@GLIBC_2.2.5
0000000000400526 T main
U printf@@GLIBC_2.2.5
Nuevamente vemos a printf
. La letra U
indica que no está definida (implementada) en nuestro programa.
El disassembly del programa muestra que en la función main
se está invocando a una tal función printf@plt
.
0000000000400526 <main>:
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: bf c4 05 40 00 mov $0x4005c4,%edi
40052f: b8 00 00 00 00 mov $0x0,%eax
400534: e8 c7 fe ff ff callq 400400 <printf@plt>
400539: b8 00 00 00 00 mov $0x0,%eax
40053e: 5d pop %rbp
40053f: c3 retq
El @plt
son las siglas de procedure linkage table y es un mecanismo que facilita el uso de funciones de librerías compartidas. Si buscas el disasembly de printf@plt
, encuentras lo siguiente.
0000000000400400 <printf@plt>:
400400: ff 25 12 0c 20 00 jmpq *0x200c12(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
400406: 68 00 00 00 00 pushq $0x0
40040b: e9 e0 ff ff ff jmpq 4003f0 <_init+0x28>
Obviamente esta no es la implementación de printf
. Es meramente un trampolin que ayuda a que el flujo del programa se redirija a la dirección en memoria donde realmente está la implementación de printf
. El brinco que dará nuestro programa para llegar a printf está determinado por una información que vive en el GOT (global offset table) de nuestro programa. La primera vez que printf
es invocado por nuestro programa, una función del sistema operativo (dl_runtime_resolve
) averigua dónde vive la implementación de printf en memoria y ajusta el contenido del GOT para printf.
Cada una de las funciones externas tiene su stub PLT y una entrada en el GOT, aun las funciones externas que son llamadas por las funciones que inserta el linker en nuestro ejecutable, por ejemplo libc_start_main
:
0000000000400410 <__libc_start_main@plt>:
400410: ff 25 0a 0c 20 00 jmp QWORD PTR [rip+0x200c0a] # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
400416: 68 01 00 00 00 push 0x1
40041b: e9 d0 ff ff ff jmp 4003f0 <_init+0x28>
Al ejecutar ldd ./hw
podemos ver las librerías compartidas de las que hace uso. Alli vemos listada a libc.so.6
.
ldd ./hw
linux-vdso.so.1 => (0x00007ffd89bb8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6945477000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6945841000)
Conocimiento util?
Si corres nm
sobre una librería compartida, puedes averiguar los nombres de todas las funciones que comparte:
$ nm -D /lib/x86_64-linux-gnu/libc.so.6
...
0000000000055800 T printf
00000000001168e0 T __printf_chk
0000000000052bc0 T __printf_fp
...
strace and ltrace
ltrace
- A library call tracer- strace - trace system calls and signals
- A signal is an asynchronous notification sent to a process or to a specific thread within the same process in order to notify it of an event that occurred [Wikipedia]
- Por ejemplo, cuando haces
kill
a un proceso en linux, le estás enviando una señalSIGTERM
.