Publicado 2022-04-05. | Modificado 2022-05-01.
Palabras clave: android
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?
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
llamaremos apkdownload
.
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
y
assets/www/js/app/registration.js
que son prácticamente idénticos
(espacio más espacio menos a excepción de la función
disablePasscodeOnActivation
en first_registration.js
) se encuentran las
funciones FirstReg.getSeedFromCupon
y Register.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
(en first_registration.js
/registration.js
)
almacena la semilla codificada utilizando VUStorage.store
y
VUStorage.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 por VURest.timeSyncAuto
y
VURest.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 de date +%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 a VUCrypto.decodeSeed
para luego generar y mostrar el
token llamando a nextToken
. La función Users.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
en users.js
llama periódicamente a loopToken
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
en assets/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)?
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:
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.
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.
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:
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);
}
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.
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.
Positivo:
Negativo:
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.
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
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: