<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Bajo el Teclado</title><link>https://pointedsec.github.io/tags/reverse-engineering/</link><description>Recent content on Bajo el Teclado</description><generator>Hugo -- gohugo.io</generator><language>es</language><managingEditor>adelcerrorodriguez@gmail.com (Andrés Del Cerro)</managingEditor><webMaster>adelcerrorodriguez@gmail.com (Andrés Del Cerro)</webMaster><copyright>© 2024 pointedsec</copyright><lastBuildDate>Sat, 03 Oct 2026 14:00:00 +0100</lastBuildDate><atom:link href="https://pointedsec.github.io/tags/reverse-engineering/index.xml" rel="self" type="application/rss+xml"/><item><title>Auditoría estática de undetek.exe v10.51: anatomía de un loader de cheat para CS2</title><link>https://pointedsec.github.io/writeups/auditoria-estatica-undetek-loader-cheat-cs2/</link><pubDate>Sat, 03 Oct 2026 14:00:00 +0100</pubDate><author>adelcerrorodriguez@gmail.com (Andrés Del Cerro)</author><guid>https://pointedsec.github.io/writeups/auditoria-estatica-undetek-loader-cheat-cs2/</guid><description>Auditoría estática de undetek.exe v10.51 TL;DR. undetek.exe es un loader de cheat para Counter-Strike 2. No es malware. Se demuestra porque su payload, 25.226 bytes auditados al 100%, no tiene IAT propia, ni constantes propias, ni una sola cadena de texto legible, ni resuelve APIs por hash, y todas sus capacidades se reducen a 12 punteros a función que el loader le entrega. No puede escribir en disco, no puede crear procesos y no tiene forma de enviar datos a ninguna parte.</description><content:encoded><![CDATA[<h1 id="auditoría-estática-de-undetekexe-v1051">Auditoría estática de undetek.exe v10.51</h1>
<blockquote>
<p><strong>TL;DR.</strong> <code>undetek.exe</code> es un loader de cheat para Counter-Strike 2. <strong>No es malware.</strong> Se demuestra porque su payload, 25.226 bytes auditados al 100%, no tiene IAT propia, ni constantes propias, ni una sola cadena de texto legible, ni resuelve APIs por hash, y todas sus capacidades se reducen a 12 punteros a función que el loader le entrega. No puede escribir en disco, no puede crear procesos y <strong>no tiene forma de enviar datos a ninguna parte</strong>.</p>
<p>En paralelo se descubre que la &ldquo;licencia&rdquo; del producto es un PIN derivado del reloj público, que cualquiera con el <code>.exe</code> puede calcular. Y que el riesgo real para el usuario no es este binario, sino la guía que le pide desactivar el antivirus.</p>
</blockquote>
<p>Este documento recoge el análisis completo. Cada afirmación lleva su evidencia y, donde la evidencia es incompleta, se dice de forma explícita. También se documentan los errores del propio analista, porque un informe que no cuenta sus fallos no es fiable.</p>
<hr>
<h2 id="1-la-muestra">1. La muestra</h2>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Ruta</strong></td>
<td><code>/home/kali/Desktop/undetek/undetek-v10.51/undetek.exe</code></td>
</tr>
<tr>
<td><strong>MD5</strong></td>
<td><code>47cdf077d83b9e8c8b80364d2dbd9527</code></td>
</tr>
<tr>
<td><strong>SHA-1</strong></td>
<td><code>77468f710e2d45fcbab758a6beb8590e767d79be</code></td>
</tr>
<tr>
<td><strong>SHA-256</strong></td>
<td><code>82eb05556ffe1597e40353a0faef69e78f072d14e671adbf360c2161c06a8276</code></td>
</tr>
<tr>
<td><strong>Formato</strong></td>
<td>PE32+ (x86-64), Microsoft Visual C++</td>
</tr>
<tr>
<td><strong>CRT</strong></td>
<td>Universal C Runtime enlazado estáticamente</td>
</tr>
<tr>
<td><strong>Empaquetado</strong></td>
<td>Ninguno. Sin packer, sin secciones ejecutables anómalas</td>
</tr>
<tr>
<td><strong>Secciones</strong></td>
<td><code>.text</code>, <code>.rdata</code>, <code>.data</code>, <code>.pdata</code>, <code>.fptable</code>, <code>.reloc</code></td>
</tr>
<tr>
<td><strong>Timestamp</strong></td>
<td><code>0x6AC0B92E</code></td>
</tr>
<tr>
<td><strong>Compilacion</strong></td>
<td>ImageBase <code>0x140000000</code>, entrypoint RVA <code>0xA5F4</code></td>
</tr>
</tbody>
</table>
<p>El directorio también incluía cuatro ficheros de texto del propio vendedor: <code>Aimbot Guide.txt</code>, <code>Install Guide.txt</code>, <code>VAC.txt</code>, <code>capa.txt</code>. Los tres primeros se usan en este análisis como contexto declarativo, es decir, para contrastar lo que el código hace con lo que el autor dice que hace. El cuarto es un informe generado automáticamente por la herramienta capa y se trata aparte, porque tiene falsos positivos.</p>
<h3 id="11-restricción-metodológica">1.1 Restricción metodológica</h3>
<p><strong>El binario nunca se ejecutó.</strong> Todo el análisis es estático. Esto tiene dos consecuencias que se repiten a lo largo del documento:</p>
<ol>
<li>Lo que solo existe en tiempo de ejecución, es decir el payload descifrado dentro del proceso remoto, es inaccesible por definición para el análisis estático convencional.</li>
<li>El payload se auditó por extracción de indicadores y análisis de capacidades, no por desensamblado semántico completo.</li>
</ol>
<p><strong>Cobertura real:</strong> 100 por ciento de los bytes del loader, y 100 por ciento de los 25.226 bytes del payload mediante el método de capacidades descrito en la sección 8, con las limitaciones declaradas en la sección 12.</p>
<hr>
<h2 id="2-inventario">2. Inventario</h2>
<p>El binario contiene aproximadamente 760 funciones. La descomposición:</p>
<table>
<thead>
<tr>
<th>Rango</th>
<th>Funciones</th>
<th>Naturaleza</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>0x140007150</code> - <code>0x140028F10</code></td>
<td>~650</td>
<td>Universal C Runtime y vcruntime estáticos</td>
</tr>
<tr>
<td><code>0x140001000</code> - <code>0x1400037D0</code></td>
<td>~87</td>
<td>Helpers de ofuscación, cliente de red, inyector</td>
</tr>
<tr>
<td><code>0x140006A00</code> - <code>0x140007470</code></td>
<td>~12</td>
<td>Módulo principal, E/S de consola</td>
</tr>
<tr>
<td>Aisladas</td>
<td>~10</td>
<td>Enumeración de procesos, envoltorios de syscalls</td>
</tr>
</tbody>
</table>
<p>El punto de entrada es <code>entry</code> en <code>0x14000A5F4</code>, que invoca <code>__scrt_common_main_seh</code> en <code>0x14000A478</code>, que a su vez invoca <code>main</code> en <code>0x140007270</code>. La confirmación es una única referencia de código desde <code>0x14000A57F</code>.</p>
<h3 id="21-imports-de-aplicación">2.1 Imports de aplicación</h3>
<p><strong>Kernel32, explícitos:</strong> <code>VirtualAllocEx</code>, <code>VirtualProtectEx</code>, <code>WriteProcessMemory</code>, <code>ReadProcessMemory</code>, <code>OpenProcess</code>, <code>CreateRemoteThread</code>, <code>WaitForSingleObject</code>, <code>CreateToolhelp32Snapshot</code>, <code>Process32First</code>, <code>Process32Next</code>, <code>GetModuleHandleA</code>, <code>GetProcAddress</code>, <code>GetTickCount64</code>, <code>CloseHandle</code>, <code>Sleep</code>, <code>GetLastError</code>.</p>
<p><strong>USER32:</strong> <code>GetAsyncKeyState</code> y <code>SendInput</code>. Importadas pero, como se verá, nunca invocadas por el loader.</p>
<p><strong>ADVAPI32:</strong> <code>RegCreateKeyExA</code>, <code>RegSetValueExA</code>, <code>RegQueryValueExA</code>, <code>RegDeleteKeyA</code>, <code>RegOpenKeyExA</code>, <code>RegOpenKeyExW</code>, <code>RegCloseKey</code>, <code>RegSetValueExW</code>.</p>
<p><strong>WS2_32, importado por ordinal y no por nombre:</strong></p>






<pre tabindex="0"><code>Ordinal_3    Ordinal_4    Ordinal_16   Ordinal_19
Ordinal_23   Ordinal_111  Ordinal_115  Ordinal_116
getaddrinfo  freeaddrinfo        (estos dos, por nombre)</code></pre>
<p>El conjunto de ordinales {3, 4, 16, 19, 23} corresponde a <code>closesocket</code>, <code>connect</code>, <code>recv</code>, <code>send</code> y <code>socket</code>. El par {115, 116} corresponde a <code>WSAStartup</code> y <code>WSACleanup</code>. El paso 111 es <code>WSAGetLastError</code>. La identificación se confirma en el descompilado, donde aparece <code>WSAStartup(MAKEWORD(2, 2))</code> con el valor <code>0x202</code>.</p>
<p><strong>Importar Winsock por ordinal es una decisión deliberada de anti-análisis.</strong> Elimina del binario las cadenas <code>socket</code>, <code>connect</code>, <code>send</code> y <code>recv</code>, que son exactamente lo que un motor heurístico busca. Conviene subrayar que esta técnica es usada tanto por malware como por inyectores legítimos, y por sí sola no constituye un indicador.</p>
<hr>
<h2 id="3-nota-sobre-el-método-los-errores-del-analista">3. Nota sobre el método: los errores del analista</h2>
<p>Antes de entrar en el binario, conviene registrar los cuatro fallos cometidos durante este análisis, porque ilustran los límites del trabajo puramente estático con herramientas caseras:</p>
<ol>
<li><strong>Desensamblador propio con las instrucciones partidas.</strong> El primer intento de desensamblar el stub de 221 bytes produjo una salida en la que las instrucciones se cortaban por la mitad, por un error en el cálculo de ModRM e inmediatos. Se detectó al leer el código Assembly a mano y comparar.</li>
<li><strong>Bucle infinito en un parser de x86-64.</strong> El script de análisis de capacidades se quedaba colgado porque, cuando el byte actual no era ni un prefijo ni un opcode reconocido, el índice no avanzaba. Se manifestó como un cuelgue sin salida, no como un error.</li>
<li><strong>Tabla de flags de sección equivocada.</strong> Los valores <code>IMAGE_SCN_MEM_*</code> estaban desplazados una posición, de modo que <code>.text</code> aparecía como lectura y escritura en lugar de ejecución y lectura. La conclusión correcta, ninguna sección tiene W+X, se alcanzó solo al comparar los valores brutos con los estándar de MSVC.</li>
<li><strong>Estructura mal formada al leer el directorio de depuración.</strong> Se uso un campo de 1 byte donde la especificación exige 4, lo que desplaza la interpretación de todo el registro.</li>
</ol>
<p>Los cuatro son fallos de instrumentación, es decir, de no tener una suite de referencia con la que validar las herramientas propias. Es la lección práctica: cuando se escribe un desensamblador a mano para un análisis puntual, hay que contrastarlo con un desensamblador real antes de fiarse de el.</p>
<hr>
<h2 id="4-ofuscación-de-cadenas">4. Ofuscación de cadenas</h2>
<h3 id="41-el-mecanismo">4.1 El mecanismo</h3>
<p>Todas las cadenas del loader están cifradas con XOR de un solo byte. La plantilla es idéntica en las 51 funciones:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span><span style="color:#007f7f">// FUN_1400019c0, forma canonica
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span><span style="color:#fff;font-weight:bold">char</span> *FUN_1400019c0(<span style="color:#fff;font-weight:bold">char</span> *buf) {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span>    <span style="color:#fff;font-weight:bold">if</span> (*buf == <span style="color:#0ff;font-weight:bold">&#39;\0&#39;</span>)                                  <span style="color:#007f7f">// cache: solo descifra una vez
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>        <span style="color:#fff;font-weight:bold">for</span> (<span style="color:#fff;font-weight:bold">int</span> i = <span style="color:#ff0;font-weight:bold">0</span>; i &lt; <span style="color:#ff0;font-weight:bold">10</span>; i++)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span>            buf[i] = PTR_DAT_1400297C8[i] ^ <span style="color:#ff0;font-weight:bold">0x34</span>;       <span style="color:#007f7f">// XOR byte a byte
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#007f7f"></span>    buf[<span style="color:#ff0;font-weight:bold">10</span>] = <span style="color:#0ff;font-weight:bold">&#39;\0&#39;</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">7</span><span>    <span style="color:#fff;font-weight:bold">return</span> buf;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">8</span><span>}</span></span></code></pre></div>
<p>Tres parámetros por cadena: dirección del blob cifrado, longitud y clave. La clave es <code>0x34</code> en todos los casos observados. Existe una variante de cadena ancha que lee <code>*(ushort*)(blob + i * 2) ^ 0x34</code>, y una función aparte, <code>XorDecodeWideInPlace</code>, que aplica el mismo XOR de 16 bits sobre un buffer ya colocado en su destino.</p>
<p>El ciclo de vida de cada cadena es siempre el mismo y no deja rastro en memoria estática:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>LEA  RCX,[RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x49A8</span>]        <span style="color:#007f7f">; buffer local en la pila
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>MOV  ECX,<span style="color:#ff0;font-weight:bold">0x15</span>                <span style="color:#007f7f">; tamaño
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span>STOSB.REP RDI                <span style="color:#007f7f">; memset a cero
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>CALL FUN_140001250           <span style="color:#007f7f">; helper de memset, devuelve el puntero
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>CALL FUN_140001D20           <span style="color:#f00">;</span> descifrador, escribe aquí</span></span></code></pre></div>
<p>Las 51 funciones de descifrado ocupan el rango <code>0x1400014B0</code> a <code>0x140003010</code>, con paso constante de <code>0x90</code>. Aparte hay 32 helpers de <code>memset</code> de tamaño fijo en <code>0x140001000</code> a <code>0x140001490</code>.</p>
<p>El descifrado en la pila es deliberado: un volcado de memoria del fichero jamás revela el texto, y un escáner de cadenas del antivirus no tiene nada que detectar.</p>
<h3 id="42-recuperación-de-las-cadenas">4.2 Recuperación de las cadenas</h3>
<p>La clave del proceso está en cómo Ghidra nombra los símbolos. Cuando un blob está definido como cadena, el descompilador lo renderiza con el nombre del símbolo incluyendo el texto cifrado, por ejemplo <code>PTR_s_R_FYU__QP4_1400297D8</code>. Descifrar es restar <code>0x34</code>.</p>
<p>La verificación cruzada se hizo con dos casos independientes:</p>






<pre tabindex="0"><code>blob &#34;R[FYU@@QP4&#34;            -&gt;  f o r m a t t e d \0   = &#34;formatted&#34;
blob &#34;z@eAQFM}ZR[FYU@][ZdF[WQGG4&#34;
                             -&gt;  NtQueryInformationProcess     (24 caracteres)
blob &#34;g[R@CUFQhAP@_4&#34;       -&gt;  S o f t w a r e \ u d t k \0 = &#34;Software\udtk&#34;</code></pre>
<p>Veinticuatro caracteres correctos de forma consecutiva no son casualidad. Eso valida la clave más allá de la suposición.</p>
<p>Para los blobs que Ghidra no tenía indexados, se escribió una herramienta que hace fuerza bruta: aplica XOR <code>0x34</code> a la sección <code>.rdata</code> completa y extrae rachas de bytes imprimibles, tanto en ASCII como en UTF-16. Así se recuperaron alrededor de 35 cadenas adicionales.</p>
<h3 id="43-las-cadenas-recuperadas">4.3 Las cadenas recuperadas</h3>
<p><strong>Infraestructura:</strong></p>
<table>
<thead>
<tr>
<th>Contenido</th>
<th>Uso</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>formatted</code></td>
<td>nombre de campo JSON de la API de hora, no un marcador de protocolo</td>
</tr>
<tr>
<td><code>vip.timezonedb.com</code></td>
<td>host, 19 bytes: 18 más NUL</td>
</tr>
<tr>
<td><code>80</code></td>
<td>puerto, 3 bytes: 2 más NUL</td>
</tr>
<tr>
<td><code>GET /v2.1/get-time-zone?key=&lt;API_KEY_REDACTED&gt;&amp;format=json&amp;by=zone&amp;zone=Europe/London HTTP/1.1\r\nHost: vip.timezonedb.com\r\nConnection: close\r\n\r\n</code></td>
<td>petición, 138 bytes literales</td>
</tr>
<tr>
<td><code>%04lld</code></td>
<td>formato del <code>sprintf</code> que genera el PIN</td>
</tr>
<tr>
<td><code>%4s</code></td>
<td>formato del <code>scanf</code> que lee el PIN</td>
</tr>
<tr>
<td><code>Software\udtk</code></td>
<td>clave de registro, 14 caracteres</td>
</tr>
</tbody>
</table>
<p><strong>Módulos y exportaciones:</strong></p>
<table>
<thead>
<tr>
<th>Contenido</th>
<th>Uso</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>cs2.exe</code></td>
<td>proceso objetivo secundario, 8 bytes</td>
</tr>
<tr>
<td><code>gameoverlayui64.exe</code></td>
<td>proceso objetivo principal, 19 bytes</td>
</tr>
<tr>
<td><code>ntdll.dll</code></td>
<td>módulo de las APIs de bajo nivel y de las matemáticas</td>
</tr>
<tr>
<td><code>client.dll</code></td>
<td>módulo del juego del que se extrae la base</td>
</tr>
<tr>
<td><code>vgui2_s.dll</code></td>
<td>módulo de la interfaz gráfica</td>
</tr>
<tr>
<td><code>NtQueryInformationProcess</code></td>
<td>resuelta por nombre desde ntdll</td>
</tr>
<tr>
<td><code>NtReadVirtualMemory</code></td>
<td>resuelta por nombre desde ntdll</td>
</tr>
<tr>
<td><code>CreateInterface</code></td>
<td>Fábrica de interfaces del motor</td>
</tr>
<tr>
<td><code>VGUI_Setup001</code></td>
<td>interfaz 1</td>
</tr>
<tr>
<td><code>VGUI_Surface039</code></td>
<td>interfaz 2</td>
</tr>
<tr>
<td><code>_itow</code>, <code>sqrt</code>, <code>pow</code>, <code>fabs</code></td>
<td>matemáticas del CRT reexportadas por ntdll</td>
</tr>
</tbody>
</table>
<p><strong>Configuración del cheat:</strong></p>
<table>
<thead>
<tr>
<th>Contenido</th>
<th>Tipo</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Aimbot</code>, <code>Smooth</code>, <code>Hitbox</code>, <code>Trigger</code>, <code>Spotted</code>, <code>Esp</code>, <code>Theme</code></td>
<td>features</td>
</tr>
<tr>
<td><code>Aim Spot</code>, <code>Key</code></td>
<td>opciones del menú</td>
</tr>
<tr>
<td><code>On</code>, <code>Off</code></td>
<td>valores de interruptor</td>
</tr>
<tr>
<td><code>Tahoma</code></td>
<td>fuente del menú</td>
</tr>
<tr>
<td><code>41 BF ?? ?? ?? FF 15 ?? ?? ?? ?? 48 8B 53 20 48 3B C2 73 0A 48 8B C8 48 2B CA 48 01 4B 30</code></td>
<td>firma de bytes buscada en memoria</td>
</tr>
</tbody>
</table>
<p>Las 51 funciones de descifrado quedaron identificadas y renombradas con su contenido real.</p>
<hr>
<h2 id="5-la-red-el-c2-que-no-era-un-c2">5. La red: el C2 que no era un C2</h2>
<h3 id="51-lo-que-parecía">5.1 Lo que parecía</h3>
<p>El perfil estático encajaba con un canal de comando y control. La función <code>C2_AuthExchange</code> en <code>0x140003030</code> confirma el patrón completo, con toda la API de Winsock importada por ordinal:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>Ordinal_115(MAKEWORD(<span style="color:#ff0;font-weight:bold">2</span>,<span style="color:#ff0;font-weight:bold">2</span>), &amp;wsadata);                     <span style="color:#007f7f">// WSAStartup
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>getaddrinfo(szHostName, szServicePort, &amp;hints, &amp;res);     <span style="color:#007f7f">// resolver
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span>Ordinal_23(ai_family, ai_socktype, ai_protocol);           <span style="color:#007f7f">// socket
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>Ordinal_4(sock, ai_addr, ai_addrlen);                      <span style="color:#007f7f">// connect
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>Ordinal_19(sock, szRequest, strlen(szRequest), <span style="color:#ff0;font-weight:bold">0</span>);          <span style="color:#007f7f">// send
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#007f7f"></span>Ordinal_16(sock, pbRecvBuf, <span style="color:#ff0;font-weight:bold">0x1000</span>, <span style="color:#ff0;font-weight:bold">0</span>);                    <span style="color:#007f7f">// recv
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">7</span><span><span style="color:#007f7f"></span>Ordinal_3(sock);                                           <span style="color:#007f7f">// closesocket
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">8</span><span><span style="color:#007f7f"></span>Ordinal_116();                                             <span style="color:#007f7f">// WSACleanup
</span></span></span></code></pre></div>
<p>Los parámetros de <code>hints</code> se leen directamente del descompilado:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>MOV dword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0xBC</span>],<span style="color:#ff0;font-weight:bold">0x0</span>     <span style="color:#007f7f">; ai_flags      = 0
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>MOV dword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0xC0</span>],<span style="color:#ff0;font-weight:bold">0x1</span>     <span style="color:#007f7f">; ai_socktype   = SOCK_STREAM
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span>MOV dword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0xC4</span>],<span style="color:#ff0;font-weight:bold">0x6</span>     <span style="color:#f00">;</span> ai_protocol   <span style="color:#f00">=</span> IPPROTO_TCP</span></span></code></pre></div>
<p>Y el <code>send</code> es una llamada única, sin bucle de reintento:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span><span style="color:#f00">140003343</span>  CALL <span style="color:#ff0;font-weight:bold">0x140001370</span>            <span style="color:#007f7f">; memset
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span><span style="color:#f00">14000334</span>B  CALL <span style="color:#ff0;font-weight:bold">0x140002500</span>            <span style="color:#007f7f">; descifrar la petición (138 bytes)
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span><span style="color:#f00">140003355</span>  MOV  RCX,[RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x68</span>]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#f00">14000335</span>A  CALL strlen                 <span style="color:#007f7f">; 0x140028660
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span><span style="color:#f00">14000335</span>F  XOR  R9D,R9D               <span style="color:#007f7f">; flags = 0
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#007f7f"></span><span style="color:#f00">140003362</span>  MOV  R8D,EAX                <span style="color:#007f7f">; len = strlen(payload)
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">7</span><span><span style="color:#007f7f"></span><span style="color:#f00">140003365</span>  MOV  RDX,[RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x68</span>]         <span style="color:#007f7f">; buf
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">8</span><span><span style="color:#007f7f"></span><span style="color:#f00">14000336</span>A  MOV  RCX,[RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x28</span>]         <span style="color:#007f7f">; sock
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">9</span><span><span style="color:#007f7f"></span><span style="color:#f00">14000336</span>F  CALL qword ptr [<span style="color:#ff0;font-weight:bold">0x140029390</span>] <span style="color:#f00">;</span> send</span></span></code></pre></div>
<h3 id="52-lo-que-realmente-es">5.2 Lo que realmente es</h3>
<p>Las cadenas descifradas dan la respuesta:</p>






