Jugando con los opcodes de Dalvik

Introducción

El objetivo de esta entrada es recoger a modo de borrador algunas notas que he ido tomando durante mi investigación con los opcodes de Dalvik, explicando algunas nociones básicas sobre el funcionamiento de los mismos.

¿Qué es Dalvik?

Extraído de la Wikipedia:
"Dalvik is the process virtual machine (VM) in Google’s Android operating system. It is the software that runs the apps on Android devices. Dalvik is thus an integral part of Android, which is typically used on mobile devices such as mobile phones and tablet computers as well as more recently on embedded devices such as smart TVs and media streamers. Programs are commonly written in Java and compiled to bytecode. They are then converted from Java Virtual Machine-compatible .class files to Dalvik-compatible .dex (Dalvik Executable) files before installation on a device. The compact Dalvik Executable format is designed to be suitable for systems that are constrained in terms of memory and processor speed."
Para información más detallada sobre cómo está diseñada la arquitectura y cuál es su funcionamiento interno, puedes leer el siguiente artículo [1].

Dalvik Bytecode

Instrucciones utilizadas para interactuar con la máquina virtual Dalvik. [2] [3] [4]

Formato de las instrucciones

Cada opcode de Dalvik está asociado a un formato de instrucción, encargado de moldear el comportamiento del mismo.
Dicho formato está compuesto por varias palabras (16 bits) separados por un espacio, donde cada carácter representa cuatro bits, leídos desde el bit más significativo al menos, utilizando el carácter “|” intercalado para facilitar su lectura.
De esta forma, encontramos:
  • Mayúsculas secuencialmente ordenadas identifican los campos dentro del formato.
  • El término “op” es utilizado para indicar la posición de un opcode de 8 bits
  • El símbolo “Ø” determinad que los bits restantes han de ser cero.


Pongamos un ejemplo

Para el formato "B|A|op CCCC":
  • Está compuesta por dos palabras de 16 bits cada una, siendo la primera "B|A|op" y la segunda "CCCC"
  • La primera palabra está compuesta por el opcode posicionado en los 8 bits menos significativos y un par de valores de cuatro bits localizados en los 8 bits más significativos.
  • La segunda palabra consiste en un único valor de 16 bits.
Un ejemplo es la instrucción "if-eq vA, vB, +CCCC" donde "vA" representa el primer registro a comprobar (4 bits), "vB" representa el segundo registro (4 bits), y "+CCCC" representa el desplazamiento a realizar (16 bits). Siendo su cometido el desplazarse a otra zona de la memoria, dada por el posicionamiento actual más el valor del offset, si el contenido de los dos registros son iguales.
Para el formato "ØØ|op"
  • Tenemos una única palabra de 16 bits.
  • Los 8 bits menos significativos están establecidos a 0.
  • Los restantes 8 bits, están reservados para el opcode
Asociado a instrucciones como "nop" o "return-void".
Para el formato "ØØ|op AAAA BBBB"
  • Está compuesto por tres palabras (16 bits/palabra), siendo la primera "ØØ|op", la segunda "AAAA", y la tercera "BBBB".
  • La primera palabra los 8 bits menos significativo están establecidos a 0, mientras que los más significativos almacenan el opcode
  • La segunda palabra almacena un único valor de 16 bits.
  • Otro valor único de 16 bits es almacenado en la tercera y última.
Un ejemplo de instrucción puede ser "move/16 vAAAA, vBBBB" donde se desplaza el contenido del registro "vBBBB" a "vAAAA".

A su vez, cada formato está asociado a un "short ID", compuesto normalmente por tres caracteres (salvo casos excepcionales), dos digitos y una letra. Donde el primer dígito simboliza el número de palabras que integran el formato, el segundo determina el número de registros que hay, o viene expresado por el carácter "r" que expresa el uso de un rango de registros dentro del formato , y por último el tercer carácter representado por una letra, es utilizado para inidicar cualquier tipo de dato extra codificado en el propio formato.

De estos podemos distinguir la siguiente tabla:
Imagen tomada de source.android.com
Utilicemos unos ejemplos, el short ID "12x" establece que se está utilizando una palabra de 16 bits, junto a dos registros de 4 bits cada uno y que no se está codificando ningún tipo de datos adicional "(x)", siendo por tanto "B|A|op" el formato especificado.

Para el short ID "22s", podemos discernir que se están utilizando dos palabras de 16 bits, dos registros, y como dato adicional, que se incluye un ‘signed short’ de 16 bits. En este caso estamos hablando de "B|A|op CCCC".

Por último, con la intención de orientar todo esto a una sintáxis más legible, se propone utilizar para cada instrucción una construcción mucho más sencilla. Donde se comienza utilizando el nombre de cada opcode, y una lista de parámetros aceptados por el mismo, utilizando el carácter "," como separador, teniendo en consideración los siguientes aspectos:

  • Todo argumento etiquetado en el formato, recibirá la misma etiqueta en la sintáxis. Para "AA|op BB|CC" su homólogo será "op vAA, vBB, vCC" (23x)
  • Aquellos registros que tengan la forma "vX", utilizando el prefijo "v" intencionadamente en lugar de "r", es para evitar conflictos con arquitecturas (no virtuales) de Dalvik que sí lo utilicen.
  • Los argumentos que indican un valor inmediato presentan la forma "#+X".
  • Aquellos que indican un desplazamiento relativo se muestran con "+X".
  • Los que muestran un "literal constant pool index", lo hacen utilizando "kind@X" donde "kind" señaliza qué "constant pool" es indicada, existiendo: "string" (string pool index) | "type" (type pool index) | "field" (field pool index | "meth" (meth pool index).
  • También es posible indicar "prelinked offsets" existiendo de dos tipos: "vtaboff" (vtable offsets). | "fieldoff" (field offsets).

Por último, en aquellos formatos cuyo "short ID" es "35c", "35ms” y “35mi
Poniendo como ejemplo, la sintáxis “op vAA, #+BBBB” cuyo ‘short ID’ es “21s” y su formato “AA|op BBBB“, a su vez también acepta otras construcciones como “op vAA, vBBBB” para el ‘short ID’ “22x” o “op vAA, +BBBB” para 21t.

Todas ellas tienen el mismo formato inicial (AA|op BBBB) a pesar de que su representación es distinta:
op vAA, vBBBB” en este caso vBBBB hace referencia a un registro.
op vAA, +BBBB” aquí BBBB representa el offset añadido a la posición base.
"op vAA, #+BBBB” es interpretado como un valor inmediato.


REFERENCIAS

Fuente: http://blog.seguesec.com
[1] The Dalvik Virtual Machine Architecture [Eng]
[2] Dalvik Opcodes.
[3] Dalvik VM Instruction Formats
[4] Bytecode for the Dalvik VM



Autor: Sebastián Guerrero
Departamento de Auditoría