2. Who We Are
Mariano Palomo
• Malware Analyst
@voidm4p
voidm4p@protonmail.com
Oscar García
• Malware Analyst
@rer0k_
r3r0k@protonmail.com
3. ¿Por qué estamos aquí?
• Como afrontar el reversing ante un “nuevo” lenguaje
• Debemos evolucionar y adquirir nuevos
conocimientos a medida que también lo hace el
malware
• Estar preparados y tener nuestro set de herramientas
para cuando nos enfrentemos a otros ejemplos en la
“vida real”
4. Go / GoLang
• Lenguaje de programación que permite
compilar binarios multiplataforma a
partir de un mismo código fuente.
• Utiliza recolector de basura para reservar
y liberar memoria automáticamente.
• Código fácil de mantener: sin clases,
herencia, constructores, excepciones, etc
5. Go Malware!
• Malware/Packers para Mac, Windows y Linux desde
un mismo código fuente
• Ejemplos:
• Ransomware: EKANS, Phantom, Robin Hood
• Go-mimikatz
• Merlin Agent
• Linux.Rex
• …
6. Proceso de generación de binarios
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
Código
Hello World
compilado
GOOS=<plataforma> GOARCH=<arquitectura> go build hello.go
7. Desde el punto de vista del reversing
_r0_386_windows (Entry Point) rt0_386 runtime.newproc(*runtime.main) main.main
_rt0_386
runtime_main main_main
1
2
3
8. Desde el punto de vista del reversing
• Casi 2000 subrutinas con tan solo 1 línea de código…
main.main
Wrapper para los panics
Carga de cadena
Llamada a println
10. Binarios stripped - Problemas
• Las funciones no retienen su nombre cuando se compila stripped. Sin
embargo, parecen seguir presentes en el binario si se utiliza el comando
strings.
• La carga de cadenas de caracteres es “extraña” y cambia con la
arquitectura. Además, todas las cadenas se agrupan en una misma
región, sin acabar en 0x00, apuntando a offsets concretos dentro de este
listado.
11. Binarios stripped – Recuperando los
nombres de las funciones
• La sección .gopclntab contiene una estructura que recoge todos los nombres
originales de funciones Presente en ELF, en PE hay que buscarla
• Magic header (LE): “FB FF FF FF 00 00”
• Estructura (x32):
+info: https://lekstu.ga/posts/pclntab-function-recovery/
Struct gopclntab {
char cabecera[8];
uint numeroDeFunciones;
struct funcion{
char dirección[4];
char offsetNombreFuncion[4];
}[numeroDeFunciones]
}
magic
12. Binarios stripped – Strings
• Definir como “data” y definir, buscando por el patrón que carga cadenas,
cada una de ellas con el tamaño adecuado
13. IDAGolangHelper
• Scripts escritos por George Zaytsev
• Cargar el binario en IDA
• Cargar script (File script file)
• Determinar versión de Go y seleccionar
• Rename functions, add standard go types,
parse types by moduledata, strings
(Shift+S)
IDA 6.8: https://github.com/sibears/IDAGolangHelper/tree/e2ead174d7abda3252f2995783fbf084d88cd56d
IDA 7.4: https://github.com/sibears/IDAGolangHelper/tree/8e063fdf573424d5d84ea02b17e73e1601b000e2
14. Reto: Go go go!
• Obtén el flag del siguiente binario
f6446fe1d43ecf9b26a6806d13528d11
15. file
• No tenemos mucha información, ¿qué tipo de fichero es?
o ELF
o Big Endian
o MIPS32
o stripped
16. MIPS
• Microprocessor without Interlocked Pipeline Stages
• Existen cinco revisiones compatibles hacia atrás del conjunto de
instrucciones del MIPS, llamadas MIPS I, MIPS II, MIPS III, MIPS IV y
MIPS 32/64
• Manual de conjunto de instrucciones:
https://www.dsi.unive.it/~gasparetto/materials/MIPS_Instruction_Set.pdf
Bueno, como sabéis esto del reversing normalmente es una tarea algo solitaria, una de esas noches navegando por internet por sitios absolutamente confiables me saltó un banner que decía algo así como.. AUDIO MI AMOL… y bueno, como la gente de CCN nos había comentado la idea de dar una charla sobre alguno de los retos de Atenea del año pasado pues, en este caso venimos dos personas a contaros lo que hemos hecho. Yo soy… OSCAR
¿Por qué estamos aquí? Pues bueno la idea es contar un poco como sería la tarea de afrontar el reversing ante un “nuevo” lenguaje de programación, que es GoLang. Entre comillas lo de nuevo, pq quizás no lo es tanto, pero puede que al no ser tan popular no lo conozca tanta gente, y por tanto no sea tan usado, o al menos eso es lo que se podría creer. Simplemente, por sondear un poco, cuantos de vosotros habéis oído hablar de Go? Vale, cuantos habéis hecho algún programa en Go? Y a cuantos les ha tocado alguna vez analizar algún binario hecho en este lenguaje?
Además, como analistas de malware, debemos evolucionar y adquirir nuevos conocimientos constantemente y, sobre todo, a medida que también adaptándonos a como evoluciona el propio desarrollo de malware.
Por tanto, debemos estar preparados y tener a punto nuestro set de herramientas para cuando nos tengamos que enfrentar a otros ejemplos en la “vida real”
Qué es GoLang? GoLang es un lenguaje de programación que permite compilar binarios multiplataforma a partir de un mismo código fuente, lo cual lo hace tremendamente atractivo desde el punto de vista del desarrollador. Solo tenemos que programar lo qe queremos hacer una vez y, dejar que el compilador haga su trabajo para obtener el mismo programa en diferentes arquitecturas y sistemas operativos. Además, a diferencia del lenguaje compilado más popular, C++, GoLang incluye un recolector de basura para el tema de reservar y liberar memoria, así que nos podemos olvidar de los alloc y free. Por otra parte, también se olvida de la orientación a objetos, herencia, constructores y demás, para poder crear un código más fácil de mantener.
Pues bien, todo esto, igual que podría sonar muy bien para cualquier programador, también lo hace para los desarrolladores de malware y, vemos ejemplos que han surgido últimamente, sobre todo con los ransomwares como EKANS, Phantom, Robin Hood, pero también otras tipologías como RATs o programas ya existentes que se han portado a Go como Go-mimikatz.
Entonces, de todo esto, desde el punto de vista del reversing, lo que nos interesa saber es que, desde un mismo código fuente, se puede generar un binario PE/ELF/MachO en el que incluye el framework de Go Runtime completo que se encarga de servir como capa intermedia entre el código programado y el sistema operativo, algo así como lo que veis en esa figurita. La compilación de un Hello World es tan sencilla como lo que veis ahí.
Si desensamblamos el código generado, vemos que el binario es enorme, debido a lo que os decía que incluye el código del framework completo. Con respecto al main que hemos programado, si partimos del entry point del programa, vemos que se realiza un salto a un label rt0_386 que crea un nuevo proceso pasándole la dirección de runtime.main que, a su vez, llama a main.main que es el código real programado.
Comienza MARIANO:
Como os decía mi compañero, cuando empiezas a indagar más en el código desensamblado, te das cuenta que con tan solo una línea de código para el Hello World, se han generado casi 2000 subrutinas que Go incluye para ejecutarse. A la derecha se aprecia cómo se ve la función main.main. Se ve la cadena “Hello World” siendo cargada pero también vemos alguna magia negra que así a simple vista no reconocemos, como llamadas a runtime.morestack.noctxt, en fin cosas que nos mete Go por medio. Todo esto son flujos que crea Go a partir de una única línea de código. Y ya, al final del todo esto, vemos la llamada a la función de la línea de código que habíamos escrito: el println de la fmt. Como se puede apreciar, tras el desensamblado de este tipo de binarios se aprecia la gran cantidad de código que se ha generado y esto puede resultar algo confuso, incluso pensar que se está intentando ocultar algo o que se trata de algo más que una simple línea de código. Un packer mega tocho o a saber.
Y, con el fin de intentar dificultar aún más la tarea de reversing, se puede utilizar la opción de compilación –s que strippea el código y “elimina” el nombre de las funciones utilizadas, como solemos ver en otros compiladores normalmente.
¿Qué pasa con esto? Pues que, por una parte, como veíais en la diapositiva anterior, las funciones ya no aparecen con su nombre bonito. Sin embargo, ocurre algo curioso y es que si le tiramos un comando strings, estos nombres de funciones siguen estando en algún sitio, porque aparecen ahí. Luego veremos por qué.
Por otra parte, las cadenas de caracteres también se cargan de otra forma, agrupándose todas como un bloque sin el final de cadena (0x00, \0) y, se referencia a ellos, mediante el inicio del bloque, el offset en el que se encuentra y su tamaño.
Pues bueno, como os adelantaba antes que ocurría con los stings, en los que seguían apareciendo el nombre de las funciones. Resulta que GoLang crea una sección en el binario llamada .gopclntab, la cual la podemos ver directamente en binarios ELF, pero en los PE hay que buscarla. Por suerte, resulta que comienza con el magic header “FB FF FF FF 00 00 XX XX” en Little Endian (8 bytes). En la estructura de esta sección, tras esto, se encuentra el tamaño de la tabla. En 32 bits, los siguientes 4 bytes apuntan a la localización de la primera función y, seguido a estos, un offset que apunta al nombre de esa función si se cuenta desde el principio de la tabla. De esta forma se puede obtener el nombre original de la función.
Con el caso de los strings, dependiendo de cada arquitectura, el algoritmo para cargarlos cambia un poco, pero la idea se basa en lo que os comentaba. Se suele hacer referencia al inicio del bloque y utilizar un offset y longitud para localizar el binario. Pero también puede ser un puntero a este valor+offset o, directamente un valor inmediato apuntando a la dirección.
Esto que os cuento, tampoco es que lo hayamos descubierto por primera vez nosotros. De hecho, hay un script de George Zaytsev que permite, tras cargar el binario en IDA aplicar esta idea. Simplemente, seleccionamos que intente determinar la versión de Go, ya que en base a esto, los algoritmos cambian un poco y, tras esto, podemos decirle que recupere el nombre de las funciones. Para el caso de los strings, habría que pulsar Shift+S para que ejecute el algoritmo dentro de la función que nos encontremos.
SIGUE OSCAR!!!
Dicho esto, entramos en materia. Atenea para el que no la conozca, es una plataforma de desafíos de seguridad informática. Esta se encuentra compuesta de diferentes tipos de retos: Criptografía y Esteganografía, Hacking Web, Forense,
Análisis de trafico, Reversing y OSINT y, del reto que os vamos a hablar, es Go go go! que se encuentra en la sección de retos archivados de 2019. El reto, dice simplemente: “obtén el flag del siguiente binario”. Pues vamos al lio.
Nos bajamos el ZIP, lo descomprimimos con la pass infected y, como no tenemos mucha información sobre el binario, le tiramos un file. Vemos que se trata de un ELF en Big Endian para arquitectura MIPS32 y que es stripped. Si se le tira un strings se pueden ver cadenas que hacen referencia a Go. Así que, además del título pues, podéis haceros una idea de por donde van los tiros no?
Vale, MIPS es el acrónimo de esos palabros que veis ahí y existen cinco revisiones del set de instrucciones. Aquí además os dejamos un enlace a un manual con el set de instrucciones que es la referencia que hemos utilizado para el reto.
Vamos a tirar con IDA para ver el desensamblado del código. Como ya explicamos en las diapositivas anteriores, las cadenas de caracteres están en un bloque conjunto que es referenciado por punteros.
Al tratarse de MIPS el Script de IDAGolangHelper no reconoce este set de instrucciones por lo tanto es necesario adaptarlo para que lo reconozca y haga el trabajo por nosotros.
Esta adaptación consiste en identificar los dos patrones mostrados en las imágenes.
Un primer formato vemos que es con la carga de la posición de memoria donde se encuentra el bloque + el desplazamiento y el tamaño que está en el load inmediatly
Un segundo formato con la carga de un inmediato que es la dirección al inicio de la cadena de caracteres directamente y, de igual forma, el tamaño.
Aquí vemos como modificamos el Script para que reconozca el nuevo patrón de instrucciones al cargar una cadena de caracteres
Si la instucción es li o la con los parámetros de un inmediato (o región de memoria) a un registro, se comprueba si es un string
Además, para el caso de los inmediatos, se añade un comentario para que aparezca el valor de la cadena cuando la carga ya que IDA no lo reconoce de forma automática.
El algoritmo para saber si es una cadena, en caso de tratarse de MIPS, sería, comprobar si los siguientes valores son sw -> li (tamaño) -> sw.
Tras esto, ya aparecerían las strings como véis aquí abajo
Una vez hecho esto, buscamos de la misma forma que antes el main que ha creado el programador (main_main). También es intenresante señalar que en la función main.init: se puede ver la carga de librerías que se han incluido para el desarrollo del programa. Destacan las dos últimas, que, como véis vienen de repositorios de github. La primera es para obtener y parsear la info del sistema (en Linux, el típico uname) y, la segunda, es para pintar cadenas como banners ASCII, rollo hacker to guapo.
Ya entrando en el main.main se puede ver que lo primero que hace es llamar al GetInfo y comprobar si el valor devuelto contiene la cadena 4.14.0, es decir, está buscando ejecutarse bajo esa versión de kernel. Después, se ve la definición de un par de arrays de bytes a partir de strings, para terminar utilizando un tercero como parámetro “key” en la creación de un nuevo objeto criptográfico AES. Más adelante, recupera una cadena en base64, pero que parece utilizar un alfabeto personalizado y, el resultado de decodificar esta cadena, es pasado a la función NewCFBDecrypter, donde el objeto AES prepara el descifrado en modo CFB los datos decodificados a descifrar.
Como se trata de MIPS, para poder debugear el binario, hemos optado por conectar el remote gdb de IDA a un qemu que inicia gdb y espera a que se le conecten por un puerto. En la imagen esta mostramos cómo lo hicimos.
Comenzamos poniendo un BP en la llamada a GetInfo que veíamos. Para evitar las comprobaciones que va a realizar, directamente, modificamos el contador del programa ($pc) para que la siguiente instrucción sea la del flujo que sigue si coincide la cadena. A partir de ahí, continuamos por todas las instrucciones que habíamos visto en estático.
Aquí vemos que la clave para la creación del objeto AES es, efectivamente, la última declarada en el código.
Continuamos con la ejecución y, vemos que llega hasta la parte en que va a decodificar la cadena Base64. Antes de esto, recupera el alfabeto personalizado que ha utilizado para encodear la cadena en el dword que señala el video. Ahí se ve, que son los caracteres del abecedario, minúscula y mayúscula, números y – y _.
Continuamos y vemos que decodifica la cadena y llega hasta la llamada a NewCFBDecrypter. En este punto, comprobamos que, pasándole el puntero a la cadena que se había decodificado, toma los 16 primeros bytes como vector de inicialización (IV) para preparar un descifrado mediante el objeto AES que veíamos al principio. La ejecución continúa y se recupera el resto de la cadena base64 decodificada para llamar a XORKeyStream que realiza el descifrado. Tras esta llamada, se puede ver como, en el objeto destino, aparece la cadena Flag: XXXX. Y, que tras comprobar en la plataforma, vemos que es correcta.
El programa tiene más flujo a partir de aquí, como comprobaciones para que el el sistema sea Linux y, para imprimir por pantalla con el arte ASCII esto. Pero, como ya teníamos el flag, no ha sido necesario seguir debugueando.