Motivación
El BROU posee una aplicación de llave digital para realizar transacciones que permite generar un one-time password.
Ya que no deseo tener instalada la aplicación en un dispositivo android…, ¿es posible replicar la generación del token en una aplicación de escritorio como KeePassXC que utilizo para gestionar mis contraseñas?
Inspección de la aplicación
Para inspeccionar la aplicación primero se debe descargar la aplicación. Para ello se me ocurren dos alternativas:
-
Instalar en un celular android la aplicación Aurora Store. Desde allí buscar la aplicación brou llave digital. Una vez encontrada, elegir la opción Manual download y cuando aparezca la opción de instalar la aplicación elegir Cancel.
Luego conectamos el celular al PC y utilizando el programa adb copiamos al pc el apk descargado:
$ doas apt-get install -Vy adb $ mkdir aurorastore; cd aurorastore $ adb pull /sdcard/Android/data/com.aurora.store/files/Downloads/uy.com.brou.token/17/uy.com.brou.token.apk . /sdcard/Android/data/com.aurora.store/files/Downloa...led, 0 skipped. 18.3 MB/s (4955654 bytes in 0.258s)
Nota: ya que confiamos en la aplicación Aurora Store confiamos en que el apk haya sido descargado directamente del Play Store y la aplicación no haya sido adulterada agregandole malware.
-
Se busca en internet el apk en base al nombre del paquete. Para ello vamos primero al Play Store y buscamos la página de la aplicación: https://play.google.com/store/apps/details?id=uy.com.brou.token. Luego buscamos en internet el apk de la aplicación
uy.com.brou.token
. Como ejemplo se encuentra el link https://apkdownload.com/down_BROU-Llave-Digital/uy.com.brou.token.v7a.html desde el cual descargamos la aplicación guardandola en un directorio que llamaremosapkdownload
.Nota: hay que tener en cuenta que un apk descargado de una fuente que no sea confiable puede haber sido adulterado y contener malware.
Por curiosidad se comparan los apk’s descargados, confirmandose que son el mismo:
$ sha256sum $(find aurorastore apkdownload -type f | sort)
d6608fd47be228bd81ed706e4438682346dd4ac116d8c6af8c01a0fa21071eec apkdownload/uy.com.brou.token.17.v7a.apk
d6608fd47be228bd81ed706e4438682346dd4ac116d8c6af8c01a0fa21071eec aurorastore/uy.com.brou.token.apk
(O sea, ya que confiamos en Aurora Store confirmamos que en este caso el apk no fué modificado por APK Downloader.)
A continuación se descarga y extrae la aplicación con apktool:
$ wget 'https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.6.1.jar'
$ java -jar apktool_2.6.1.jar d aurorastore/uy.com.brou.token.apk
I: Using Apktool 2.6.1 on uy.com.brou.token.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/jmpc/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
Ya se puede inspeccionar el código.
Lo primero que se nota es que es una aplicación hecha en cordova, o sea, es una aplicación que está escrita en parte utilizando HTML, CSS y JavaScript.
Se comienza a inspeccionar el código fuente javascript de la aplicación.
Código relacionado a la registración:
-
Comenzando por los archivos
assets/www/js/app/first_registration.js
yassets/www/js/app/registration.js
que son prácticamente idénticos (espacio más espacio menos a excepción de la funcióndisablePasscodeOnActivation
enfirst_registration.js
) se encuentran las funcionesFirstReg.getSeedFromCupon
yRegister.getSeedFromCupon
(según el archivo) que contiene el siguiente código:// TEST MODE! if (cupon == "99999999" && (priv.password == "999999" || priv.password == "999998")) { seedObtained("jNnbU15eXl5dMsxWZ5alkd9nFoWo1Eb1t0Izj4nh5PKVMGI0hOQLBQMv8k2t", cupon); } else if (priv.lastCuponUsed == null || priv.lastCuponUsed != cupon) { VURest.getSeed(priv.url, cupon, priv.responseDiv, seedObtained); } else { seedObtained(priv.lastSeedObtained, cupon); }
NOTA: ¡se encuentran datos de test!.
-
La función
VURest.getSeed
se encuentra en el archivo/assets/www/js/app/vurest.js
y es utilizada para intercambiar el código de asociación enviado por el banco vía email por una semilla (que despues veremos que está codificada) mediante el servicio <https://servicios.brou.com.uy/etoken/a.php?cupon={código de asociación}&callback=?>:var data; $.ajax({ url: Config.baseUrl() + "cupon=" + cupon + Config.getBankCode() + "&callback=?", dataType: 'json', data: data, timeout: Config.connectionTimeout }).done(function (data) {
-
Si bien en el caso anterior se adelantó el valor de
Config.baseUrl()
, este se encuentra en el archivo/assets/www/js/app/common.js
:Config.baseUrl = function () { if (Config.vuDebug) { return "https://vuas5.dev.vusecurity.com/vuserver/a.php?"; //return "http://demo.cloud.vusecurity.com//vuserver/a.php?"; } else { //return "https://ds-ap-b9.intra.brou.com.uy:8080/vuserver/a.php?" return "https://servicios.brou.com.uy/etoken/a.php?"; //return "https://servicios.brou.com.uy/etokentest/a.php?"; } }
-
La función
seedObtained
(enfirst_registration.js
/registration.js
) almacena la semilla codificada utilizandoVUStorage.store
yVUStorage.storeTempAccount
.Aquí se encuentra una referencia a
VUCrypto.decodeSeed
pero el valor obtenido -decryptedSeed
- no se almacena sinó que se utiliza para confirmar que se tiene una semilla válida.Aquí también se encuentra que se llama a
VURest.timeSyncAuto
. -
VUCrypto.decodeSeed
se encuentra en el archivo/assets/www/js/app/vucrypto.js
. Esta función decodifica la semilla a partir de la semilla codificada y la clave del usuario de cuatro dígitos.VUCrypto.decodeSeed = function(account, clave) { if (account.seed != null) { var decrypted = Aes.Ctr.decrypt(account.seed, clave, 256); var decrypted_seed = decrypted.split(" ")[1]; var decrypted_crc = decrypted.split(" ")[0]; if (clave != "" && decrypted_seed != undefined && decrypted_crc == crc32(decrypted_seed)) { return decrypted_seed; } } return null; }
-
VURest.timeSync
es llamada porVURest.timeSyncAuto
yVURest.timeSyncManual
. Esta llama a https://servicios.brou.com.uy/etoken/a.php?timesyncauto=do&callback=? o https://servicios.brou.com.uy/etoken/a.php?timesyncmanual=do&callback=? para obtener la hora (comparar con la salida dedate +%s
):$ curl -X POST 'https://servicios.brou.com.uy/etoken/a.php?timesyncauto=do&callback=?' ?([1649339854542,5])
Una vez obtenida la hora del servidor, se calcula el desfazaje existente con la hora del sistema y se almacena dicho valor para ser utilizado más adelante para corregir la hora al momento de generar el password:
var time_differential = parseInt(new Date().getTime()) - data[0]; VUStorage.setTimeDiff(time_differential);
Código relacionado a la generación del password:
-
Users.loadLoginToken
en/assets/www/js/app/users.js
: se decodifica la semilla llamando aVUCrypto.decodeSeed
para luego generar y mostrar el token llamando anextToken
. La funciónUsers.auth
es similar pero agrega lógica para borrar el token cuando se sobrepase una cantidad dada de errores al ingresar el PIN para decodificar la semilla. -
nexToken
enusers.js
llama periódicamente aloopToken
que genera el password y lo muestra. La lógica de generación del password es la siguiente:VUStorage.getTimeDiff(function(value){ delta = value; var time = new Date().getTime() - delta; var step = time/40000; var T = parseInt(step); priv.otp = hotp(priv.seed,T,"dec6");
Aquí
VUStorage.getTimeDiff
es utilizada para cargar la diferencia calculada entre la hora del sistema y la NTP (obtenida del servicio del banco). Si no hubiera diferencia en el tiempo,time
contendría la hora actual. -
hotp
enassets/www/js/lib/crypto/hotp-min.js
suponemos que calcula el HOTP. ¿El parámetro “dec6” indica que se devuelvan seis carácteres (el largo del password)?
Prueba de generación del código HOTP usando información de test
En este paso se instala la aplicación y se confirma que el cupón y password (pin) de pruebas funcionan y generan un token. Se va a Configuración > Gestión de Cuentas > +Agregar. Se aceptan los términos y condiciones y en la pantalla de Nuevo Usuario se ingresa:
- Alias: Test
- PIN: 999999
- Código de asociación: 99999999
Al aceptar aparece un mensaje que indica que el token se ha activado
correctamente. Ahora podemos elegir el usuario Test, ingresar como pin
999999
y se genera un código de seguridad.
Lo que se busca ahora es, partiendo de que se tiene el código y
que tenemos el seed codificado correspondiente al token,
jNnbU15eXl5dMsxWZ5alkd9nFoWo1Eb1t0Izj4nh5PKVMGI0hOQLBQMv8k2t
, encontrar si es
posible generar el mismo código de seguridad.
Tests utilizando código hardcoded en programa
Se codifica un script en nodejs para replicar la funcionalidad de generación del password.
NOTA: No he encontrado los parámetros ni la librería para reemplazar la
funcionalidad del archivo assets/www/js/lib/crypto/aes-ctr-min.js
, por lo que
este se utilizará en el código de prueba pero no se agrega al post ya que se
desconoce la licencia que posee.
Para realizar el código se utiliza nvm para manejar las versiones de node,
la versión de node lts/gallium
y las utilidades nodemon y js-beautify:
$ nvm install lts/gallium
$ echo `lts/gallium` > .nvmrc
$ nvm use
$ npm install --global nodemon
$ npm install --global js-beautify
Se genera el archivo aes.js
:
{
cat uy.com.brou.token/assets/www/js/lib/crypto/utf8-min.js
cat uy.com.brou.token/assets/www/js/lib/crypto/base64-min.js
cat uy.com.brou.token/assets/www/js/lib/crypto/aes-min.js
cat <<'END'
var ctrTxt = "";
END
cat uy.com.brou.token/assets/www/js/lib/crypto/aes-ctr-min.js
cat <<'END'
export { Aes };
END
} | js-beautify - > aes.js
Aquí agregamos las dependencias necesarias para utilizar Aes.Ctr.decrypt
y
desofuscamos el código para que sea más legible al momento de querer ver que
acciones realiza.
Se crea el archivo package.json para importar los módulos e incluir dependencias que serán utilizadas más adelante:
{
"name": "replicate-brou",
"version": "0.0.1",
"type": "module",
"dependencies": {
"crc-32": "^1.2.2",
"node-fetch": "^3.2.3",
"otplib": "^12.0.1"
}
}
Se instalan las dependencias:
$ npm install
El código de prueba para generar el token se escribe en el archivo test.js:
import { Aes } from "./aes.js";
import crc32 from 'crc-32/crc32.js';
import { hotp } from 'otplib';
const key = "999999";
const ciphertext = "jNnbU15eXl5dMsxWZ5alkd9nFoWo1Eb1t0Izj4nh5PKVMGI0hOQLBQMv8k2t";
// validate seed
const plaintext = Aes.Ctr.decrypt(ciphertext, key, 256);
const [ crc, seed ] = plaintext.split(" ");
if (crc != crc32.str(seed)) {
console.log("Invalid seed - abort");
process.exit(1);
}
console.log("Seed: " + seed);
// generate token
const counter = Math.floor(new Date().getTime() / 40000)
const token = hotp.generate(seed, counter);
console.log("Code: " + token);
Confirmamos que el token generado es correcto, o sea, que teniendo la semilla codificada y el pin se puede generar el mismo token que genera la aplicación del banco ejecutando a la vez la aplicación android y haciendo en la consola:
$ node test.js
para ejecutar el código anterior.
Replicación de aplicación android
En el archivo script.js se crea el programa para la obtención de la semilla a partír del código de activación y la posterior generación del password a partir de la semilla.
Durante las pruebas se constató que:
- El banco permite cambiar la llave virtual la cantidad de veces que se desee.
- En caso de error al capturar la salida del response al servicio del banco durante el intercambio del código de activación por la semilla codificada no hay otra opción más que ir al cajero más cercano a resetear la llave virtual.
Generación del password
A partir del código anterior, si tenemos el valor de la semilla es trivial generar el password:
function generatePassword(seed) {
const counter = Math.floor(new Date().getTime() / 40000)
return hotp.generate(seed, counter);
}
Decodificación de la semilla
Como se vió en el ejemplo previo del test, para decodificar la semilla se puede utilizar el siguiente código:
function obtainSeed(encryptedSeed, key) {
// decrypt seed and validate
const decryptedSeed = Aes.Ctr.decrypt(encryptedSeed, key, 256);
const [ crc, seed ] = decryptedSeed.split(" ");
if (crc != crc32.str(seed)) {
console.error("Decrypted invalid seed: " + decryptedSeed);
process.exit(1);
}
return seed;
}
Aquí nos interesa que en caso de error se nos muestre el valor decodificado de la semilla que consiste en la salida del CRC y el valor de la semilla.
Intercambio del código de activación por la semilla codificada
Los dos pasos previos los pudimos realizar sin necesitar invocar ningún servicio del banco y (por suerte) con la información de test que se encontraba en el código.
El paso que nos queda es obtener la semilla a partir del código de asociación que nos envía el banco vía email invocando un servicio web.
Ya se vió del código javascript de la aplicación que se realiza un request GET a <https://servicios.brou.com.uy/etoken/a.php?cupon={código de asociación}&callback=?> el cual devuelve el seed codificado.
Aquí es importante imprimir en caso de error la respuesta del servicio, ya que el código de activación solo puede ser utilizado una vez y si ocurre algún error en el programa hay que pedir otro código de activación y confirmar el pedido yendo a un cajero.
La respuesta esperada al intercambiar un código de activación es algo como esto:
?(["jNnbU15eXl5dMsxWZ5alkd9nFoWo1Eb1t0Izj4nh5PKVMGI0hOQLBQMv8k2t"])
A continuación el código:
async function retrieveEncryptedSeed(activationCode) {
// retrieve encrypted seed
const response = await fetch(`https://servicios.brou.com.uy/etoken/a.php?cupon=${activationCode}&callback=?`);
const body = await response.text();
// extract from response
try {
const [ , encryptedSeed ] = /"([^\"]+)"/.exec(body);
return encryptedSeed;
} catch(error) {
console.error(`Error extracting encrypted seed from: \`${body}\``);
console.error(error);
process.exit(1);
}
}
Como se indicó previamente, en caso de error al extraer la semilla codificada se muestra el contenido del mensaje.
Conclusiones
Positivo:
- Debido a que la aplicación de llave digital estaba codificada en javascript fué sencillo poder determinar su funcionamiento y replicarlo.
- La práctica en decompilar, modificar y generar aplicaciones android.
Negativo:
- Con el escaso tiempo dedicado no me fué posible encontrar un reemplazo a las funciones AES CTR, por lo que terminé utilizando las de la aplicación. Esto último condicionó a que el script utilizara nodejs.
- Quedó pendiente el modificar la semilla para poder utilizarla directamente en KeePassXC y que este genere el código TOTP.
Apendice - origen de la aplicación
A partir de la url inválida:
<http://www.comafi.com.ar/tokenempresas /descarga/solucionpc.aspx> que se
encuentra en los archivos first_registration.js
, help-terms.js
, help.js
y
registration.js
del directorio /assets/www/js/app
suponemos que la
aplicación fué adquirida a BANCO COMAFI. Siguiendo el link a la aplicación
android en https://www.comafi.com.ar/tokenempresas/ y comparando las pantallas
de las aplicaciones Comafi Token Empresas y BROU Llave Digital se encuentra
un parecido importante.
Apendice - modificación de aplicación cordova
Una alternativa que finalmente no fué utilizada consiste en decompilar, modificar, construir y ejecutar un apk modificado de la aplicación.
Para ello los pasos a seguir son:
-
Extraer archivos con apktool.
-
Renombrar directorio de trabajo y nombre de archivo apk generado.
-
Modificar nombre del paquete en el manifest y hacer la aplicación debuggable para poder inspeccionar los logs mediante logcat. También modificar el nombre de la aplicación en los recursos para no confundirla si se tiene la original instalada.
Los cambios anteriores están resumidos en el archivo 01-change-app-name.patch.
-
Modificar el código de la aplicación.
El cambio está resumido en el archivo 02-show-log-message.patch.
-
Construir el apk correspondiente a la aplicación modificada.
-
Crear debug keystore y firmar la aplicación.
-
Instalar la aplicación, ejecutar utilizando am y ver la salida de logcat.
Se creó el script modify.sh que realiza los pasos 1. a 6. Para el paso 7. se utilizan los comandos:
$ adb logcat -c
$ adb install uy.jumapico.brou.token/dist/uy.jumapico.brou.token-signed.apk
Performing Streamed Install
Success
$ adb shell am start -D -n uy.jumapico.brou.token/uy.com.brou.token.MainActivity
$ adb logcat 2>&1 | tee logcat.out
Apendice - generación de código
Mientras no investigue como cargar el seed para generar el OTP con KeePassXC es posible utilizar la aplicación javascript de esta sección:
Code: