Seguir el acceso a registros consecutivos y comprobar cambios sobre una copia del binario.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 7 --example student-arrayEstoy siguiendo «Arrays de estructuras y parches» del curso de Lobera. Seguir el acceso a registros consecutivos y comprobar cambios sobre una copia del binario. Dame primero una pista. Después contrasta la explicación con las instrucciones y guarda las evidencias que hayamos comprobado.
El agente y la consola comparten el estado de radare2. Preparación y funcionamiento.
Hoy vamos a trabajar con un ejemplo más completo relacionado con structs. El objetivo es entender correctamente cómo funciona el programa internamente y explorar algunas características adicionales de r2.
#include <stdio.h>
const int MAX = 20;
"td struct stud { char fletter; int age; float mark;};"
struct stud
{
char fletter;
int age;
float mark;
};
main(){
func1();
getchar();
}
void showstudents(struct stud students[], int n){
printf("Showing students: \n");
for(int i = 0; i < n; i++){
printf("Student num %d: ", i);
printf("First letter = %c ", students[i].fletter);
printf("Age = %d ", students[i].age);
printf("Mark = %f \n", students[i].mark);
}
}
int addstudents(struct stud students[], int i){
printf("Round: %d \n", i);
if(i > MAX){
printf("Students list is FULL\n");
return -1;
}
else{
printf("First letter?\n");
scanf(" %c",&students[i].fletter);
printf("Age?\n");
scanf(" %i",&students[i].age);
printf("Mark?\n");
scanf(" %f",&students[i].mark);
return i+1;
}
}
func1(){
struct stud students[MAX];
int i = 0;
char q =' ';
while(q != 'q' && i!=-1){
printf("Action? (q=Quit, a=Add, s=ShowAll)\n");
scanf(" %c", &q);
if(q == 'a'){
i = addstudents(students,i);
}
}
}
Localizar las funciones
Veamos qué funciones tenemos:
[0x000009c0]> afl
0x000006a0 1 42 entry0
0x000006d0 4 50 -> 40 sym.deregister_tm_clones
0x00000710 4 66 -> 57 sym.register_tm_clones
0x00000760 5 58 -> 51 sym.__do_global_dtors_aux
0x000007a0 1 10 entry.init0
0x00000b90 1 2 sym.__libc_csu_fini
0x000009c0 8 352 sym.func1
0x00000b94 1 9 sym._fini
0x000007c4 4 240 sym.showstudents
0x00000b20 4 101 sym.__libc_csu_init
0x000008b4 4 268 sym.addstudents
0x000007aa 1 26 main
0x00000670 1 6 sym.imp.getchar
0x00000618 3 23 sym._init
0x00000640 1 6 sym.imp.puts
0x00000650 1 6 sym.imp.__stack_chk_fail
0x00000660 1 6 sym.imp.printf
0x00000000 5 97 -> 123 loc.imp._ITM_deregisterTMCloneTable
0x00000680 1 6 sym.imp.__isoc99_scanf
Las funciones más relevantes son showstudents, addstudents y func1; el resto corresponde a la biblioteca stdio y al punto de entrada del programa. Como ya sabemos, el programa «comienza» en sym.func1, así que veamos:
[0x000006a0]> s sym.func1
[0x000009c0]> pdf
; CALL XREF from main @ 0x7b3
┌ 352: sym.func1 ();
│ ; var int64_t var_2dh @ rbp-0x2d
│ ; var uint32_t var_2ch @ rbp-0x2c
│ ; var int64_t var_28h @ rbp-0x28
│ ; var int64_t var_20h @ rbp-0x20
│ ; var int64_t canary @ rbp-0x18
│ ; var int64_t var_10h @ rbp-0x10
│ 0x000009c0 55 push rbp
│ 0x000009c1 4889e5 mov rbp, rsp
│ 0x000009c4 4154 push r12
│ 0x000009c6 53 push rbx
│ 0x000009c7 4883ec20 sub rsp, 0x20
│ 0x000009cb 64488b3c2528. mov rdi, qword fs:[0x28]
│ 0x000009d4 48897de8 mov qword [canary], rdi
│ 0x000009d8 31ff xor edi, edi
│ 0x000009da 4889e6 mov rsi, rsp
│ 0x000009dd 4989f4 mov r12, rsi
│ 0x000009e0 be14000000 mov esi, 0x14
│ 0x000009e5 4863f6 movsxd rsi, esi
│ 0x000009e8 4883ee01 sub rsi, 1
│ 0x000009ec 488975d8 mov qword [var_28h], rsi
│ 0x000009f0 be14000000 mov esi, 0x14
│ 0x000009f5 4863f6 movsxd rsi, esi
│ 0x000009f8 4889f0 mov rax, rsi
│ 0x000009fb ba00000000 mov edx, 0
│ 0x00000a00 486bfa60 imul rdi, rdx, 0x60
│ 0x00000a04 486bf000 imul rsi, rax, 0
│ 0x00000a08 4801fe add rsi, rdi
│ 0x00000a0b bf60000000 mov edi, 0x60 ; '`'
│ 0x00000a10 48f7e7 mul rdi
│ 0x00000a13 4801d6 add rsi, rdx
│ 0x00000a16 4889f2 mov rdx, rsi
│ 0x00000a19 b814000000 mov eax, 0x14
│ 0x00000a1e 4863d0 movsxd rdx, eax
│ 0x00000a21 4889d0 mov rax, rdx
│ 0x00000a24 4801c0 add rax, rax
│ 0x00000a27 4801d0 add rax, rdx
│ 0x00000a2a 48c1e002 shl rax, 2
│ 0x00000a2e b814000000 mov eax, 0x14
│ 0x00000a33 4898 cdqe
│ 0x00000a35 4889c1 mov rcx, rax
│ 0x00000a38 bb00000000 mov ebx, 0
│ 0x00000a3d 486bd360 imul rdx, rbx, 0x60
│ 0x00000a41 486bc100 imul rax, rcx, 0
│ 0x00000a45 488d3402 lea rsi, [rdx + rax]
│ 0x00000a49 b860000000 mov eax, 0x60 ; '`'
│ 0x00000a4e 48f7e1 mul rcx
│ 0x00000a51 488d0c16 lea rcx, [rsi + rdx]
│ 0x00000a55 4889ca mov rdx, rcx
│ 0x00000a58 b814000000 mov eax, 0x14
│ 0x00000a5d 4863d0 movsxd rdx, eax
│ 0x00000a60 4889d0 mov rax, rdx
│ 0x00000a63 4801c0 add rax, rax
│ 0x00000a66 4801d0 add rax, rdx
│ 0x00000a69 48c1e002 shl rax, 2
│ 0x00000a6d 488d5003 lea rdx, [rax + 3]
│ 0x00000a71 b810000000 mov eax, 0x10
│ 0x00000a76 4883e801 sub rax, 1
│ 0x00000a7a 4801d0 add rax, rdx
│ 0x00000a7d bb10000000 mov ebx, 0x10
│ 0x00000a82 ba00000000 mov edx, 0
│ 0x00000a87 48f7f3 div rbx
│ 0x00000a8a 486bc010 imul rax, rax, 0x10
│ 0x00000a8e 4829c4 sub rsp, rax
│ 0x00000a91 4889e0 mov rax, rsp
│ 0x00000a94 4883c003 add rax, 3
│ 0x00000a98 48c1e802 shr rax, 2
│ 0x00000a9c 48c1e002 shl rax, 2
│ 0x00000aa0 488945e0 mov qword [var_20h], rax
│ 0x00000aa4 c745d4000000. mov dword [var_2ch], 0
│ 0x00000aab c645d320 mov byte [var_2dh], 0x20 ; "@"
│ ┌─< 0x00000aaf eb40 jmp 0xaf1
│ │ ; CODE XREF from sym.func1 @ 0xafd
│ ┌──> 0x00000ab1 488d3d880100. lea rdi, str.Action___q_Quit__a_Add__s_ShowAll ; 0xc40 ; "Action? (q=Quit, a=Add, s=ShowAll)" ; const char *s
│ ╎│ 0x00000ab8 e883fbffff call sym.imp.puts ; int puts(const char *s)
│ ╎│ 0x00000abd 488d45d3 lea rax, [var_2dh]
│ ╎│ 0x00000ac1 4889c6 mov rsi, rax
│ ╎│ 0x00000ac4 488d3d5e0100. lea rdi, [0x00000c29] ; " %c" ; const char *format
│ ╎│ 0x00000acb b800000000 mov eax, 0
│ ╎│ 0x00000ad0 e8abfbffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ ╎│ 0x00000ad5 0fb645d3 movzx eax, byte [var_2dh]
│ ╎│ 0x00000ad9 3c61 cmp al, 0x61
│ ┌───< 0x00000adb 7514 jne 0xaf1
│ │╎│ 0x00000add 488b45e0 mov rax, qword [var_20h]
│ │╎│ 0x00000ae1 8b55d4 mov edx, dword [var_2ch]
│ │╎│ 0x00000ae4 89d6 mov esi, edx ; signed int64_t arg2
│ │╎│ 0x00000ae6 4889c7 mov rdi, rax ; int64_t arg1
│ │╎│ 0x00000ae9 e8c6fdffff call sym.addstudents
│ │╎│ 0x00000aee 8945d4 mov dword [var_2ch], eax
│ │╎│ ; CODE XREFS from sym.func1 @ 0xaaf, 0xadb
│ └─└─> 0x00000af1 0fb645d3 movzx eax, byte [var_2dh]
│ ╎ 0x00000af5 3c71 cmp al, 0x71
│ ╎┌─< 0x00000af7 7406 je 0xaff
│ ╎│ 0x00000af9 837dd4ff cmp dword [var_2ch], 0xffffffff
│ └──< 0x00000afd 75b2 jne 0xab1
│ │ ; CODE XREF from sym.func1 @ 0xaf7
│ └─> 0x00000aff 4c89e4 mov rsp, r12
│ 0x00000b02 90 nop
│ 0x00000b03 488b5de8 mov rbx, qword [canary]
│ 0x00000b07 6448331c2528. xor rbx, qword fs:[0x28]
│ ┌─< 0x00000b10 7405 je 0xb17
│ │ 0x00000b12 e839fbffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from sym.func1 @ 0xb10
│ └─> 0x00000b17 488d65f0 lea rsp, [var_10h]
│ 0x00000b1b 5b pop rbx
│ 0x00000b1c 415c pop r12
│ 0x00000b1e 5d pop rbp
└ 0x00000b1f c3 ret
Esto empieza a tener cierta complejidad; el bloque es grande. En situaciones así puede interesarte examinar el programa con un estilo más cercano a C para obtener una visión general. Los decompiladores ayudan con eso. Radare2 incluye un decompilador llamado r2dec, que se puede instalar mediante el gestor de paquetes de radare2, r2pm.
Podemos instalar un paquete —r2dec en este caso— con lo siguiente:
r2pm init
r2pm install r2dec
Y después, con pdd, ejecutamos el decompilador.
0x000009c0]> pdd
/* r2dec pseudo code output */
/* struct1 @ 0x9c0 */
#include <stdint.h>
int64_t func1 (void) {
int64_t var_2dh;
uint32_t var_2ch;
int64_t var_28h;
int64_t var_20h;
int64_t canary;
int64_t var_10h;
rdi = *(fs:0x28);
canary = *(fs:0x28);
edi = 0;
rsi = rsp;
r12 = rsp;
esi = 0x14;
rsi = (int64_t) esi;
rsi--;
var_28h = rsi;
esi = 0x14;
rsi = (int64_t) esi;
rax = rsi;
edx = 0;
rdi = rdx * 0x60;
rsi = rax * 0;
rsi += rdi;
edi = 0x60;
rdx:rax = rax * rdi;
rsi += rdx;
rdx = rsi;
eax = 0x14;
rdx = (int64_t) eax;
rax = rdx;
rax += rax;
rax += rdx;
rax <<= 2;
eax = 0x14;
rax = (int64_t) eax;
rcx = rax;
ebx = 0;
rdx = rbx * 0x60;
rax = rcx * 0;
rsi = rdx + rax;
eax = 0x60;
rdx:rax = rax * rcx;
rcx = rsi + rdx;
rdx = rcx;
eax = 0x14;
rdx = (int64_t) eax;
rax = rdx;
rax += rax;
rax += rdx;
rax <<= 2;
rdx = rax + 3;
eax = 0x10;
rax--;
rax += rdx;
ebx = 0x10;
edx = 0;
rax = rdx:rax / rbx;
rdx = rdx:rax % rbx;
rax *= 0x10;
rax = rsp;
rax += 3;
rax >>= 2;
rax <<= 2;
var_20h = rax;
var_2ch = 0;
var_2dh = 0x20;
while (var_2ch != 0xffffffff) {
puts ("Action? (q=Quit, a=Add, s=ShowAll)");
rax = &var_2dh;
rsi = rax;
rdi = 0x00000c29;
eax = 0;
isoc99_scanf ();
eax = (int32_t) var_2dh;
if (al == 0x61) {
rax = var_20h;
edx = var_2ch;
esi = var_2ch;
rdi = rax;
showstudents ();
var_2ch++;
}
eax = (int32_t) var_2dh;
if (al == 0x71) {
goto label_0;
}
}
label_0:
rbx = canary;
rbx ^= *(fs:0x28);
if (var_2ch != 0xffffffff) {
stack_chk_fail ();
}
rsp = &var_10h;
return rax;
}
En este ejemplo nos encontramos con el primer proyecto en el que omitiremos algunas partes; es decir, obtendremos la visión general y luego nos centraremos solo en las áreas o aspectos estratégicos del programa. A partir de lo que vimos al ejecutar el decompilador podemos ver que la función establece el stack canary, ejecuta un bloque grande de cálculos, almacena el resultado en var_20h e inicializa un par de variables: var_2ch a cero y var_2dh con 0x20 (espacio ASCII ' '). Como ya tenemos el código fuente original, podemos deducir rápidamente que corresponden a la inicialización de las variables de la función. Lo almacenado en var_20h debería ser la dirección base del array de structs; sabiendo eso, podemos entender mejor todo el bloque de operaciones que aparece justo antes. Si lo examinamos con más atención, podemos detectar fácilmente que:
│ 0x000009e0 be14000000 mov esi, 0x14
│ 0x000009e5 4863f6 movsxd rsi, esi
│ 0x000009e8 4883ee01 sub rsi, 1
│ 0x000009ec 488975d8 mov qword [var_28h], rsi
│ 0x000009f0 be14000000 mov esi, 0x14
│ 0x000009f5 4863f6 movsxd rsi, esi
│ 0x000009f8 4889f0 mov rax, rsi
│ 0x000009fb ba00000000 mov edx, 0
│ 0x00000a00 486bfa60 imul rdi, rdx, 0x60
│ 0x00000a04 486bf000 imul rsi, rax, 0
│ 0x00000a08 4801fe add rsi, rdi
│ 0x00000a0b bf60000000 mov edi, 0x60 ; '`'
│ 0x00000a10 48f7e7 mul rdi
│ 0x00000a13 4801d6 add rsi, rdx
│ 0x00000a16 4889f2 mov rdx, rsi
│ 0x00000a19 b814000000 mov eax, 0x14
│ 0x00000a1e 4863d0 movsxd rdx, eax
│ 0x00000a21 4889d0 mov rax, rdx
│ 0x00000a24 4801c0 add rax, rax
│ 0x00000a27 4801d0 add rax, rdx
│ 0x00000a2a 48c1e002 shl rax, 2
│ 0x00000a2e b814000000 mov eax, 0x14
Reservar y recorrer el array
0x14 (decimal 20) se repite varias veces, por lo que el programa básicamente «reserva» espacio suficiente para almacenar el array completo en memoria; dicho de otro modo, calcula una dirección de inicio para la estructura, ya que no puede referenciar simplemente una variable con ebp-20 o similar, porque nuestra estructura contiene varios valores de distintos tamaños. Otro detalle a tener en cuenta: como se puede ver, no se llama a ninguna función en ese bloque de código, lo que indica que no ocurre nada especialmente relevante para la lógica del algoritmo. A veces, cuando ves bloques así, suelen corresponder a cálculos de direcciones de memoria u operaciones matemáticas sobre variables. Presta siempre atención a valores estáticos como esos 0x14 (o el espacio ASCII, etc.); pueden indicar el tamaño de estructuras de datos, índices, inicializaciones de variables, valores de comparación (límites), etc. En la mayoría de los casos no es necesario entender el 100 % del código instrucción por instrucción; con experiencia y una visión general puedes centrarte rápidamente en las partes estratégicas.
Ahora sabemos que la función comienza con un cálculo de direcciones de memoria y también inicializa algunas variables; después parece saltar a un bucle while:
│ 0x00000aab c645d320 mov byte [var_2dh], 0x20 ; "@"
│ ┌─< 0x00000aaf eb40 jmp 0xaf1
│ │ ; CODE XREF from sym.func1 @ 0xafd
│ ┌──> 0x00000ab1 488d3d880100. lea rdi, str.Action___q_Quit__a_Add__s_ShowAll ; 0xc40 ; "Action? (q=Quit, a=Add, s=ShowAll)" ; const char *s
│ ╎│ 0x00000ab8 e883fbffff call sym.imp.puts ; int puts(const char *s)
│ ╎│ 0x00000abd 488d45d3 lea rax, [var_2dh]
│ ╎│ 0x00000ac1 4889c6 mov rsi, rax
│ ╎│ 0x00000ac4 488d3d5e0100. lea rdi, [0x00000c29] ; " %c" ; const char *format
│ ╎│ 0x00000acb b800000000 mov eax, 0
│ ╎│ 0x00000ad0 e8abfbffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ ╎│ 0x00000ad5 0fb645d3 movzx eax, byte [var_2dh]
│ ╎│ 0x00000ad9 3c61 cmp al, 0x61
│ ┌───< 0x00000adb 7514 jne 0xaf1
│ │╎│ 0x00000add 488b45e0 mov rax, qword [var_20h]
│ │╎│ 0x00000ae1 8b55d4 mov edx, dword [var_2ch]
│ │╎│ 0x00000ae4 89d6 mov esi, edx ; signed int64_t arg2
│ │╎│ 0x00000ae6 4889c7 mov rdi, rax ; int64_t arg1
│ │╎│ 0x00000ae9 e8c6fdffff call sym.addstudents
│ │╎│ 0x00000aee 8945d4 mov dword [var_2ch], eax
│ │╎│ ; CODE XREFS from sym.func1 @ 0xaaf, 0xadb
│ └─└─> 0x00000af1 0fb645d3 movzx eax, byte [var_2dh]
│ ╎ 0x00000af5 3c71 cmp al, 0x71
│ ╎┌─< 0x00000af7 7406 je 0xaff
│ ╎│ 0x00000af9 837dd4ff cmp dword [var_2ch], 0xffffffff
│ └──< 0x00000afd 75b2 jne 0xab1
│ │ ; CODE XREF from sym.func1 @ 0xaf7
│ └─> 0x00000aff 4c89e4 mov rsp, r12
A continuación, lo que sigue ya debería resultarnos familiar.
│ ┌──> 0x00000ab1 488d3d880100. lea rdi, str.Action___q_Quit__a_Add__s_ShowAll ; 0xc40 ; "Action? (q=Quit, a=Add, s=ShowAll)" ; const char *s
│ ╎│ 0x00000ab8 e883fbffff call sym.imp.puts ; int puts(const char *s)
│ ╎│ 0x00000abd 488d45d3 lea rax, [var_2dh]
│ ╎│ 0x00000ac1 4889c6 mov rsi, rax
│ ╎│ 0x00000ac4 488d3d5e0100. lea rdi, [0x00000c29] ; " %c" ; const char *format
│ ╎│ 0x00000acb b800000000 mov eax, 0
│ ╎│ 0x00000ad0 e8abfbffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ ╎│ 0x00000ad5 0fb645d3 movzx eax, byte [var_2dh]
│ ╎│ 0x00000ad9 3c61 cmp al, 0x61
│ ┌───< 0x00000adb 7514 jne 0xaf1
│ │╎│ 0x00000add 488b45e0 mov rax, qword [var_20h]
La cadena que se imprime ya nos indica qué ocurre en ese bloque de código: lee la entrada del usuario (opción: quit, add, show) y, como compara la entrada con 0x61 (ASCII 'a'), comprueba la entrada y ejecutará una acción según ella.
Veamos qué ocurre cuando el usuario introduce 'a' (add):
│ │╎│ 0x00000add 488b45e0 mov rax, qword [var_20h]
│ │╎│ 0x00000ae1 8b55d4 mov edx, dword [var_2ch]
│ │╎│ 0x00000ae4 89d6 mov esi, edx ; signed int64_t arg2
│ │╎│ 0x00000ae6 4889c7 mov rdi, rax ; int64_t arg1
│ │╎│ 0x00000ae9 e8c6fdffff call sym.addstudents
│ │╎│ 0x00000aee 8945d4 mov dword [var_2ch], eax
El contenido de var_20h y var_2ch se pasan como parámetros. Como ya sabemos, var_20h representa (o eso creemos) el puntero al array de structs; var_2ch, como vimos, se inicializa a cero y se actualiza con el valor de eax, por lo que podemos asumir que es el contador. Se pasa a la función addstudents para indicar la posición actual en la que se está trabajando.
Después de eso, el bucle continúa y el programa realiza un par de cmps:
│ ╎ 0x00000af5 3c71 cmp al, 0x71
│ ╎┌─< 0x00000af7 7406 je 0xaff
│ ╎│ 0x00000af9 837dd4ff cmp dword [var_2ch], 0xffffffff
│ └──< 0x00000afd 75b2 jne 0xab1
Que representan la condición q != 'q' && i!=-1. Al ser un &&, la condición será falsa si q=='q' y no necesitará comprobar i!=-1, de ahí que tengamos esos dos cmp encadenados de esa forma.
Reconstruir los campos de Student
Dicho esto, podemos saltar a la función addstudents para inspeccionar cómo funciona.
[0x000008b4]> pdf
; CALL XREF from sym.func1 @ 0xae9
┌ 268: sym.addstudents (int64_t arg1, signed int64_t arg2);
│ ; var signed int64_t var_ch @ rbp-0xc
│ ; var int64_t var_8h @ rbp-0x8
│ ; arg int64_t arg1 @ rdi
│ ; arg signed int64_t arg2 @ rsi
│ 0x000008b4 55 push rbp
│ 0x000008b5 4889e5 mov rbp, rsp
│ 0x000008b8 4883ec10 sub rsp, 0x10
│ 0x000008bc 48897df8 mov qword [var_8h], rdi ; arg1
│ 0x000008c0 8975f4 mov dword [var_ch], esi ; arg2
│ 0x000008c3 8b45f4 mov eax, dword [var_ch]
│ 0x000008c6 89c6 mov esi, eax
│ 0x000008c8 488d3d2a0300. lea rdi, str.Round:__d ; 0xbf9 ; "Round: %d \n" ; const char *format
│ 0x000008cf b800000000 mov eax, 0
│ 0x000008d4 e887fdffff call sym.imp.printf ; int printf(const char *format)
│ 0x000008d9 b814000000 mov eax, 0x14
│ 0x000008de 3945f4 cmp dword [var_ch], eax
│ ┌─< 0x000008e1 7e16 jle 0x8f9
│ │ 0x000008e3 488d3d1b0300. lea rdi, str.Students_list_is_FULL ; 0xc05 ; "Students list is FULL" ; const char *s
│ │ 0x000008ea e851fdffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x000008ef b8ffffffff mov eax, 0xffffffff ; -1
│ ┌──< 0x000008f4 e9c5000000 jmp 0x9be
│ ││ ; CODE XREF from sym.addstudents @ 0x8e1
│ │└─> 0x000008f9 488d3d1b0300. lea rdi, str.First_letter ; 0xc1b ; "First letter?" ; const char *s
│ │ 0x00000900 e83bfdffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x00000905 8b45f4 mov eax, dword [var_ch]
│ │ 0x00000908 4863d0 movsxd rdx, eax
│ │ 0x0000090b 4889d0 mov rax, rdx
│ │ 0x0000090e 4801c0 add rax, rax
│ │ 0x00000911 4801d0 add rax, rdx
│ │ 0x00000914 48c1e002 shl rax, 2
│ │ 0x00000918 4889c2 mov rdx, rax
│ │ 0x0000091b 488b45f8 mov rax, qword [var_8h]
│ │ 0x0000091f 4801d0 add rax, rdx
│ │ 0x00000922 4889c6 mov rsi, rax
│ │ 0x00000925 488d3dfd0200. lea rdi, [0x00000c29] ; " %c" ; const char *format
│ │ 0x0000092c b800000000 mov eax, 0
│ │ 0x00000931 e84afdffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ │ 0x00000936 488d3df00200. lea rdi, str.Age ; 0xc2d ; "Age?" ; const char *s
│ │ 0x0000093d e8fefcffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x00000942 8b45f4 mov eax, dword [var_ch]
│ │ 0x00000945 4863d0 movsxd rdx, eax
│ │ 0x00000948 4889d0 mov rax, rdx
│ │ 0x0000094b 4801c0 add rax, rax
│ │ 0x0000094e 4801d0 add rax, rdx
│ │ 0x00000951 48c1e002 shl rax, 2
│ │ 0x00000955 4889c2 mov rdx, rax
│ │ 0x00000958 488b45f8 mov rax, qword [var_8h]
│ │ 0x0000095c 4801d0 add rax, rdx
│ │ 0x0000095f 4883c004 add rax, 4
│ │ 0x00000963 4889c6 mov rsi, rax
│ │ 0x00000966 488d3dc50200. lea rdi, [0x00000c32] ; " %i" ; const char *format
│ │ 0x0000096d b800000000 mov eax, 0
│ │ 0x00000972 e809fdffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ │ 0x00000977 488d3db80200. lea rdi, str.Mark ; 0xc36 ; "Mark?" ; const char *s
│ │ 0x0000097e e8bdfcffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x00000983 8b45f4 mov eax, dword [var_ch]
│ │ 0x00000986 4863d0 movsxd rdx, eax
│ │ 0x00000989 4889d0 mov rax, rdx
│ │ 0x0000098c 4801c0 add rax, rax
│ │ 0x0000098f 4801d0 add rax, rdx
│ │ 0x00000992 48c1e002 shl rax, 2
│ │ 0x00000996 4889c2 mov rdx, rax
│ │ 0x00000999 488b45f8 mov rax, qword [var_8h]
│ │ 0x0000099d 4801d0 add rax, rdx
│ │ 0x000009a0 4883c008 add rax, 8
│ │ 0x000009a4 4889c6 mov rsi, rax
│ │ 0x000009a7 488d3d8e0200. lea rdi, [0x00000c3c] ; " %f" ; const char *format
│ │ 0x000009ae b800000000 mov eax, 0
│ │ 0x000009b3 e8c8fcffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ │ 0x000009b8 8b45f4 mov eax, dword [var_ch]
│ │ 0x000009bb 83c001 add eax, 1
│ │ ; CODE XREF from sym.addstudents @ 0x8f4
│ └──> 0x000009be c9 leave
└ 0x000009bf c3 ret
[0x000008b4]>
Esa función parece algo más extensa que las de los tutoriales anteriores, pero se puede ver fácilmente que sigue un patrón:
│ │└─> 0x000008f9 488d3d1b0300. lea rdi, str.First_letter ; 0xc1b ; "First letter?" ; const char *s
│ │ 0x00000900 e83bfdffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x00000905 8b45f4 mov eax, dword [var_ch]
│ │ 0x00000908 4863d0 movsxd rdx, eax
│ │ 0x0000090b 4889d0 mov rax, rdx
│ │ 0x0000090e 4801c0 add rax, rax
│ │ 0x00000911 4801d0 add rax, rdx
│ │ 0x00000914 48c1e002 shl rax, 2
│ │ 0x00000918 4889c2 mov rdx, rax
│ │ 0x0000091b 488b45f8 mov rax, qword [var_8h]
│ │ 0x0000091f 4801d0 add rax, rdx
│ │ 0x00000922 4889c6 mov rsi, rax
│ │ 0x00000925 488d3dfd0200. lea rdi, [0x00000c29] ; " %c" ; const char *format
│ │ 0x0000092c b800000000 mov eax, 0
│ │ 0x00000931 e84afdffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ │ 0x00000936 488d3df00200. lea rdi, str.Age ; 0xc2d ; "Age?" ; const char *s
El patrón se repite tres veces para los tres valores que la función solicita: letter, age y mark, con una única diferencia. En el bloque de age vemos un add 0x4 adicional, y 0x8 en el bloque del float. Podemos relacionar fácilmente esos valores con el tamaño de int y float en C.
Inspeccionemos primero el bloque relacionado con la lectura del char. Carga el valor de local_ch, ejecuta algunas operaciones sobre él y luego lo suma al valor de local_8h. Como podemos ver examinando el inicio de esta función y el orden de los parámetros que se le pasan en sym.func1, local_ch corresponde al int relacionado con la posición en el array y local_8h a la dirección base del array. ¿Qué hacen esas operaciones? En este caso son relevantes porque nos ayudan a entender mejor cómo están estructurados nuestros datos.
Esas operaciones se traducen en esto:
val = local_ch
val = val + val
val = val + local_ch
val = val + 2^2 (the shl 2 translates to that)
addr_char_towrite = local_8h + val
Como podemos calcular, para un índice de 0x0 el resultado será 0xC (decimal 12). En el siguiente bloque se repite la misma operación y se añade un 0x4 adicional; en el bloque final, un 0x8. Hay por tanto una distancia de 0xC (decimal 12) entre el primero y el último, lo que tiene todo el sentido. Con eso puedes deducir la estructura completa. Intenta depurar el programa, añadir algunos valores y volcar el struct.
Una vez que hemos «deducido» la estructura de los datos, podemos incluso ejecutar r2 en modo depuración y volcar el contenido así:
[0x5645465e8aee]> pf 5c...df @ 0x7ffe3530d310
0x7ffe3530d310 [0] {
0x7ffe3530d310 = 'A'
0x7ffe3530d314 = 23
0x7ffe3530d318 = 3.29999995
}
0x7ffe3530d31c [1] {
0x7ffe3530d31c = 'L'
0x7ffe3530d320 = 2
0x7ffe3530d324 = 3.4000001
}
0x7ffe3530d328 [2] {
0x7ffe3530d328 = 'B'
0x7ffe3530d32c = 19
0x7ffe3530d330 = 6.4000001
}
0x7ffe3530d334 [3] {
0x7ffe3530d334 = 'C'
0x7ffe3530d338 = 32
0x7ffe3530d33c = 7.69999981
}
Relacionar lectura y escritura
Si recuerdas, había otra función interesante en el programa: sym.showstudents, pero no la hemos visto en ningún punto del código. De momento podemos buscarla con s e inspeccionarla.
[0x000007c4]> pdf
┌ 240: sym.showstudents (int64_t arg1, signed int64_t arg2);
│ ; var signed int64_t var_1ch @ rbp-0x1c
│ ; var int64_t var_18h @ rbp-0x18
│ ; var int64_t var_4h @ rbp-0x4
│ ; arg int64_t arg1 @ rdi
│ ; arg signed int64_t arg2 @ rsi
│ 0x000007c4 55 push rbp
│ 0x000007c5 4889e5 mov rbp, rsp
│ 0x000007c8 4883ec20 sub rsp, 0x20
│ 0x000007cc 48897de8 mov qword [var_18h], rdi ; arg1
│ 0x000007d0 8975e4 mov dword [var_1ch], esi ; arg2
│ 0x000007d3 488d3dd20300. lea rdi, str.Showing_students: ; 0xbac ; "Showing students: " ; const char *s
│ 0x000007da e861feffff call sym.imp.puts ; int puts(const char *s)
│ 0x000007df c745fc000000. mov dword [var_4h], 0
│ ┌─< 0x000007e6 e9ba000000 jmp 0x8a5
│ │ ; CODE XREF from sym.showstudents @ 0x8ab
│ ┌──> 0x000007eb 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x000007ee 89c6 mov esi, eax
│ ╎│ 0x000007f0 488d3dc80300. lea rdi, str.Student_num__d: ; 0xbbf ; "Student num %d: " ; const char *format
│ ╎│ 0x000007f7 b800000000 mov eax, 0
│ ╎│ 0x000007fc e85ffeffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x00000801 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x00000804 4863d0 movsxd rdx, eax
│ ╎│ 0x00000807 4889d0 mov rax, rdx
│ ╎│ 0x0000080a 4801c0 add rax, rax
│ ╎│ 0x0000080d 4801d0 add rax, rdx
│ ╎│ 0x00000810 48c1e002 shl rax, 2
│ ╎│ 0x00000814 4889c2 mov rdx, rax
│ ╎│ 0x00000817 488b45e8 mov rax, qword [var_18h]
│ ╎│ 0x0000081b 4801d0 add rax, rdx
│ ╎│ 0x0000081e 0fb600 movzx eax, byte [rax]
│ ╎│ 0x00000821 0fbec0 movsx eax, al
│ ╎│ 0x00000824 89c6 mov esi, eax
│ ╎│ 0x00000826 488d3da30300. lea rdi, str.First_letter____c ; 0xbd0 ; "First letter = %c " ; const char *format
│ ╎│ 0x0000082d b800000000 mov eax, 0
│ ╎│ 0x00000832 e829feffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x00000837 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x0000083a 4863d0 movsxd rdx, eax
│ ╎│ 0x0000083d 4889d0 mov rax, rdx
│ ╎│ 0x00000840 4801c0 add rax, rax
│ ╎│ 0x00000843 4801d0 add rax, rdx
│ ╎│ 0x00000846 48c1e002 shl rax, 2
│ ╎│ 0x0000084a 4889c2 mov rdx, rax
│ ╎│ 0x0000084d 488b45e8 mov rax, qword [var_18h]
│ ╎│ 0x00000851 4801d0 add rax, rdx
│ ╎│ 0x00000854 8b4004 mov eax, dword [rax + 4]
│ ╎│ 0x00000857 89c6 mov esi, eax
│ ╎│ 0x00000859 488d3d830300. lea rdi, str.Age____d ; 0xbe3 ; "Age = %d " ; const char *format
│ ╎│ 0x00000860 b800000000 mov eax, 0
│ ╎│ 0x00000865 e8f6fdffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x0000086a 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x0000086d 4863d0 movsxd rdx, eax
│ ╎│ 0x00000870 4889d0 mov rax, rdx
│ ╎│ 0x00000873 4801c0 add rax, rax
│ ╎│ 0x00000876 4801d0 add rax, rdx
│ ╎│ 0x00000879 48c1e002 shl rax, 2
│ ╎│ 0x0000087d 4889c2 mov rdx, rax
│ ╎│ 0x00000880 488b45e8 mov rax, qword [var_18h]
│ ╎│ 0x00000884 4801d0 add rax, rdx
│ ╎│ 0x00000887 f30f104008 movss xmm0, dword [rax + 8]
│ ╎│ 0x0000088c f30f5ac0 cvtss2sd xmm0, xmm0
│ ╎│ 0x00000890 488d3d560300. lea rdi, str.Mark____f ; 0xbed ; "Mark = %f \n" ; const char *format
│ ╎│ 0x00000897 b801000000 mov eax, 1
│ ╎│ 0x0000089c e8bffdffff call sym.imp.printf ; int printf(const char *format)
│ ╎│ 0x000008a1 8345fc01 add dword [var_4h], 1
│ ╎│ ; CODE XREF from sym.showstudents @ 0x7e6
│ ╎└─> 0x000008a5 8b45fc mov eax, dword [var_4h]
│ ╎ 0x000008a8 3b45e4 cmp eax, dword [var_1ch]
│ └──< 0x000008ab 0f8c3affffff jl 0x7eb
│ 0x000008b1 90 nop
│ 0x000008b2 c9 leave
└ 0x000008b3 c3 ret
[0x000007c4]>
Como vemos, aparece exactamente el mismo patrón.
│ ╎│ 0x00000801 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x00000804 4863d0 movsxd rdx, eax
│ ╎│ 0x00000807 4889d0 mov rax, rdx
│ ╎│ 0x0000080a 4801c0 add rax, rax
│ ╎│ 0x0000080d 4801d0 add rax, rdx
│ ╎│ 0x00000810 48c1e002 shl rax, 2
│ ╎│ 0x00000814 4889c2 mov rdx, rax
│ ╎│ 0x00000817 488b45e8 mov rax, qword [var_18h]
│ ╎│ 0x0000081b 4801d0 add rax, rdx
│ ╎│ 0x0000081e 0fb600 movzx eax, byte [rax]
│ ╎│ 0x00000821 0fbec0 movsx eax, al
│ ╎│ 0x00000824 89c6 mov esi, eax
│ ╎│ 0x00000826 488d3da30300. lea rdi, str.First_letter____c ; 0xbd0 ; "First letter = %c " ; const char *format
│ ╎│ 0x0000082d b800000000 mov eax, 0
│ ╎│ 0x00000832 e829feffff call sym.imp.printf ; int printf(const char *format)
Tiene mucho sentido, ya que la función es casi idéntica a addstudents. La diferencia principal es que addstudents escribe en memoria desde un puntero y esta lee desde un puntero; el cálculo del puntero es el mismo, al igual que los parámetros de entrada. La función sigue el mismo patrón para imprimir los valores de cada struct.
Un parche sobre el programa de práctica
¿Qué ocurre entonces con la función showstudents? No aparece en el código; puede ser un error del desarrollador o una funcionalidad oculta. Podemos intentar corregirlo parcheando el programa.
Parchear un binario ELF (o cualquier programa) no es trivial; puede ser una operación muy compleja, ya que desplazar o añadir un solo byte puede romper todo. Explicar el formato ELF, los alineamientos de memoria y conceptos similares queda fuera del alcance de este artículo; lo trataremos más adelante. Hay que tener en cuenta que el programa tiene un tamaño fijo en disco y una estructura determinada. Si queremos añadir más código —más instrucciones—, como una llamada a sym.showstudents, tenemos dos opciones: a) sobreescribir parte del espacio existente (probablemente sobreescribiendo instrucciones) en, por ejemplo, la sección .text (marcada como ejecutable), o b) aumentar el tamaño del fichero, añadir el código y realinear todo. Optaremos por la primera opción ahora y profundizaremos en la segunda más adelante (si quieres más información sobre esa segunda opción, consulta: http://phrack.org/issues/66/14.html).
Seguiremos con la primera opción: sobreescribir espacio existente (instrucciones) en el programa para llamar a la función showstudents.
Tras examinar el código, vemos que la función sym.addstudents se llama aquí:
│ │╎│ 0x00000ae4 89d6 mov esi, edx ; signed int64_t arg2
│ │╎│ 0x00000ae6 4889c7 mov rdi, rax ; int64_t arg1
│ │╎│ 0x00000ae9 e8c6fdffff call sym.addstudents
│ │╎│ 0x00000aee 8945d4 mov dword [var_2ch], eax
│ │╎│ ; CODE XREFS from sym.func1 @ 0xaaf, 0xadb
El contexto alrededor de esta llamada es ideal: como tanto la dirección base como el contador se cargan como parámetros, sería perfecto añadir una llamada a showstudents justo antes de la llamada a addstudents, pero no podemos hacerlo sin «romper» el programa, ya que no podemos sobreescribir ninguna instrucción de ese bloque porque todas son relevantes.
Pero... volvamos a mirar dentro de showstudents:
│ 0x000007cc 48897de8 mov qword [var_18h], rdi ; arg1
│ 0x000007d0 8975e4 mov dword [var_1ch], esi ; arg2
│ 0x000007d3 488d3dd20300. lea rdi, str.Showing_students: ; 0xbac ; "Showing students: " ; const char *s
│ 0x000007da e861feffff call sym.imp.puts ; int puts(const char *s)
Hay un printf que no es necesario, así que podemos sobreescribirlo con una llamada a addstudents; de ese modo, antes de mostrar los estudiantes, el programa nos permitirá añadir uno nuevo.
Tras sobreescribir ahí, también podemos sobreescribir la llamada en sym.func1 con una llamada a showstudents. Si hacemos eso, también tendremos que actualizar el mov dword var_2ch, eax; como showstudents no devuelve nada, podemos sustituir ese mov por un inc.
En r2, el parcheo se realiza con wa. Con s nos desplazamos a la dirección que queremos actualizar y con wa ensamblamos una nueva instrucción. Recuerda que hay que abrir el programa con la opción de escritura habilitada (r2 -wA program) para editarlo.
$ r2 -wA struct1
[0x0000138b]> s 0x000014b8
[0x000014b8]> wa inc [ebp-0x2c]
│ │╎│ 0x00000ae6 4889c7 mov rdi, rax ; int64_t arg1
│ │╎│ 0x00000ae9 e8c6fdffff call sym.addstudents
│ │╎│ 0x00000aee ff45d4 inc dword [var_2ch]
│ │╎│ ; CODE XREFS from sym.func1 @ 0xaaf, 0xadb
│ └─└─> 0x00000af1 0fb645d3 movzx eax, byte [var_2dh]
[0x0000118f]> s 0x000011a5
[0x000011a5]> wa call sym.addstudents
Written 5 byte(s) (call sym.addstudents) = wx e8d5000000
[0x000011a5]> s 0x0000119e
[0x0000119e]> wa nop
Written 1 byte(s) (nop) = wx 90
[0x0000118f]> s 0x0000119f
[0x0000119f]> wa nop
│ 0x000007d3 90 nop
│ 0x000007d4 90 nop
│ 0x000007d5 3dd2030000 cmp eax, 0x3d2 ; "9_scanf"
│ 0x000007da e8d5000000 call sym.addstudents
│ 0x000007df c745fc000000. mov dword [var_4h], 0
[0x0000138b]> s 0x000014b3
[0x000014b3]> wa call sym.showstudents
Written 5 byte(s) (call sym.showstudents) = wx e8d7fcffff
│ │╎│ 0x00000ae6 4889c7 mov rdi, rax ; int64_t arg1
│ │╎│ 0x00000ae9 e8d6fcffff call sym.showstudents
│ │╎│ 0x00000aee ff45d4 inc dword [var_2ch]
│ │╎│ ; CODE XREFS from sym.func1 @ 0xaaf, 0xadb
First letter?
X
Age?
34
Mark?
4.6
Student num 0: First letter = P Age = 23 Mark = 3.400000
Student num 1: First letter = k Age = 3 Mark = 4.000000
Action? (q=Quit, a=Add, s=ShowAll)
En las siguientes lecciones profundizaremos en la modificación y el parcheo de binarios.
Vuelve al binario
Seguir el acceso a registros consecutivos y comprobar cambios sobre una copia del binario. Identifica al menos una instrucción y el dato que utiliza; conserva la compilación con la que lo has observado.
Fuente de esta práctica: student-array.c ↓.
Descargar los 13 ejemplos de fundamentos ↙
Puedes utilizar la consola del companion sin conversar con el agente, o abrir el ejecutable en tu desensamblador habitual.
./coursectl start 7 --example student-array
./coursectl consoleCierra la sesión anterior con ./coursectl stop antes de cambiar de laboratorio. Ver preparación.
Antes de continuar
Explica con tus palabras la operación estudiada y señala las instrucciones o capturas que la justifican. Distingue los datos observados de los nombres y tipos que has reconstruido.
El progreso se guarda en este navegador. Puedes recorrer las lecciones en cualquier orden.