by Miguel Angel G @M23Gzz
agradecimientos especiales @ZEr0Luis
El siguiente reto fue presentado durante el redpwnCTF
r1sc 55 solves / 487 points
"Look, Mum, no opcodes!"
TD;LR: Consiste en una VM muy particular, que implementa 3 instrucciones (ret, subleq y int3). Una vez que logramos entender cómo implementaba la VM dichas instrucciones el reto consistió en entender la lógica del programa. Se nos ocurrió poner breakpoints cuando el programa realizaba la lectura de nuestra input, y al hacerlo descubrimos que los 48 caracteres que conforman el input se dividen en palabras de 8 bytes, dichos bytes son interpretados como un entero sin signo, se realizan dos sustracciones a ese valor y el resultado se compara contra un valor especifico, de esta comparación se determina si la flag es correcta o incorrecta, dicho valor era para todos los casos 0xfffffffffffffffe y la comparación correcta ocurre cuando el valor obtenido de la sustracción es mayor, dejándonos una sola posibilidad 0xffffffffffffffff. Finalmente solo fue cuestión de obtener los valores que se restaban a cada palabra de 8 bytes de nuestro input de 48 bytes en tiempo de ejecución, y resolver la ecuación.
El nombre del archivo en conjunto con la descripción ya nos daban algunas pistas sobre el reto. r1sc hace referencia a la arquitectura RISC la cual se caracteriza por tener un instruction set reducido, mientras que a la vez nos advierte "Look, Mum no opcodes!" de alguna manera logran implementar dicha arquitectura de una manera no convencional sin el uso de "opcodes".
Al ejecutar el binario, solicita un código de acceso, el cuál al ser correcto desplegará el texto “Access authorized.” de no serlo "Access denied." ver Figura 1
Figura 1 - Ejecución del binario
El flujo del programa es siguiente:
- Lee input de usuario
- Ejecuta una sub-rutina
- Se compara una localidad de memoria contra 0 (Se infiere que dicha localidad es modificada en el paso 2)
- Si es igual a 0 “Access authorized.” de lo contrario "Access denied."
Solo quedaba analizar la subrutina del paso 2 para poder entender cómo obtener la flag.
En la figura 2 nos muestra el desensamblado de dicha rutina.
Figura 2 - Código desensamblado en GDB.
Vemos que los valores los toma con base en el registro RBP dicho registro se carga con una dirección estática en la función principal (offset 0x2038)
También notamos que la función es recursiva (offset 0x10b7)
Deducimos que es ahí donde se implementa la vm.
Con el fin de replicar las instrucciones dadas por el binario, se extrae la sección señalada por el RBP almacenado en el binario y se guardo en un binario aparte que contiene todas las instrucciones de la VM, esto se realizo con la ayuda de HxD.
Reubicando la dirección 0x0000555555557038 señalada en la ejecución, sería equivalente a la dirección 00003038 dentro del binario, la cual fue extraída del binario original y colocada en otro diferente que se denominó como “r1sc_rom”.
Figura 3 - Localización de la entrada en el binario
Al introducir la entrada “aaaaaaaaaaaa”
se puede identificar la dirección donde comienza el uso del input introducido
por el usuario siendo 0x555555557050 como se aprecia en las siguientes
imágenes, siendo 61 el código hexadecimal del carácter a.
Figura 4 - Entrada al binario
Figura 5 - Ubicación de la entrada en el binario
Al reubicar la dirección en el binario
“r1sc_rom” se manipulará directamente el input modificando el contenido
hexadecimal del binario que se encuentre en el rango de 00000018 a 00000048 que
comprendería los 6 bytes que constituye el input.
Figura 6 - Binario r1sc_rom
Desmenuzando la lógica:
1. RBP + RSI * 8 conforman la direccion efectiva de la instrucción actual
2. Solo se cuentan con 3 instrucciones:
- ret -> termina la ejecución del programa
- int3 -> Debugger
- subleq -> subtract and branch if less than or equal to zero
3. El registro RSI puede considerarse el program counter (PC)
4. Después de cada instrucción incrementa el PC en 3
5. Cada instrucción ocupa 24 bytes de memoria, conformando 3 argumentos de 8 bytes cada uno:
arg1, arg2, arg3
6. el registro RBP contiene el inicio del espacio de memoria para la VM dicho espacio es RAM y ROM.
7. Cada localidad de memoria tiene un tamaño de 8 bytes de ahí que [rsi] = rbp * rsi * 8
La primera instrucción es la siguiente
[0] 09 00 00 00 00 00 00 00
[1] 09 00 00 00 00 00 00 00
[2] 10 00 00 00 00 00 00 00
decodificada en little-endian tiene la siguiente estructura
9, 9, 10
La ejecucion es determinada no por el valor de los argumentos (De ahí la pista "Look Mum, no opcodes") sino por el resultado de la resta de los valores de memoria a los que apunta arg1 y arg2 , el valor de arg3 y en conjunto con el valor del PC (registro RSI)
si pc == 1:
fin del programa
si [arg2] > [arg1]:
pc = pc + 3
si no:
si arg3 == 2:
int3
si arg3 == 0:
pc = pc + 3
si arg3 != 0:
pc = arg3
[arg2] = [arg2] - [arg1]
Nota: [argn] denota el operador de indirección para el valor argn ej.[3] = rbp + 3 * 8 = rbp + 24
Antes de dar inicio a la ejecucion del programa se nos pide introducir un input de hasta 48 bytes, el cual se va almacenar en el mismo espacio de memoria del programa, específicamente de las localidades
rbp + 0x018 a rbp + 0x048 (o [3] a [8] inclusive)
dando como resultado 6 palabras de 64 bits cada una como entrada de usuario
Para encontrar la flag, lo que se hizo fue encontrar los lugares donde arg1 o arg2 entraran dentro del rango donde se encontraba almacenada la entrada del usuario.
Al encontrar los segmentos del programa que tenían interacción con el input se encontro lo siguiente
(para el caso de la localidad [5]):
PC arg1 arg2 arg3 [arg1] [arg2]
154 11 5 0 023a367c34e563f0 7463757274736e69
238 273 5 0 72293ef63f8e0a7a 72293ef63f8e0a79
253 15 5 277 fffffffffffffffe ffffffffffffffff
Como podemos observar en el fragmento anterior en la instruccion [154] se observa que arg1 = 11 y arg2 = 5 arg2 en este caso apunta a la direccion con offset rbp + 0x028 que conforma parte del input, en dicha instruccion se resta nuestro input contra el valor 0x23a367c34e563f0 y posteriormente en la direccion [238] se vuelve a restar pero ahora con el valor de 0x72293ef63f8e0a7a, para finalmente hacer la comparacion en la direccion [253] contra el valor de 0xfffffffffffffffe, el resultado de las restas ocurridas en [154] y [238] debe ser mayor o 0xfffffffffffffffe de tal manera que el pc se incrementara en 3, de lo contrario se realiza un salto a 277.
PC arg1 arg2 arg3 [arg1] [arg2]
277 9 9 0 0 0
280 9 9 0 0 0
283 9 9 0 0 0
286 9 9 0 0 0
289 9 9 0 0 0
292 9 9 1 0 0
Como se puede observar, si la ejecucion cae en la direccion de memoria [277] se inicia la secuencia de salida del programa. Mientras que si se cumple la condicion se obtiene lo siguiente:
PC arg1 arg2 arg3 [arg1] [arg2]
253 15 7 280 fffffffffffffffe -24311deb30242b22
Ahora se compara contra las localidades rbp + 0x38 que de igual manera conforma parte del input, este patrón es el mismo para las 6 palabras que conforma el input.
De lo anterior podemos deducir lo siguiente:
xi - d1i - d2i > fffffffffffffffe
xi - d1i - d2i = ffffffffffffffff
xi = d1i + d2i + ffffffffffffffff
donde xi contien 8 bytes del input original
Para cada una de las 6 palabras de 8 bytes que conforman nuestro input.
Resolviendo la ecuacion
xi = d1i + d21 - 1
[3] - d9cd4ab6a2d747 - 73899430b0be9520 > fffffffffffffffe
[3] = 7463617B67616C66
[4] - 16069317e428ca9 - 5dd2f647ee29d4cd > fffffffffffffffe
[4] = 5F335F796C6C6175
[5] - 23a367c34e563f0 - 72293ef63f8e0a7a > fffffffffffffffe
[5] = 7463757274736E69
[6] - 39a9fadb327f099 - 71d8bf8cc0467ed1 > fffffffffffffffe
[6] = 75735F3A736E6F69
[7]- 5d4d629e80d5489 - 5f9d8902895817da > fffffffffffffffe
[7] = 65725F2C71656C62
[8] - 96f75d79b354522 - 73c3fe96ce29e753 > fffffffffffffffe
[8] = 7D33746E695F2C74
[3] = tca{galf
[4] = _3_yllau
[5] = tcurtsni
[6] = us_:snoi
[7] = er_,qelb
[8] = }3tni_,t
finalmente para obtener la flag le hacemos el inverso a los numeros que obtuvimos
flag{actually_3_instructions:_subleq,_ret,_int3}
Se implementó el siguiente código
en Python3 para resolver este reto, se trata de la misma implementación de la vm pero ahora en python, se utiliza en conjunto con el archivo r1sc_rom que extrajimos del binario original:
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | import struct def vm(opcode, rom = []): jump = 0 arg1 = rom[opcode] arg2 = rom[opcode + 1] arg3 = rom[opcode + 2] if(arg1 < 9) or (arg2<9): print(rom[3:9]) print(opcode,arg1,arg2,arg3," ", "%x"%rom[arg1], "%x"%rom[arg2]) if(opcode == 1): return None if (rom[arg2] > rom[arg1]) or (jump != 0): opcode = opcode + 3 else: if arg3 == 2: print("debugging") return None if arg3 == 0: opcode = opcode + 3 if arg3 != 0: opcode = arg3 rom[arg2] = rom[arg2] - rom[arg1] if(arg1 < 9) or (arg2<9): print(rom[3:9]) print(opcode,arg1,arg2,arg3," ", "%x"%rom[arg1], "%x"%rom[arg2]) return opcode, rom def gen_rom(f): with open(f,'rb') as fd: buff = fd.read() r_iter = struct.iter_unpack('<Q',buff) rom = [i[0] for i in r_iter] return rom return None def main(): rom = gen_rom('r1sc_rom') if rom is None: return opcode, rom = vm(0, rom) counter = 0 while opcode is not None: try: opcode, rom = vm(opcode, rom) counter += 1 except Exception as e: break print(counter) if __name__ == "__main__": main() |
Comentarios
Publicar un comentario