<pre tabindex="0"><code>host   = &#34;vip.timezonedb.com&#34;
puerto = &#34;80&#34;
petición =
  GET /v2.1/get-time-zone?key=&lt;API_KEY_REDACTED&gt;&amp;format=json&amp;by=zone
  &amp;zone=Europe/London HTTP/1.1\r\n
  Host: vip.timezonedb.com\r\n
  Connection: close\r\n\r\n</code></pre>
<p>Y el supuesto &ldquo;marcador de protocolo&rdquo; que el cliente exige encontrar en la respuesta es la cadena <code>formatted</code>.</p>
<p><strong><code>formatted</code> no es un marcador de protocolo propio. Es el nombre de un campo de la respuesta JSON de timezonedb.com:</strong></p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>{<span style="font-weight:bold">&#34;country&#34;</span>:<span style="color:#0ff;font-weight:bold">&#34;GB&#34;</span>,<span style="font-weight:bold">&#34;countryName&#34;</span>:<span style="color:#0ff;font-weight:bold">&#34;United Kingdom&#34;</span>,<span style="font-weight:bold">&#34;región&#34;</span>:<span style="color:#0ff;font-weight:bold">&#34;England&#34;</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span> <span style="font-weight:bold">&#34;city&#34;</span>:<span style="color:#0ff;font-weight:bold">&#34;London&#34;</span>,<span style="font-weight:bold">&#34;zone&#34;</span>:<span style="color:#0ff;font-weight:bold">&#34;Europe/London&#34;</span>,<span style="font-weight:bold">&#34;abbreviation&#34;</span>:<span style="color:#0ff;font-weight:bold">&#34;BST&#34;</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span> <span style="font-weight:bold">&#34;dst&#34;</span>:<span style="color:#fff;font-weight:bold">true</span>,<span style="font-weight:bold">&#34;offset&#34;</span>:<span style="color:#ff0;font-weight:bold">3600</span>,<span style="font-weight:bold">&#34;currenttime&#34;</span>:<span style="color:#ff0;font-weight:bold">1759248896</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span> <span style="font-weight:bold">&#34;formatted&#34;</span>:<span style="color:#0ff;font-weight:bold">&#34;2026-05-31 14:14:56&#34;</span>}</span></span></code></pre></div>
<p>El <code>strstr(respuesta, &quot;formatted&quot;)</code> busca ese nombre de campo. El código parsea después la hora, la trata como <code>HH:MM:SS</code> y deriva de ella un número.</p>
<h3 id="53-el-recv-y-sus-defectos">5.3 El <code>recv</code> y sus defectos</h3>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 1</span><span><span style="color:#f00">1400033</span>CF  XOR  R9D,R9D
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 2</span><span><span style="color:#f00">1400033</span>D2  MOV  R8D,[RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x5C</span>]      <span style="color:#007f7f">; 0x1000
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 3</span><span><span style="color:#007f7f"></span><span style="color:#f00">1400033</span>D7  LEA  RDX,[RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x730</span>]     <span style="color:#007f7f">; buffer de 4096
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 4</span><span><span style="color:#007f7f"></span><span style="color:#f00">1400033</span>DF  MOV  RCX,[RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x28</span>]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 5</span><span><span style="color:#f00">1400033</span>E4  CALL qword ptr [<span style="color:#ff0;font-weight:bold">0x140029358</span>]  <span style="color:#007f7f">; recv
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 6</span><span><span style="color:#007f7f"></span><span style="color:#f00">1400033</span>EA  MOV  [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x20</span>],EAX
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 7</span><span><span style="color:#f00">1400033</span>EE  CMP  dword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x20</span>],<span style="color:#ff0;font-weight:bold">0x0</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 8</span><span><span style="color:#f00">1400033</span>F3  JLE  <span style="color:#ff0;font-weight:bold">0x1400033F7</span>          <span style="color:#007f7f">; si ret &lt;= 0, salta
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 9</span><span><span style="color:#007f7f"></span><span style="color:#f00">1400033</span>F5  JMP  <span style="color:#ff0;font-weight:bold">0x1400033FE</span>         <span style="color:#007f7f">; si ret &gt; 0, sale
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">10</span><span><span style="color:#007f7f"></span><span style="color:#f00">1400033</span>F7  CMP  dword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x20</span>],<span style="color:#ff0;font-weight:bold">0x0</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">11</span><span><span style="color:#f00">1400033</span>FC  JG   <span style="color:#ff0;font-weight:bold">0x1400033CF</span>         <span style="color:#007f7f">; ret &gt; 0 -&gt; reintenta
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">12</span><span><span style="color:#007f7f"></span><span style="color:#f00">1400033</span>FE  ...</span></span></code></pre></div>
<p><strong>El bucle de reintento es código muerto.</strong> La instrucción <code>JLE</code> solo se toma cuando <code>ret &lt;= 0</code>, y en <code>0x1400033F7</code> se reevalúa <code>CMP ret, 0</code> con <code>ret &lt;= 0</code>, de modo que <code>JG</code> no se toma nunca. Es un único <code>recv</code> bloqueante, sin <code>MSG_WAITALL</code> y sin acumulación.</p>
<p>Dos consecuencias prácticas: una lectura parcial del servidor se interpreta como respuesta completa, que es una fragilidad real; y el buffer se pone a cero con <code>memset</code> antes de usar, de modo que queda terminado en NUL de facto, lo que evita un desbordamiento pero no por diseño.</p>
<h3 id="54-conclusión-de-esta-fase">5.4 Conclusión de esta fase</h3>
<p><strong>No existe servidor del autor.</strong> No hay comando y control, no hay descarga de configuración, no hay telemetría.</p>
<p>El loader llama a una API REST pública de zona horaria por HTTP plano, sin TLS, usando una clave de límite de peticiones que es pública, y emplea la hora oficial de la zona <code>Europe/London</code> como reloj.</p>
<p>La petición es un literal de 138 bytes sin ninguna llamada a <code>sprintf</code> que lo parametrice. <strong>No se envía nada identificativo de la máquina</strong>: no hay nombre de equipo, ni huella de hardware, ni usuario. No hay exfiltración porque no hay datos que exfiltrar.</p>
<hr>
<h2 id="6-el-pin-un-reloj-no-una-licencia">6. El PIN: un reloj, no una licencia</h2>
<h3 id="61-el-algoritmo">6.1 El algoritmo</h3>
<p>La función <code>DeriveAuthCodeFromHMS</code> en <code>0x140003670</code> implementa la derivación:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span><span style="color:#fff;font-weight:bold">void</span> DeriveAuthCodeFromHMS(<span style="color:#fff;font-weight:bold">char</span> *tok) {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span>    <span style="color:#fff;font-weight:bold">uint32_t</span> h = atoi(tok);   <span style="color:#fff;font-weight:bold">while</span> (*p != <span style="color:#0ff;font-weight:bold">&#39;:&#39;</span>) p++;   <span style="color:#007f7f">// &#34;14:14:56&#34; -&gt; 14
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span>    <span style="color:#fff;font-weight:bold">uint32_t</span> m = atoi(p);     <span style="color:#fff;font-weight:bold">while</span> (*p != <span style="color:#0ff;font-weight:bold">&#39;:&#39;</span>) p++;   <span style="color:#007f7f">//             -&gt; 14
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>    <span style="color:#fff;font-weight:bold">uint32_t</span> s = atoi(p);                                 <span style="color:#007f7f">//             -&gt; 56
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span>    <span style="color:#fff;font-weight:bold">int64_t</span>  slot = (h * <span style="color:#ff0;font-weight:bold">0xE10</span> + m * <span style="color:#ff0;font-weight:bold">0x3C</span> + s) / <span style="color:#ff0;font-weight:bold">0xB4</span>;  <span style="color:#007f7f">// (3600h + 60m + s) / 180
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">7</span><span><span style="color:#007f7f"></span>    <span style="color:#fff;font-weight:bold">uint32_t</span> code = (slot * <span style="color:#ff0;font-weight:bold">0x75BCD15</span>) % <span style="color:#ff0;font-weight:bold">10000</span>;          <span style="color:#007f7f">// 0x75BCD15 = 123.456.789
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">8</span><span><span style="color:#007f7f"></span>    sprintf(g_szDerivedAuthCode, <span style="color:#0ff;font-weight:bold">&#34;%04lld&#34;</span>, abs(code));
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">9</span><span>}</span></span></code></pre></div>
<p>Las constantes merecen comentario. <code>0xE10</code> es 3600, <code>0x3C</code> es 60, <code>0xB4</code> es 180: la ventana de tres minutos. Y <code>0x75BCD15</code> es exactamente <code>123.456.789</code>, los primeros dígitos de pi, que es la constante multiplicadora clásica de una función hash de Stephen Nash. No es una clave; es un número elegante.</p>
<p>Reduciendo módulo 10000, el resultado se simplifica a:</p>






<pre tabindex="0"><code>PIN = ( slot * 6789 ) mod 10000</code></pre>
<p>donde <code>slot</code> son los segundos desde medianoche en Londres divididos por 180.</p>
<h3 id="62-el-valor-está-verificado-por-inversión">6.2 El valor está verificado por inversión</h3>
<p>Un modelo algorítmico no es una prueba. Se comprueba invirtiendo la congruencia. Se resuelve <code>6789 * slot = 2202 (mod 10000)</code> mediante euclides extendido, que da <code>6789</code> inverso congruente con <code>109</code> módulo <code>10000</code>:</p>






<pre tabindex="0"><code>slot = 109 * 2202  =  240018  =  18  (mod 10000)</code></pre>
<p>Comprobación directa: <code>18 * 123456789 = 2.222.222.202</code>, cuyo residuo módulo 10000 es exactamente <code>2202</code>.</p>
<p><strong>Y <code>slot = 18</code> significa <code>18 * 180 = 3240</code> segundos, es decir las 00:54:00 hora de Londres.</strong> Un PIN de cuatro dígitos se convierte en una hora exacta del día. Eso confirma el modelo de forma independiente.</p>
<h3 id="63-lo-que-esto-significa-como-diseño">6.3 Lo que esto significa como diseño</h3>
<table>
<thead>
<tr>
<th>Propiedad</th>
<th>Valor</th>
</tr>
</thead>
<tbody>
<tr>
<td>Secretos de servidor</td>
<td><strong>ninguno</strong>, todo se computa desde el reloj</td>
</tr>
<tr>
<td>Valores posibles por día</td>
<td><strong>480</strong> (86399 / 180 + 1)</td>
</tr>
<tr>
<td>Ventana de validez</td>
<td>180 segundos</td>
</tr>
<tr>
<td>Formato de entrada</td>
<td>4 dígitos, <code>%4s</code></td>
</tr>
<tr>
<td>Dónde se decide</td>
<td><strong>solo en el cliente</strong>, en un <code>strcmp</code></td>
</tr>
</tbody>
</table>
<p>La comprobación final, literalmente:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span><span style="color:#f00">140003601</span>  CALL <span style="color:#ff0;font-weight:bold">0x140003670</span>         <span style="color:#007f7f">; DeriveAuthCodeFromHMS
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span><span style="color:#f00">140003616</span>  MOV  RCX,[RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x1750</span>]    <span style="color:#007f7f">; el PIN que tecleo el usuario
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span><span style="color:#f00">14000361</span>E  CALL strcmp
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#f00">140003623</span>  TEST EAX,EAX
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#f00">140003625</span>  JNZ  fail
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#f00">140003627</span>  MOV  AL,<span style="color:#ff0;font-weight:bold">0x1</span>              <span style="color:#f00">;</span> <span style="color:#f00">é</span>xito</span></span></code></pre></div>
<p>Y el global <code>g_szDerivedAuthCode</code> en <code>0x14003CEA8</code> es de solo escritura: sus únicas dos referencias de código están dentro de la propia función que lo escribe. Nadie más lo lee.</p>
<h3 id="64-qué-es-esto-en-realidad">6.4 Qué es esto en realidad</h3>
<p><strong>Es un TOTP de tres minutos con el reloj público como único secreto.</strong> El autor calcula el mismo valor en su página web y lo muestra como PIN descargable. No hay cuenta, no hay servidor, no hay estado.</p>
<p>Un PIN que se deriva de la hora no es un secreto, y una comprobación que vive exclusivamente en el cliente no es una comprobación. Cualquiera que tenga el <code>.exe</code> puede calcularlo. La página web <code>getpin-...php</code> es una tercera implementación del mismo cálculo, y que coincida con la del binario no añade protección: solo añade otra copia del mismo número público.</p>
<p><strong>Recomendacion práctica: no hay que pagar por esto.</strong></p>
<hr>
<h2 id="7-la-cadena-de-inyección">7. La cadena de inyección</h2>
<h3 id="71-abrir-el-objetivo">7.1 Abrir el objetivo</h3>
<p>La función <code>OpenTargetAndResolveGameGlobals</code> en <code>0x140006A00</code>:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>g_hTargetProcess = OpenProcess(<span style="color:#ff0;font-weight:bold">0x1FFFFF</span>, FALSE, pid);     <span style="color:#007f7f">// PROCESS_ALL_ACCESS
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>g_pNtQueryInformationProcess = GetProcAddress(GetModuleHandleA(<span style="color:#0ff;font-weight:bold">&#34;ntdll.dll&#34;</span>),
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span>                                               <span style="color:#0ff;font-weight:bold">&#34;NtQueryInformationProcess&#34;</span>);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span>g_pNtReadVirtualMemory = GetProcAddress(GetModuleHandleA(<span style="color:#0ff;font-weight:bold">&#34;ntdll.dll&#34;</span>),
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span>                                        <span style="color:#0ff;font-weight:bold">&#34;NtReadVirtualMemory&#34;</span>);
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#fff;font-weight:bold">uint64_t</span> base = GetRemoteModuleBase(<span style="color:#0ff;font-weight:bold">&#34;client.dll&#34;</span>);</span></span></code></pre></div>
<p>La función <code>GetRemoteModuleBase</code> camina la lista de módulos del proceso remoto:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>NtQueryInformationProcess(hProcess, <span style="color:#ff0;font-weight:bold">0</span>, pbi, <span style="color:#ff0;font-weight:bold">0x30</span>);      <span style="color:#007f7f">// ProcessBasicInformation -&gt; PEB
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>NtReadVirtualMemory(PEB,    PEB + <span style="color:#ff0;font-weight:bold">0x20</span>,  <span style="color:#ff0;font-weight:bold">0x2A0</span>);        <span style="color:#007f7f">// PEB_LDR_DATA
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span>NtReadVirtualMemory(Ldr,    Ldr + <span style="color:#ff0;font-weight:bold">0x20</span>,  <span style="color:#ff0;font-weight:bold">0x58</span>);         <span style="color:#007f7f">// InMemoryOrderModuleList
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span><span style="color:#fff;font-weight:bold">while</span> (NtReadVirtualMemory(entry, <span style="color:#ff0;font-weight:bold">0xE0</span>)) {              <span style="color:#007f7f">// LDR_DATA_TABLE_ENTRY
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>    comparar BaseDllName (UTF-<span style="color:#ff0;font-weight:bold">16</span>) con el nombre buscado
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span>    entry = entry-&gt;Flink;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">7</span><span>}</span></span></code></pre></div>
<p><code>OpenProcess</code> con <code>0x1FFFFF</code> es <code>PROCESS_ALL_ACCESS</code>, el permiso más amplio posible. Requiere privilegios de administrador para aplicarse a un proceso de Steam protegido.</p>
<h3 id="72-resolución-de-punteros-ocultos-en-el-juego">7.2 Resolución de punteros ocultos en el juego</h3>
<p>Una vez conocida la base del módulo, la función resuelve cuatro punteros globales del juego. El patrón es el de un puntero escondido como desplazamiento relativo de 32 bits:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span><span style="color:#007f7f">// FUN_140009920, rebautizada ResolveRelocatedPtr32
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>longlong ResolveRelocatedPtr32(longlong addr, <span style="color:#fff;font-weight:bold">int</span> a, <span style="color:#fff;font-weight:bold">int</span> b) {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span>    <span style="color:#fff;font-weight:bold">uint32_t</span> v;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span>    NtReadVirtualMemory(g_hTargetProcess, addr + a, &amp;v, <span style="color:#ff0;font-weight:bold">4</span>);   <span style="color:#007f7f">// lee un int32
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>    <span style="color:#fff;font-weight:bold">return</span> (<span style="color:#fff;font-weight:bold">uint64_t</span>)v + addr + b;                             <span style="color:#007f7f">// base + int32
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#007f7f"></span>}</span></span></code></pre></div>
<p>El layout en la sección de datos del juego es de siete bytes de cabecera seguidos del desplazamiento de 32 bits. Las cuatro llamadas usan los desplazamientos <code>0x21A14A</code>, <code>0x1C5990</code>, <code>0xC13E93</code> y <code>0xBA20EB</code> sobre la base de <code>client.dll</code>. De los cuatro, dos se desreferencian después con una segunda lectura de 8 bytes, porque apuntan a punteros dentro del juego.</p>
<p><strong>Esto no es escaneo de firmas.</strong> Es acceso directo a offsets fijos conocidos de la versión concreta de CS2. Por eso el manual del autor dice que hay que esperar una build nueva cuando el juego se actualiza.</p>
<h3 id="73-las-cinco-etapas-de-la-inyección">7.3 Las cinco etapas de la inyección</h3>
<p><strong>Etapa 1: reservar y arrancar el stub.</strong></p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>MOV qword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x20</span>],<span style="color:#ff0;font-weight:bold">0x40</span>     <span style="color:#007f7f">; PAGE_EXECUTE_READWRITE
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>MOV R9D,<span style="color:#ff0;font-weight:bold">0x3000</span>                    <span style="color:#007f7f">; MEM_COMMIT | MEM_RESERVE
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span>MOV R8D,<span style="color:#ff0;font-weight:bold">0x20000</span>                   <span style="color:#007f7f">; 131072 bytes
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>XOR EDX,EDX                       <span style="color:#007f7f">; dirección nula, que elija el kernel
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>MOV RCX,qword ptr [<span style="color:#ff0;font-weight:bold">0x14003CE70</span>]    <span style="color:#007f7f">; proceso objetivo
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#007f7f"></span>CALL qword ptr [<span style="color:#ff0;font-weight:bold">0x1400290A0</span>]      <span style="color:#f00">;</span> VirtualAllocEx</span></span></code></pre></div>
<p>Región de 128 KB con lectura, escritura y ejecución desde el momento de reservarla. Sin <code>VirtualProtect</code> posterior: la protección es RWX desde el principio.</p>
<p>Se escribe un struct de <code>0xC0</code> bytes con cinco cadenas descifradas y los punteros a <code>GetModuleHandleA</code> y <code>GetProcAddress</code>, seguidos de 221 bytes de stub:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>MOV qword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x20</span>],<span style="color:#ff0;font-weight:bold">0x0</span>      <span style="color:#007f7f">; lpThreadId = NULL
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>MOV dword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x28</span>],<span style="color:#ff0;font-weight:bold">0x0</span>      <span style="color:#007f7f">; dwCreationFlags = 0
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span>MOV RCX,qword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x48</span>]      <span style="color:#007f7f">; hProcess
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>MOV qword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x20</span>],RCX      <span style="color:#007f7f">; lpParameter      = base
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>MOV R9,RAX                        <span style="color:#007f7f">; lpStartAddress   = base
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#007f7f"></span>XOR R8D,R8D                       <span style="color:#007f7f">; dwStackSize      = 0
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">7</span><span><span style="color:#007f7f"></span>XOR EDX,EDX                       <span style="color:#007f7f">; lpThreadAttributes = NULL
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">8</span><span><span style="color:#007f7f"></span>CALL qword ptr [<span style="color:#ff0;font-weight:bold">0x1400290B8</span>]       <span style="color:#f00">;</span> CreateRemoteThread</span></span></code></pre></div>
<p><strong><code>lpStartAddress</code> y <code>lpParameter</code> son la misma dirección.</strong> Es el patrón clásico de shellcode: el código ejecutado recibe su propia dirección como parámetro y escribe allí un resultado. El loader lo recoge después:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span><span style="color:#f00">140003</span>B1E  CALL WaitForSingleObject   <span style="color:#007f7f">; INFINITE
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span><span style="color:#f00">140003</span>B2C  CALL CloseHandle
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#f00">140003</span>B52  CALL ReadProcessMemory     <span style="color:#f00">;</span> <span style="color:#ff0;font-weight:bold">0xC0</span> bytes de vuelta desde base</span></span></code></pre></div>
<p>Los cinco buffers del struct de <code>0xC0</code> son:</p>
<table>
<thead>
<tr>
<th>Offset</th>
<th>Long.</th>
<th>Contenido</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>+0x00</code></td>
<td>18</td>
<td>slot muerto, el stub no lo lee</td>
</tr>
<tr>
<td><code>+0x20</code></td>
<td>12</td>
<td><code>vgui2_s.dll</code></td>
</tr>
<tr>
<td><code>+0x40</code></td>
<td>16</td>
<td><code>CreateInterface</code></td>
</tr>
<tr>
<td><code>+0x60</code></td>
<td>14</td>
<td><code>VGUI_Setup001</code></td>
</tr>
<tr>
<td><code>+0x80</code></td>
<td>16</td>
<td><code>VGUI_Surface039</code></td>
</tr>
<tr>
<td><code>+0xA0</code></td>
<td>8</td>
<td>puntero a <code>GetModuleHandleA</code></td>
</tr>
<tr>
<td><code>+0xA8</code></td>
<td>8</td>
<td>puntero a <code>GetProcAddress</code></td>
</tr>
</tbody>
</table>
<p>Es decir: el loader <strong>no llama</strong> a <code>CreateInterface</code>. Le pasa al stub el módulo, el nombre del export y los dos nombres de interfaz, más los punteros a las dos funciones de la API que el stub necesita para resolverlo por su cuenta dentro del proceso objetivo.</p>
<p><strong>Etapa 2: segunda región y payload.</strong></p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>MOV qword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x20</span>],<span style="color:#ff0;font-weight:bold">0x40</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span>MOV R9D,<span style="color:#ff0;font-weight:bold">0x3000</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span>MOV R8D,<span style="color:#ff0;font-weight:bold">0x4000</span>                    <span style="color:#007f7f">; 16384 bytes
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>CALL VirtualAllocEx               <span style="color:#007f7f">; segunda región RWX
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>CALL ReadProcessMemory            <span style="color:#007f7f">; lee un puntero desde [resultado+0xB0]
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#007f7f"></span>CALL operator new                 <span style="color:#007f7f">; 0x800
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">7</span><span><span style="color:#007f7f"></span>CALL ReadProcessMemory            <span style="color:#f00">;</span> <span style="color:#ff0;font-weight:bold">2048</span> bytes desde ese puntero</span></span></code></pre></div>
<p><strong>Etapa 3: cabecera de configuración.</strong> Se construye un bloque de <code>0x4620</code> bytes y se escribe entero en el proceso remoto. Contiene la tabla de diez features, la tabla de APIs resueltas, la ruta del registro en UTF-16, dos vectores de 256 bytes de estado de teclas, las tablas de firmas con máscara y los cuatro punteros del juego.</p>
<p><strong>Etapa 4: payload principal.</strong> Se parchea un centinela para auto-relocación y se escriben 17.108 bytes:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>MOV  DL,<span style="color:#ff0;font-weight:bold">0x11</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span>LEA  RCX,[<span style="color:#ff0;font-weight:bold">0x1400360E0</span>]            <span style="color:#007f7f">; blob del payload en .data
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span>CALL FUN_140009730                <span style="color:#007f7f">; strchr(blob, 0x11)
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>MOV  RCX,qword ptr [RSP<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x48</span>]     <span style="color:#007f7f">; base de la región
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>MOV  qword ptr [RAX],RCX          <span style="color:#007f7f">; *(centinela) = base
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#007f7f"></span>CALL WriteProcessMemory           <span style="color:#f00">;</span> <span style="color:#ff0;font-weight:bold">0x42D4</span> bytes a base<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">0x4620</span></span></span></code></pre></div>
<p><strong>Etapa 5: activación y parche en caliente.</strong></p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>CALL WriteProcessMemory           <span style="color:#007f7f">; 8 bytes: &amp;base2 dentro de cs2.exe
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>CALL ScanRemoteMemoryForString     <span style="color:#007f7f">; busca la firma
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#007f7f"></span>ADD  RAX,<span style="color:#ff0;font-weight:bold">2</span>                        <span style="color:#007f7f">; el match + 2
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>CALL VirtualProtectEx             <span style="color:#007f7f">; 1 byte, PAGE_EXECUTE_READWRITE
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"></span>CALL WriteProcessMemory            <span style="color:#007f7f">; escribe un 0x00
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#007f7f"></span>CALL VirtualProtectEx             <span style="color:#f00">;</span> restaura la protección original</span></span></code></pre></div>
<p>El patrón buscado es una secuencia real de x86-64 con comodines:</p>






<pre tabindex="0"><code>41 BF ?? ?? ?? FF 15 ?? ?? ?? ?? 48 8B 53 20 48 3B C2 73 0A
48 8B C8 48 2B CA 48 01 4B 30</code></pre>
<p>Desensamblada, es <code>mov edi, edi</code> seguido de una llamada indirecta, una comparación y una suma a un campo de una estructura: una función de VGUI. El parche inyecta un <code>0x00</code> en el offset más 2, es decir un <strong>hook en línea</strong> de código del juego.</p>
<p>La función devuelve códigos de error numerados: 0 si todo va bien, 1 si no se encuentra el destino, 2 si falla <code>OpenProcess</code>, 3 si falla <code>VirtualAllocEx</code> y 4 si falla <code>CreateRemoteThread</code>.</p>
<hr>
<h2 id="8-el-payload-análisis-de-capacidades">8. El payload: análisis de capacidades</h2>
<h3 id="81-qué-contiene">8.1 Qué contiene</h3>
<p>La sección <code>.data</code> contiene cuatro blobs y nada más:</p>
<table>
<thead>
<tr>
<th>Blob</th>
<th>VA</th>
<th>Tamaño</th>
<th>Entropía</th>
<th>Contenido</th>
</tr>
</thead>
<tbody>
<tr>
<td>stub de etapa 1</td>
<td><code>0x140036000</code></td>
<td>221 B</td>
<td>4.14</td>
<td>bootstrap de 221 bytes</td>
</tr>
<tr>
<td>payload principal</td>
<td><code>0x1400360E0</code></td>
<td>17.108 B</td>
<td>5.01</td>
<td>código del cheat</td>
</tr>
<tr>
<td>bloque de config A</td>
<td><code>0x14003A3C0</code></td>
<td>1.097 B</td>
<td>4.59</td>
<td>biblioteca de vectores SIMD</td>
</tr>
<tr>
<td>bloque de config B</td>
<td><code>0x14003A810</code></td>
<td>6.800 B</td>
<td>4.97</td>
<td>tabla de datos</td>
</tr>
</tbody>
</table>
<p>Total 25.226 bytes. Una entropía entre 4 y 5 descarta cifrado, que daría entre 7.9 y 8.0, y descarta compresion, que daría entre 7.0 y 7.9. <strong>Ninguno de los cuatro blobs está protegido.</strong></p>
<p>El bloque de config A se desensambla como una biblioteca de <code>Vector3</code> y <code>Vector4</code> en instrucciones SIMD:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>movups xmm0, [rcx<span style="color:#f00">+</span>rsi]           <span style="color:#007f7f">; carga 16 bytes
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#007f7f"></span>movups xmm1, [rcx<span style="color:#f00">+</span>rsi<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">10</span>h]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span>addps  xmm0, xmm1                <span style="color:#007f7f">; suma de vectores
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"></span>movups [rcx<span style="color:#f00">+</span>rdi], xmm0
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f">...</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span>divss  xmm0, xmm2                <span style="color:#007f7f">; escalar dividido por escalar
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">7</span><span><span style="color:#007f7f"></span>subss  xmm0, xmm1
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">8</span><span>cvtsi2ss xmm1, [rax<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">8</span>Ch]         <span style="color:#007f7f">; int a float
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">9</span><span><span style="color:#007f7f"></span>comiss xmm0, [rax<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">80</span>h]           <span style="color:#f00">;</span> comparación</span></span></code></pre></div>
<p>Con los desplazamientos de campo <code>0x54</code>, <code>0x6C</code>, <code>0x7C</code>, <code>0x80</code>, <code>0x88</code>, <code>0x8C</code> y <code>0x90</code> sobre una estructura, y con pasos de 16 y 4 bytes. Es exactamente la aritmética que necesita un aimbot: proyectar de mundo a pantalla, calcular ángulos, normalizar vectores y medir distancias. <strong>Cero E/S, cero red, cero registro, cero criptografía.</strong></p>
<h3 id="82-el-método-de-análisis">8.2 El método de análisis</h3>
<p>El payload se autorreferencia con un placeholder:</p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span><span style="color:#f00">1400360</span>E0  <span style="color:#ff0;font-weight:bold">44</span> <span style="color:#ff0;font-weight:bold">88</span> <span style="color:#ff0;font-weight:bold">44</span> <span style="color:#ff0;font-weight:bold">24</span> <span style="color:#ff0;font-weight:bold">18</span>           mov  [rsp<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">18</span>h], r8d
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span><span style="color:#f00">1400360</span>E5  <span style="color:#ff0;font-weight:bold">48</span> <span style="color:#ff0;font-weight:bold">89</span> <span style="color:#ff0;font-weight:bold">54</span> <span style="color:#ff0;font-weight:bold">24</span> <span style="color:#ff0;font-weight:bold">10</span>           mov  [rsp<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">10</span>h], rdx
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span><span style="color:#f00">1400360</span>EA  <span style="color:#ff0;font-weight:bold">48</span> <span style="color:#ff0;font-weight:bold">89</span> <span style="color:#ff0;font-weight:bold">4</span>C <span style="color:#ff0;font-weight:bold">24</span> <span style="color:#ff0;font-weight:bold">08</span>           mov  [rsp<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">08</span>h], rcx
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#f00">1400360</span>EF  <span style="color:#ff0;font-weight:bold">48</span> <span style="color:#ff0;font-weight:bold">81</span> EC <span style="color:#ff0;font-weight:bold">78</span> <span style="color:#ff0;font-weight:bold">04</span> <span style="color:#ff0;font-weight:bold">00</span> <span style="color:#ff0;font-weight:bold">00</span>     sub  rsp, <span style="color:#ff0;font-weight:bold">478</span>h
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#f00">1400360</span>F6  <span style="color:#ff0;font-weight:bold">48</span> B8 <span style="color:#ff0;font-weight:bold">11</span> <span style="color:#ff0;font-weight:bold">11</span> <span style="color:#ff0;font-weight:bold">11</span> <span style="color:#ff0;font-weight:bold">11</span> <span style="color:#ff0;font-weight:bold">11</span> <span style="color:#ff0;font-weight:bold">11</span> <span style="color:#ff0;font-weight:bold">11</span> <span style="color:#ff0;font-weight:bold">11</span>   mov rax, <span style="color:#ff0;font-weight:bold">1111111111111111</span>h
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">6</span><span><span style="color:#f00">1400360</span>FF  <span style="color:#ff0;font-weight:bold">48</span> <span style="color:#ff0;font-weight:bold">89</span> <span style="color:#ff0;font-weight:bold">44</span> <span style="color:#ff0;font-weight:bold">24</span> <span style="color:#ff0;font-weight:bold">40</span>           mov  [rsp<span style="color:#f00">+</span><span style="color:#ff0;font-weight:bold">40</span>h], rax</span></span></code></pre></div>
<p>El inmediato <code>0x1111111111111111</code> está en el offset 24 del blob, y es exactamente lo que el loader localiza con <code>strchr(blob, 0x11)</code> y sustituye por la dirección base. Ese registro contiene <strong>la base de la cabecera de <code>0x4620</code> bytes</strong>, no el inicio del código.</p>
<p>De ahí se deriva el método: cada operando de memoria con desplazamiento de 32 bits relativo a ese registro es un acceso a la cabecera, y <strong>la cabecera es el conjunto de capacidades</strong>. No hay que interpretar nada: es aritmética sobre offsets.</p>
<h3 id="83-resultados">8.3 Resultados</h3>
<p>Un barrido lineal de los 17.108 bytes arroja lo siguiente:</p>
<ul>
<li><strong>236 offsets distintos de la cabecera, 1052 accesos.</strong> Todos dentro del rango <code>0x0000</code> a <code>0x461F</code>.</li>
<li><strong>Cero llamadas <code>call</code> ni <code>jmp</code> indirectas con desplazamiento relativo al IP de instrucción.</strong> No hay IAT propia, ni vtables propias, ni thunks.</li>
<li><strong>Cero referencias a datos con desplazamiento relativo al IP de instrucción.</strong> No hay constantes propias.</li>
<li><strong>Un único <code>MOVABS</code> de 64 bits en 17 KB:</strong> el placeholder <code>0x1111111111111111</code>.</li>
</ul>
<p>Los dos últimos puntos son los determinantes. Si el payload construyera nombres de API o tablas de hash en tiempo de ejecución, necesitaría cargar inmediatos de 64 bits, y no hay ninguno. Si tuviera datos propios, necesitaría referencias relativas al IP de instrucción, y no hay ninguna.</p>
<p>Repitiendo el análisis sobre los otros tres blobs:</p>
<table>
<thead>
<tr>
<th>Blob</th>
<th>Accesos a cabecera</th>
<th>Llamadas indirectas</th>
<th>MOVABS</th>
<th>Datos RIP</th>
</tr>
</thead>
<tbody>
<tr>
<td>stub de etapa 1</td>
<td>5</td>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>payload principal</td>
<td>1052</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>config A</td>
<td>9</td>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>config B</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>El bloque de config B con cero accesos a memoria con desplazamiento de 32 bits confirma que es dato puro y no código.</p>
<h3 id="84-indicadores">8.4 Indicadores</h3>
<p>Un barrido adicional sobre los 25.226 bytes, buscando indicadores de compromiso, dio:</p>
<ul>
<li><strong>Cero cadenas legibles en ASCII.</strong> Las 55 coincidencias iniciales resultaron ser fragmentos de instrucción: por ejemplo la secuencia de bytes <code>44 24 50 48 63 44 24 54 48</code>, que es el final de un <code>mov [rsp+50h], rax</code> seguido de <code>movsxd rax, [rsp+54h]</code>. Todos los prefijos REX.W producen bytes imprimibles.</li>
<li><strong>Cero cadenas en UTF-16.</strong></li>
<li><strong>Cero resoluciones de API por hash ROR13</strong>, calculado sobre una lista de alrededor de 120 nombres de API de proceso, fichero, red, registro, criptografía, teclado y anti-depuración. Ese es el método estándar de shellcode y reflective loader.</li>
<li><strong>Cero constantes de MD5, SHA-1, SHA-256, ChaCha20 ni TEA.</strong></li>
<li><strong>Cero direcciones IP en claro y cero puertos sospechosos.</strong></li>
</ul>
<p><strong>En 25 KB de payload no hay ni una sola cadena de texto legible.</strong> Para malware eso es extraordinario: un stealer, un dropper o un ransomware siempre deja algo, una URL, una ruta, un nombre de mutex o un nombre de API.</p>
<h3 id="85-el-mapa-de-la-cabecera">8.5 El mapa de la cabecera</h3>
<table>
<thead>
<tr>
<th>Offset</th>
<th>Contenido</th>
<th>Accesos</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>0x0000</code> - <code>0x01B8</code></td>
<td>tabla de diez features, cada una de <code>0x2C</code> bytes</td>
<td>varios</td>
</tr>
<tr>
<td><code>0x01D0</code> - <code>0x0200</code></td>
<td>siete punteros a función del juego, extraidos de una vtable</td>
<td>66</td>
</tr>
<tr>
<td><code>0x01EA8</code></td>
<td>puntero a la interfaz del motor</td>
<td><strong>78</strong></td>
</tr>
<tr>
<td><code>0x0208</code> - <code>0x03F8</code></td>
<td>región leída densamente, probable tabla de offsets del juego</td>
<td>~450</td>
</tr>
<tr>
<td><code>0x00A0</code></td>
<td>puntero a <code>GetModuleHandleA</code></td>
<td>3</td>
</tr>
<tr>
<td><code>0x00A8</code></td>
<td>puntero a <code>GetProcAddress</code></td>
<td>2</td>
</tr>
<tr>
<td><code>0x2020</code></td>
<td><code>OpenProcess</code></td>
<td>1</td>
</tr>
<tr>
<td><code>0x2028</code></td>
<td><code>GetAsyncKeyState</code></td>
<td>2</td>
</tr>
<tr>
<td><code>0x2030</code></td>
<td><code>SendInput</code></td>
<td>3</td>
</tr>
<tr>
<td><code>0x2038</code></td>
<td><code>GetTickCount64</code></td>
<td>2</td>
</tr>
<tr>
<td><code>0x2040</code></td>
<td><code>NtReadVirtualMemory</code></td>
<td>25</td>
</tr>
<tr>
<td><code>0x2048</code></td>
<td><code>RegSetValueExW</code></td>
<td>1</td>
</tr>
<tr>
<td><code>0x2050</code></td>
<td><code>RegOpenKeyExW</code></td>
<td>1</td>
</tr>
<tr>
<td><code>0x2058</code></td>
<td><code>RegCloseKey</code></td>
<td>1</td>
</tr>
<tr>
<td><code>0x2060</code></td>
<td><code>L&quot;Software\udtk&quot;</code> en UTF-16</td>
<td>2</td>
</tr>
<tr>
<td><code>0x20BC</code></td>
<td><code>BYTE[256]</code>, flanco de bajada por código de tecla</td>
<td>5</td>
</tr>
<tr>
<td><code>0x21BC</code></td>
<td><code>BYTE[256]</code>, estado actual por código de tecla</td>
<td>3</td>
</tr>
<tr>
<td><code>0x22C0</code></td>
<td>bandera de &ldquo;ya inicializado&rdquo;</td>
<td>26</td>
</tr>
<tr>
<td><code>0x22C8</code> - <code>0x22E0</code></td>
<td>los cuatro punteros del juego</td>
<td>5</td>
</tr>
<tr>
<td><code>0x2388</code> - <code>0x23B4</code></td>
<td>cluster denso, probable tabla de firmas</td>
<td>~100</td>
</tr>
<tr>
<td><code>0x3E50</code></td>
<td>último offset tocado</td>
<td>1</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="9-la-demostración-de-contención">9. La demostración de contención</h2>
<p>Esta es la sección central del informe.</p>
<h3 id="91-las-únicas-apis-resueltas-en-tiempo-de-ejecución">9.1 Las únicas APIs resueltas en tiempo de ejecución</h3>
<p>El binario hace <code>GetProcAddress</code> exactamente seis veces desde código de aplicación:</p>
<table>
<thead>
<tr>
<th>Dirección</th>
<th>Export resuelto</th>
<th>Módulo</th>
<th>Propósito</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>0x140006ADD</code></td>
<td><code>NtQueryInformationProcess</code></td>
<td><code>ntdll.dll</code></td>
<td>obtener el PEB del proceso remoto</td>
</tr>
<tr>
<td><code>0x140006B59</code></td>
<td><code>NtReadVirtualMemory</code></td>
<td><code>ntdll.dll</code></td>
<td>leer memoria del proceso remoto</td>
</tr>
<tr>
<td><code>0x140006467</code></td>
<td><code>_itow</code></td>
<td><code>ntdll.dll</code></td>
<td>conversión de entero a cadena</td>
</tr>
<tr>
<td><code>0x1400064D2</code></td>
<td><code>sqrt</code></td>
<td><code>ntdll.dll</code></td>
<td>matemáticas del aimbot</td>
</tr>
<tr>
<td><code>0x140006579</code></td>
<td><code>pow</code></td>
<td><code>ntdll.dll</code></td>
<td>matemáticas del aimbot</td>
</tr>
<tr>
<td><code>0x140006602</code></td>
<td><code>fabs</code></td>
<td><code>ntdll.dll</code></td>
<td>matemáticas del aimbot</td>
</tr>
</tbody>
</table>
<p>Las tres últimas resuelven funciones matemáticas del CRT que <code>ntdll.dll</code> reexporta por compatibilidad. Es un detalle elegante: en vez de enlazar una biblioteca de runtime, el payload roba <code>sqrt</code>, <code>pow</code> y <code>fabs</code> de ntdll.</p>
<p>Las otras tres referencias a <code>GetProcAddress</code> del binario están en el rango del UCRT y son sus envoltorios de carga diferida.</p>
<p><strong>No se resuelve ninguna API de red, de fichero, de proceso, de hilo ni de criptografía en tiempo de ejecución.</strong> Ninguna.</p>
<h3 id="92-el-conjunto-de-capacidades">9.2 El conjunto de capacidades</h3>
<p>Combinando el mapa de la cabecera con lo anterior, el payload tiene acceso a doce punteros a función:</p>
<table>
<thead>
<tr>
<th>API disponible</th>
<th>Qué puede hacer</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>OpenProcess</code></td>
<td>abrir otros procesos</td>
</tr>
<tr>
<td><code>GetAsyncKeyState</code></td>
<td>consultar el estado de una tecla</td>
</tr>
<tr>
<td><code>SendInput</code></td>
<td>inyectar entrada sintética</td>
</tr>
<tr>
<td><code>GetTickCount64</code></td>
<td>reloj de alta resolución</td>
</tr>
<tr>
<td><code>NtReadVirtualMemory</code></td>
<td>leer memoria de otros procesos</td>
</tr>
<tr>
<td><code>RegSetValueExW</code></td>
<td>escribir en el registro</td>
</tr>
<tr>
<td><code>RegOpenKeyExW</code></td>
<td>abrir claves del registro</td>
</tr>
<tr>
<td><code>RegCloseKey</code></td>
<td>cerrar claves del registro</td>
</tr>
<tr>
<td><code>GetModuleHandleA</code></td>
<td>localizar módulos cargados</td>
</tr>
<tr>
<td><code>GetProcAddress</code></td>
<td>resolver exportaciones</td>
</tr>
<tr>
<td>interfaz del motor</td>
<td>siete funciones del juego</td>
</tr>
<tr>
<td>cuatro punteros del juego</td>
<td>estructuras internas de CS2</td>
</tr>
</tbody>
</table>
<p><strong>Lo que no está en la lista:</strong></p>
<ul>
<li>Ninguna función de fichero. <strong>No puede escribir en disco.</strong></li>
<li>Ninguna función de red. <strong>No puede enviar datos a ninguna parte.</strong></li>
<li>Ninguna función de creación de procesos o hilos.</li>
<li>Ninguna función criptográfica.</li>
<li>Ninguna función de portapapeles, captura de pantalla ni inyección de eventos.</li>
</ul>
<p><strong>El payload no tiene capacidad de exfiltración.</strong> No tiene ni un solo socket disponible. No tiene forma de construir un mensaje de red porque no tiene la primitiva para hacerlo.</p>
<hr>
<h2 id="10-por-qué-esto-es-un-cheat-y-no-malware">10. Por qué esto es un cheat y no malware</h2>
<h3 id="101-evidencia-directa">10.1 Evidencia directa</h3>
<p><strong>Las cadenas del menú.</strong> El binario contiene <code>Aimbot</code>, <code>Smooth</code>, <code>Hitbox</code>, <code>Trigger</code>, <code>Spotted</code>, <code>Esp</code>, <code>Theme</code>, <code>Aim Spot</code>, <code>Key</code>, <code>On</code>, <code>Off</code>, y la fuente <code>Tahoma</code>. Son exactamente los nombres de las opciones de un menú de cheat.</p>
<p><strong>Las tablas de configuración.</strong> La tabla de diez entradas con paso <code>0x2C</code> contiene los valores por defecto de cada feature:</p>
<table>
<thead>
<tr>
<th>Entrada</th>
<th>Nombre</th>
<th>Campo 1</th>
<th>Campo 2</th>
<th>Campo 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>(ilegible)</td>
<td>0</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td>1</td>
<td><code>Aimbot</code></td>
<td>2</td>
<td>5</td>
<td></td>
</tr>
<tr>
<td>2</td>
<td><code>Smooth</code></td>
<td>1</td>
<td>30</td>
<td>100</td>
</tr>
<tr>
<td>3</td>
<td><code>Hitbox</code></td>
<td>1</td>
<td>1</td>
<td>4</td>
</tr>
<tr>
<td>4</td>
<td><code>Key</code></td>
<td>0</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td>5</td>
<td><code>Trigger</code></td>
<td>2</td>
<td>6</td>
<td></td>
</tr>
<tr>
<td>6</td>
<td><code>Aim Spot</code></td>
<td>0</td>
<td>0</td>
<td></td>
</tr>
<tr>
<td>7</td>
<td><code>Esp</code></td>
<td>0</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td>8</td>
<td>(ilegible)</td>
<td>0</td>
<td>0</td>
<td></td>
</tr>
<tr>
<td>9</td>
<td><code>Theme</code></td>
<td>1</td>
<td>1</td>
<td>30</td>
</tr>
</tbody>
</table>
<p>Los valores son coherentes con parámetros reales de un aimbot: <code>Smooth</code> con 30 sobre un máximo de 100, <code>Hitbox</code> con escala 4, <code>Theme</code> con índice.</p>
<p><strong>El manual del autor.</strong> <code>Aimbot Guide.txt</code> describe exactamente estas opciones: &ldquo;Low smooth = fast aimbot, high smooth = slow aimbot&rdquo;, y &ldquo;Spotted es la verificación de visibilidad de los pobres, el ESP cambiará de color cuando el enemigo sea visible&rdquo;. El binario contiene <code>Spotted</code> y <code>Esp</code> como features separados. La correspondencia es directa.</p>
<p><strong>La cadena del motor Source.</strong> <code>vgui2_s.dll</code>, <code>CreateInterface</code>, <code>VGUI_Setup001</code> y <code>VGUI_Surface039</code> son el sistema de interfaces gráficas del motor de Valve. El cheat se engancha ahí.</p>
<p><strong>La firma de hook.</strong> Los bytes buscados en memoria descomponen una función de VGUI con una comparación y una suma a un campo de estructura. El parche de un byte la desvía.</p>
<p><strong>El disparador de input.</strong> <code>GetAsyncKeyState</code> y <code>SendInput</code> entregados al código que corre dentro del juego, más <code>GetTickCount64</code> como reloj, son exactamente el juego de herramientas de un triggerbot: consultar si una tecla está pulsada, generar el clic en el instante preciso y medir el intervalo. Que el <code>SendInput</code> se invoque desde el proceso del juego hace que el input pase el filtro del juego como si fuera interno.</p>
<p><strong>El bucle de teclas.</strong> El payload recorre los 256 códigos de tecla virtual, llama a <code>GetAsyncKeyState</code> para cada uno y detecta flancos de bajada, guardando el resultado en dos vectores de 256 bytes en la cabecera. Ese es el fundamento de la detección de &ldquo;keybind mantenido&rdquo;.</p>
<p><strong>El texto del propio binario.</strong> <code>&quot;Injected, press INSERT to open the menú&quot;</code> y <code>&quot;Injecting FREE cheat...&quot;</code> no dejan lugar a duda.</p>
<h3 id="102-ausencia-de-comportamiento-malicioso">10.2 Ausencia de comportamiento malicioso</h3>
<p><strong>No toca el sistema de ficheros.</strong> <code>CreateFileW</code>, <code>ReadFile</code> y <code>WriteFile</code> solo se llaman desde el UCRT, en el código de entrada y salida de consola. No hay <code>DeleteFile</code>, ni <code>MoveFileEx</code>, ni <code>CopyFile</code>, ni <code>CreateProcess</code>, ni escritura en disco de ningun tipo.</p>
<p><strong>No exfiltra.</strong> La única conexión saliente es una consulta a una API pública de hora, con una petición literal de 138 bytes que no contiene datos de la máquina.</p>
<p><strong>No roba credenciales.</strong> No hay rutas de navegadores, ni acceso a LSASS, ni <code>CryptUnprotectData</code>, ni portapapeles, ni captura de pantalla.</p>
<p><strong>No escala privilegios.</strong> No hay manipulación de tokens, ni creación de servicios, ni carga de drivers, ni bypass de UAC. Pide administrador únicamente porque <code>OpenProcess</code> con todos los permisos sobre un proceso de Steam lo requiere.</p>
<p><strong>No persiste como ejecutable.</strong> Su único uso del registro es una clave de configuración, y usa <code>RegDeleteKeyA</code> para <strong>invalidar</strong> cuando el tipo o el tamaño no cuadran, no para instalar. No hay claves de autoarranque, ni tareas programadas, ni servicios.</p>
<p><strong>Endurecido, no debilitado.</strong> El binario tiene cookies de pila y tabla de manejadores de excepciones seguros:</p>






<pre tabindex="0"><code>SecurityCookie  0x000000014003C2C0
SEHandlerTable   0x00000001400293A0</code></pre>
<p>El valor <code>0x14003C2C0</code> es la misma constante de comprobación de pila que aparece en el epílogo de todas las funciones. El malware rara vez se compila con estas protecciones porque estorban.</p>
<hr>
<h2 id="11-técnicas-de-anti-análisis">11. Técnicas de anti-análisis</h2>
<p>Cinco, y ninguna define por sí sola la malicia:</p>
<ol>
<li><strong>XOR de un byte en todas las cadenas</strong>, descifrado en la pila, de modo que no existe texto en claro en el fichero ni en un volcado de memoria.</li>
<li><strong>Winsock importado por ordinal</strong>, eliminando las cadenas <code>socket</code>, <code>connect</code>, <code>send</code> y <code>recv</code> del binario.</li>
<li><strong>Tablas de firmas construidas byte a byte.</strong> El código escribe cada byte de los patrones con instrucciones <code>MOV</code> individuales, de forma que nunca existe un bloque contiguo de datos que un escáner pueda firmar como patrón.</li>
<li><strong>Tabla de resolución indirecta de punteros</strong>, con el desplazamiento de 32 bits escondido dentro del juego en vez de una tabla legible en el loader.</li>
<li><strong>Compilacion con optimización guiada por perfil.</strong> El directorio de depuración registra <code>IMAGE_DEBUG_TYPE_POGO</code>, no <code>CODEVIEW</code>. Es decir, el binario fue construido con PGO, que produce código notablemente mejor optimizado y por tanto más difícil de invertir, y <strong>la ruta del PDB fue eliminada</strong>, de modo que no hay atribución por entorno de compilacion.</li>
</ol>
<hr>
<h2 id="12-auditoría-de-la-estructura-pe">12. Auditoría de la estructura PE</h2>
<table>
<thead>
<tr>
<th>Sección</th>
<th>Flags</th>
<th>Permisos</th>
<th>Estándar MSVC</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>.text</code></td>
<td><code>0x60000020</code></td>
<td>código, ejecución, lectura</td>
<td>idéntico</td>
</tr>
<tr>
<td><code>.rdata</code></td>
<td><code>0x40000040</code></td>
<td>datos, lectura</td>
<td>idéntico</td>
</tr>
<tr>
<td><code>.data</code></td>
<td><code>0xC0000040</code></td>
<td>datos, lectura, escritura</td>
<td>idéntico</td>
</tr>
<tr>
<td><code>.pdata</code></td>
<td><code>0x40000040</code></td>
<td>datos, lectura</td>
<td>idéntico</td>
</tr>
<tr>
<td><code>.reloc</code></td>
<td><code>0x42000040</code></td>
<td>datos, descartable, lectura</td>
<td>normal</td>
</tr>
</tbody>
</table>
<p><strong>Ninguna sección con escritura y ejecución simultáneas.</strong></p>
<table>
<thead>
<tr>
<th>Directorio</th>
<th>Valor</th>
<th>Nota</th>
</tr>
</thead>
<tbody>
<tr>
<td>TLS</td>
<td><code>0x0</code></td>
<td><strong>vacío, sin callbacks</strong></td>
</tr>
<tr>
<td>Overlay</td>
<td>ninguno</td>
<td>el fichero acaba donde acaba la última sección</td>
</tr>
<tr>
<td>Export</td>
<td><code>0x0</code></td>
<td></td>
</tr>
<tr>
<td>Resource</td>
<td><code>0x0</code></td>
<td>sin recursos incrustados</td>
</tr>
<tr>
<td>BoundImport</td>
<td><code>0x0</code></td>
<td></td>
</tr>
<tr>
<td>DelayImport</td>
<td><code>0x0</code></td>
<td></td>
</tr>
<tr>
<td>CLR</td>
<td><code>0x0</code></td>
<td>no es .NET</td>
</tr>
</tbody>
</table>
<p><strong>No hay callbacks TLS</strong>, que son la vía estándar de ejecución de código antes de <code>main</code> y la forma más común de anti-depuración persistente. <strong>No hay overlay</strong>, lo que descarta de raíz la técnica de dropper con carga embebida al final del fichero. Las protecciones de imagen activas son ASLR, DEP y <code>TERMINAL_SERVER_AWARE</code>, sin <code>NO_SEH</code> y sin CFG.</p>
<h3 id="121-sobre-el-patrón-del-bloque-de-config-b">12.1 Sobre el patrón del bloque de config B</h3>
<p>El bloque de 6.800 bytes presenta 809 apariciones del valor de 4 bytes <code>0x00FFFFFF</code>. El análisis de la distribución muestra que <strong>no es un valor repetido</strong>, sino la secuencia de bytes <code>FF FF FF 00</code> repetida:</p>






<pre tabindex="0"><code>bytes 0xFF:  2555  (37.6 por ciento)
bytes 0x00:   994  (14.6 por ciento)
rachas consecutivas de 0xFF:  937
   de las cuales 809 de longitud 3, y 128 de longitud 1</code></pre>
<p>Es una <strong>tabla dispersa</strong>: 809 entradas con el marcador de vacío <code>FF FF FF 00</code> y 128 entradas pobladas con datos reales. Encaja exacto con las posiciones: el bloque se copia a la cabecera en el offset <code>0x23C0</code>, y <code>0x23C0 + 0x1A90 = 0x3E50</code>, que es el último offset que toca el payload.</p>
<p>Una tabla de 128 entradas pobladas con 809 huecos vacíos es exactamente lo que se espera de una tabla de offsets de entidades y huesos de un motor de juego. Un malware que prepara terreno no deja un 52 por ciento de marcadores de vacío.</p>
<hr>
<h2 id="13-veredicto">13. Veredicto</h2>
<p><strong><code>undetek.exe</code> v10.51 no es malware.</strong></p>
<p>La conclusión se sostiene sobre tres pilares, todos verificables:</p>
<ol>
<li>
<p><strong>Contención de capacidades.</strong> El payload no tiene IAT propia, ni constantes propias, ni resuelve APIs por hash, y todas sus capacidades se reducen a doce punteros que el loader le entrega. No tiene ninguna primitiva de red, de fichero ni de proceso. <strong>No puede exfiltrar.</strong></p>
</li>
<li>
<p><strong>Ausencia de comportamiento malicioso en el loader.</strong> Sin escritura en disco, sin exfiltración, sin robo de credenciales, sin escalada de privilegios, sin persistencia como ejecutable, y con protecciones de pila y SEH activas.</p>
</li>
<li>
<p><strong>Evidencia positiva de cheat.</strong> Cadenas del menú, tabla de diez features con valores por defecto coherentes, resolución de interfaces del motor de Valve, hook en línea sobre una función de VGUI, disparador de input dentro del proceso del juego, y un mensaje de éxito que dice literalmente que se ha inyectado un cheat.</p>
</li>
</ol>
<h3 id="131-lo-que-sí-es-un-riesgo">13.1 Lo que sí es un riesgo</h3>
<p><strong>El riesgo mayor no es este binario.</strong> Es la guía de instalación que lo acompaña:</p>






<pre tabindex="0"><code>1. Turn off windows defender
2. Turn off windows defender firewall
3. Turn off smartscreen (app and browser control)
4. Turn UAC to a low setting.
5. Turn off Valorant Anticheat.
6. Turn off FACEIT Anticheat.</code></pre>
<p>Desactivar el antivirus, el firewall, SmartScreen, el control de cuentas de usuario y los anticheats de terceros para ejecutar un ejecutable de un vendedor anónimo <strong>es el vector de entrada real del malware para jugadores</strong>, mucho más que cualquier bytecode concreto. Ese es el canal por el que se distribuye malware para esta plataforma, y aquí llega con instrucciones paso a paso.</p>
<p><strong>El PIN no es una licencia</strong> y no debe pagarse por el. Es una función del reloj público, sin secreto de servidor, con 480 valores posibles por día y la misma respuesta para todos los usuarios a la vez.</p>
<p><strong>Legal.</strong> Inyectar código en Counter-Strike 2 viola las condiciones de servicio de Valve. El riesgo de bloqueo permanente de VAC es del usuario, no del autor del loader.</p>
<h3 id="132-limitaciones-del-método">13.2 Limitaciones del método</h3>
<p>Conviene ser explícito sobre lo que este análisis <strong>no</strong> demuestra:</p>
<ul>
<li>
<p><strong>El análisis de capacidades es un barrido lineal de operandos de memoria, no un desensamblador.</strong> Puede perder sincronización, y se ha observado que lo hace: los accesos &ldquo;fuera de la cabecera&rdquo; que produce en una de las etapas son valores que contienen bytes de opcode de x86, es decir artefactos del parser. Es evidencia fuerte, no prueba formal.</p>
</li>
<li>
<p><strong>La lógica del aimbot no se ha reconstruido.</strong> Se han identificado las variables de estado de teclas, la resolución de interfaces y la biblioteca de vectores, pero no la matemática concreta de predicción, campo de visión o compensación de retroceso.</p>
</li>
<li>
<p><strong>El payload tiene <code>GetModuleHandleA</code> y <code>GetProcAddress</code> disponibles.</strong> Podría resolver cualquier API en tiempo de ejecución si quisiera. Para hacerlo necesitaría nombres, y no hay cadenas, ni hashes, ni inmediatos de 64 bits en los 17 KB. Lo único que no puede descartarse sin desensamblado completo es que construyera un nombre byte a byte en la pila con inmediatos de 8 bits, que el barrido no vería. Es la misma técnica que usa el propio loader con sus 51 descifradores, así que no es descartable de forma teórica.</p>
</li>
<li>
<p><strong>Dos cadenas de texto se identificaron por longitud y no por lectura directa</strong>, por lo que su contenido literal puede diferir en un carácter.</p>
</li>
</ul>
<hr>
<h2 id="14-indicadores-de-compromiso">14. Indicadores de compromiso</h2>






<pre tabindex="0"><code>MD5     47cdf077d83b9e8c8b80364d2dbd9527
SHA-1   77468f710e2d45fcbab758a6beb8590e767d79be
SHA-256 82eb05556ffe1597e40353a0faef69e78f072d14e671adbf360c2161c06a8276

Red observada (única):
  vip.timezonedb.com:80        API pública de zona horaria, GET HTTP
  clave de la API: &lt;redactada&gt;
  ruta: /v2.1/get-time-zone

Registro:
  HKEY_CURRENT_USER\Software\udtk
  valor por defecto (sin nombre), REG_BINARY, 440 bytes

Objetivo:
  procesos:  cs2.exe, gameoverlayui64.exe
  módulos:   client.dll, vgui2_s.dll, ntdll.dll
  símbolos:  CreateInterface, VGUI_Setup001, VGUI_Surface039

Cadenas en claro en el binario: NINGUNA, todas cifradas con XOR 0x34</code></pre>
<p>Que un análisis antivirus marque este fichero como <code>Trojan.Win32.Generic</code> es esperable: la asignación de memoria con ejecución y escritura simultáneas más la carga reflectiva más la inyección en otro proceso son exactamente los heurísticos que disparan los motores. Este informe explica por qué saltan aquí y en qué se diferencian de una infección real: un Troyano real tiene cadenas, sockets y ficheros de los que este binario no tiene nada.</p>
<hr>
<h2 id="15-conclusiones">15. Conclusiones</h2>
<p>Un loader de cheat y un malware se parecen mucho en la superficie: reservan memoria ejecutable, escriben en la memoria de otro proceso, crean un hilo remoto y resuelven símbolos en tiempo de ejecución. Por eso un análisis únicamente basado en indicadores heurísticos no puede separarlos, y por eso este caso merece un análisis estático completo.</p>
<p>Lo que separa este binario de un Troyano son tres cosas concretas y verificables.</p>
<p><strong>La primera es la contención de capacidades.</strong> El payload no tiene ninguna primitiva de red ni de fichero. Eso no es una propiedad de &ldquo;parece limpio&rdquo;; es una lista cerrada de doce punteros a función derivada de los desplazamientos reales de las instrucciones. Un Troyano necesita salir de la máquina; este no puede.</p>
<p><strong>La segunda es la ausencia de cadenas.</strong> Veinticinco kilobytes de payload sin una sola cadena legible es prácticamente inaudito en malware. Cuando el código necesita un <code>socket</code> o una ruta, deja un rastro. Aquí todo pasa por punteros que el loader entrega ya resueltos.</p>
<p><strong>La tercera es la coherencia interna.</strong> El binario contiene los nombres de las features, los valores por defecto de esas features, la resolución de las interfaces del motor de Valve, un hook sobre una función de ese motor, y mensajes que dicen que se ha inyectado un cheat. Todo apunta en la misma dirección y nada apunta en la contraria.</p>
<p>Queda una lección para el usuario final que es más importante que el veredicto técnico: <strong>el riesgo real de un binario de este tipo no es lo que hace, sino lo que hay que desactivar para poder usarlo.</strong> Un ejecutable que exige apagar el antivirus, el firewall y el control de cuentas antes de correr es, en si mismo, un indicador de compromiso mucho más fuerte que cualquier análisis estático.</p>
<p>Y una nota sobre el PIN, que es el hallazgo más instructivo del análisis desde el punto de vista del diseño de software: <strong>un secreto que se deriva del reloj no es un secreto.</strong> El autor implementó un TOTP con el reloj público como única entrada, y el resultado es que cualquier usuario puede calcular su propia licencia. La ofuscación de las cadenas, los imports por ordinal y la compilacion con optimización guiada por perfil son esfuerzo serio en una sola dirección, y al mismo tiempo la lógica de autorización es trivialmente invertible. Son cosas que no se ven a la vez.</p>
<hr>
<h2 id="agradecimientos">Agradecimientos</h2>
<p>Este análisis se realizó con la asistencia de <strong>opencode</strong>, en el marco de un modelo operado por <strong>Space Bunny</strong> con <strong>Claude Sonnet 5.5</strong>, usando el servidor MCP de Ghidra para el desensamblado y la ingeniería inversa, y scripts propios en Python para el volcado de cadenas, la extracción de indicadores y el análisis de capacidades.</p>
<p>El trabajo de los modelos de lenguaje fue la parte mecánica: descifrar cadenas, enumerar referencias cruzadas, correlacionar longitudes de blob con contenido y aplicar transformaciones aritméticas. <strong>Las decisiones sobre que es evidencia y que es hipótesis, y sobre dónde están los límites del método, fueron del analista humano.</strong> Los cuatro fallos de instrumentación documentados en la sección 3 son también parte del registro, porque un informe de análisis sin sus errores no es auditable.</p>
<hr>
<p><em>Análisis realizado exclusivamente por medios estáticos. El binario no fue ejecutado en ningun momento. Herramientas: Ghidra con servidor MCP, y utilidades propias en Python estándar sin dependencias externas.</em></p>
]]></content:encoded></item></channel></rss>