2 puntos por GN⁺ 2025-01-10 | 1 comentarios | Compartir por WhatsApp
  • La conversión de caracteres Best-Fit de Windows sustituye caracteres por otros de apariencia similar al convertir cadenas UTF-16 a páginas de códigos ANSI, y este comportamiento se convierte en la superficie de ataque WorstFit, que puede derivar en Path Traversal, Argument Injection y RCE
  • El problema surge de la combinación de las API ANSI, el runtime de C/C++, el código de inicio insertado por el compilador y el uso por parte de desarrolladores de API de caracteres no wide; las rutas GetCommandLineA, GetEnvironmentVariableA, getenv e int main() se ven afectadas
  • CVE-2024-4577 eludió el parche de PHP-CGI cuando U+00AD se convertía en - en páginas de códigos chinas/japonesas; Filename Smuggling genera confusión de rutas cuando ¥, y la barra fullwidth se convierten en / o \
  • Argument Splitting puede crear caracteres de parsing de línea de comandos mediante comillas dobles fullwidth o signos de yen/won, permitiendo inyectar argumentos en herramientas CLI como wget.exe, tar.exe, openssl.exe y java.exe; es difícil bloquearlo solo con el escape de argumentos habitual de PHP, Python, Node.js y Rust
  • Para mitigarlo, hay que activar la opción UTF-8 de Windows o que los desarrolladores usen la Wide Character API y rutas wide character como _wgetcwd, _wgetenv y wmain(); hasta que Microsoft active UTF-8 por defecto en todas las ediciones de Windows, podrían repetirse problemas similares

Estructura de codificación de Windows y Best-Fit

  • Windows usaba inicialmente páginas de códigos ANSI, y según la región lingüística variaban páginas de códigos como 1252, 932, 936, 949 y 950
    • ACP (ANSI Code Page) se usa en la mayoría de las aplicaciones y configuraciones del sistema, como operaciones de archivos y variables de entorno
    • OEMCP (Original Equipment Manufacturer Code Page) se usa principalmente para comunicación con dispositivos, como lectura/escritura en consola
    • chcp muestra la OEMCP, no la ACP, por lo que no sirve para verificar la ACP, que es el foco de esta investigación
  • Windows migró a Unicode a mediados de la década de 1990, y hoy sus API centrales usan wide characters basados en UTF-16
    • API centrales como sistema de archivos, información del sistema y procesamiento de texto pasaron a API de caracteres wide
    • La funcionalidad UTF-8 existe, pero no viene activada por defecto en la mayoría de los idiomas; el artículo la describe como en fase beta
  • Por compatibilidad hacia atrás, la API de Windows ofrece versiones ANSI y Unicode
    • Las API ANSI llevan el sufijo A, como GetEnvironmentVariableA
    • Las API Unicode llevan el sufijo W, como GetEnvironmentVariableW
    • Cuando se llama una API ANSI, Windows convierte la cadena UTF-16 interna a una cadena ANSI mediante RtlUnicodeStringToAnsiString o WideCharToMultiByte

Cómo Best-Fit se convierte en WorstFit

  • Best-Fit es el comportamiento que mapea un carácter UTF-16 a otro de apariencia o sensación similar cuando no puede representarse exactamente en la página de códigos ANSI de destino
    • Por ejemplo, en Windows-1252, U+221E se mapea a 8
    • Si √π⁷≤∞ pasa por una API ANSI, puede convertirse en algo como "vp7=8"
  • El mapeo se comporta de forma distinta según la página de códigos
    • ¥ U+00A5 se mapea a \ en la página de códigos japonesa 932
    • En la página de códigos centroeuropea 1250 se mapea a Y
    • En la mayoría de las demás páginas de códigos no cambia
  • La misma conversión ocurre no solo al llamar directamente a la API de Windows, sino también en funciones CRT y en la ruta habitual de la función main
    • La conversión Best-Fit se aplica en funciones CRT non-wide como getenv
    • La conversión también interviene cuando se reciben argumentos y variables de entorno con la forma int main(int argc, char* argv[], char* envp[])
    • Esto se debe a la combinación del código de inicio CRT insertado por el compilador y el uso de API ANSI de Windows
  • Para verificar los mapeos, se puede consultar Best-fit Mapping Grepper y los datos de mapeo sin procesar WindowsBestFit de Unicode.org

Primer caso de WorstFit: PHP-CGI CVE-2024-4577

  • CVE-2024-4577 es un caso de ataque WorstFit que permitió comprometer servidores PHP-CGI configurados con páginas de códigos chinas/japonesas usando solo la solicitud ?%ADs
    • Las páginas de códigos afectadas son 932 (japonés), 936 (chino simplificado) y 950 (chino tradicional)
    • El carácter amenazante es ­ U+00AD
  • La vulnerabilidad de PHP-CGI de 2012 fue un caso de argument injection que ocurrió porque Apache procesaba automáticamente la query string como primer argumento del programa CGI
    • Al agregar ?-s, era posible filtrar el código fuente de la página y lograr RCE
    • El parche de PHP consistía en detener el parsing de argumentos si la query string comenzaba con un dash
  • Debido a Best-Fit, el soft hyphen U+00AD se convierte en - en páginas de códigos chinas/japonesas, lo que permite eludir el parche existente
    • ?%ADs puede comportarse como -s desde la perspectiva de PHP-CGI
    • A partir de este caso, el equipo de investigación se encontró por primera vez con el término Best-Fit

Filename Smuggling: el problema de la conversión de caracteres de ruta

  • Filename Smuggling es un ataque en el que caracteres Unicode incluidos en nombres de archivo se convierten en / o \ en rutas de API ANSI, lo que puede crear path traversal
    • Las API relacionadas incluyen GetCurrentDirectoryA, getcwd, FindFirstFileA, findfirst*, GetFullPathNameA, entre otras
    • Las páginas de códigos afectadas son 874, 125x, 932 (JP) y 949 (KR)
    • Los caracteres amenazantes son U+FF0F, U+FF3C, ¥ U+00A5 (JP) y U+20A9 (KR)
  • d8.exe, el Developer Shell de Chrome V8, usa GetCurrentDirectoryA() en su implementación interna para obtener el directorio de trabajo actual
    • Si se puede crear un directorio de trabajo con caracteres Unicode maliciosos, al acceder mediante la API ANSI se convierte en un payload de path traversal
    • Como ejemplo, permite un acceso no intencional a C:/windows/win.ini
  • La implementación para Windows de Dir.getwd() en mruby depende de la función CRT ANSI _getcwd()
    • El valor devuelto puede contaminarse y derivar en Path Traversal

Cuckoo Sandbox: de Path Traversal a RCE

  • El acceso al sistema de archivos de Windows desde Python podía usar la API wide o la API ANSI según si la cadena era wide o narrow
    • Después de PEP 529, la codificación del sistema de archivos de Windows se estandarizó en UTF-8
    • Python 2 y Python 3 antes de Python 3.6 quedaron vulnerables a ataques WorstFit
  • Cuckoo Sandbox es una plataforma automatizada de análisis de malware, y su versión oficial más reciente depende de Python 2.7
    • Cuckoo está compuesto por Cuckoo Host y un clúster de VM
    • Las muestras subidas se ejecutan de forma aislada en una VM, y los paquetes de red, archivos droppeados y logs se sincronizan mediante su propio mecanismo
  • Si el malware crea un archivo droppeado con nombre Unicode, puede producirse un Path Traversal en el procesamiento de rutas de Python en Cuckoo Host
    • El PoC de ejemplo genera la ruta AAAA\u00a5..\u00a5..\u00a5..\u00a5..\u00a5..\u00a5conf\u00a5cuckoo.conf
    • Tras finalizar el análisis, cuando el usuario presiona el botón de descarga en la interfaz web, se dispara una operación de archivo de Python
    • Cuckoo Host puede procesar la ruta convertida que contiene ../ y enviar datos sensibles al atacante
  • El atacante puede descargar cuckoo.conf, recolectar la información sensible necesaria para calcular el PIN de Flask y lograr RCE en el Sandbox Host
    • El video de demostración está disponible como Video 11

Argument Splitting: Best-Fit que cambia el parsing de la línea de comandos

  • Argument Splitting es un ataque en el que la cadena de línea de comandos cambia y los argumentos se separan en la salida de GetCommandLineA o en la ruta non-Unicode de int main()
    • Las API y rutas relacionadas son GetCommandLineA, int main()
    • Las páginas de códigos afectadas son 874, 125x, 932(JP), 949(KR)
    • Los caracteres de amenaza son U+FF02, U+FF3C, ¥ U+00A5(JP), U+20A9(KR)
  • El código PHP de ejemplo envuelve de forma segura una URL con escapeshellarg() y luego ejecuta wget.exe -q, pero con la entrada " --use-askpass=calc " es posible ejecutar calc.exe
    • La misma entrada no se mitiga aunque se cambie a Node.js, Rust o Python
    • También funciona en el ejemplo con subprocess.run(["wget", "-q", ...]) de versiones recientes de Python
  • Windows pasa al proceso nuevo toda la línea de comandos como una sola cadena, y el ejecutable la parsea directamente
    • No es una estructura en la que siempre se entregue un arreglo de argumentos como en sistemas tipo UNIX
    • La API CreateProcess recibe directamente el parámetro lpCommandLine
  • En el parsing común de línea de comandos, los caracteres importantes son espacios y tabs, double quote y backslash
    • Los espacios y tabs separan argumentos cuando no se está en quote mode
    • " alterna el quote mode
    • \ escapa double quote y backslash en secuencias específicas
  • Las bibliotecas estándar de la mayoría de los lenguajes escapan los argumentos de usuario conforme a estas reglas, pero el escape termina antes de la conversión Best-Fit
    • PHP escapeshellarg cambia los double quote por espacios, envuelve el argumento entre quotes y procesa los backslash
    • Python subprocess usa list2cmdline para escapar según las reglas de parsing de línea de comandos de Microsoft CRT
    • Si después, durante la conversión ANSI, U+FF02 se convierte en " U+0022, cambia la sintaxis original de la línea de comandos
  • Los programas que solo usan int main() también pueden ser vulnerables
    • El compilador genera mainCRTStartup en el binario, y esta función de inicio se enlaza con la biblioteca CRT
    • Si internamente la CRT obtiene y parsea la línea de comandos con la API ANSI, interviene la conversión Best-Fit
    • Debido a este comportamiento, es difícil bloquear completamente el ataque solo con la biblioteca estándar de un lenguaje de programación específico

Casos reales de Argument Splitting

  • ElFinder es un administrador de archivos web open source basado en un backend PHP, y por defecto soporta servidores Windows y la creación/extracción de archivos comprimidos
    • El procesamiento de archivos se implementa mediante ejecución de shell command, y los argumentos se escapan con escapeshellarg
    • Para procesar el formato tar se usa el tar.exe integrado de Windows
    • Con un nombre de archivo tar como aaa" "--use-compress-program=calc" "bbb.tar, es posible inyectar el argumento --use-compress-program y ejecutar comandos arbitrarios
    • La demo está basada en un servidor Windows configurado en inglés y Code Page 1252, y se resume que también debería funcionar en páginas de códigos 125x y Code Page 874
    • El video de demostración está disponible como Video 12
  • El caso del plink.exe modificado usado en TortoiseGit puede disparar ejecución de código si se ingresa un URI malicioso como entrada de clone
  • RStudio soporta control de versiones SVN, y si hay un proyecto SVN en una carpeta creada maliciosamente, es posible ejecutar la calculadora con un solo clic
  • El caso de Microsoft Excel es CVE-2024-49026, que combina Argument Splitting con la función “Open-With” de Windows
    • Windows mantiene una handler table por extensión de archivo, que se puede consultar con ftype y assoc
    • El nombre de archivo pasa a formar parte de los argumentos del programa handler, por lo que el ataque puede aplicarse mediante el nombre de archivo
    • Un nombre de archivo en el que dots, slash, backslash y double quote se cambian a sus formas fullwidth provoca argument injection en Excel.exe
    • Como Excel en sí no tiene argumentos adecuados para una explotación adicional, se logra RCE usando también NTLM Relay y RBCD/ADCS
    • El video de demostración es Video 15

Confusión de variables de entorno

  • Confusión de variables de entorno ocurre cuando GetEnvironmentVariableA, GetEnvironmentStringsA y char *getenv() devuelven la versión de las variables de entorno transformada con Best-Fit
    • No se especifican las páginas de código afectadas ni los caracteres amenazantes
    • En el caso de Apache HTTPd, están involucrados los valores 0x00-0xFF
  • Para que este ataque funcione, las variables de entorno deben poder ser controladas por el usuario
    • Esto aplica cuando un proceso padre pasa información a un proceso hijo que creó
    • En CGI, gran parte de la información de una solicitud HTTP, como el query string y los headers HTTP, se pasa mediante variables de entorno
  • El ejemplo de evasión de WAF trata una situación en la que un script CGI actúa como un routing service
    • La configuración de Apache tiene una regla que rechaza REQUEST_URI que incluya /admin, para impedir el acceso remoto a /cgi.pl/admin
    • Debido al comportamiento WorstFit de Perl en Windows, es posible evadirla si se reemplaza parte de admin por un equivalente Best-Fit
    • En la Code Page 1250, à U+00E0 se convierte en a durante la conversión ANSI
    • La solicitud /cgi.pl/%E0dmin se ve como una ruta distinta para las reglas del lado del servidor, pero cuando el script Perl CGI lee PATH_INFO con la API ANSI, se procesa como /admin
  • En PHP-CGI on Windows, en ciertas configuraciones se confirmó un oracle de existencia de archivos y un posible LFI
    • La causa es la forma en que se procesan PATH_INFO y otras variables de entorno relacionadas con rutas
    • Una solicitud /index.php/foo/bar, según Apache, se pasa mediante variables de entorno como REDIRECT_URL, REQUEST_URI, PATH_INFO y PATH_TRANSLATED
    • Solo con esta información es difícil distinguir con claridad el límite entre el nombre del archivo PHP y el PATH_INFO adicional, y php-cgi.exe lo interpreta
  • En la página de código japonesa, usar ¥ hace que el servidor web y PHP-CGI interpreten las rutas de manera distinta
    • El servidor web trata todo /..¥..¥windows/win.ini/foo como PATH_INFO adicional
    • PHP-CGI recibe un valor transformado como REQUEST_URI=/index.php/..\..\windows/win.ini/foo y se confunde al separar el archivo PHP real de PATH_INFO
    • En Apache, es posible un file existence oracle por la diferencia de respuesta entre archivos inexistentes y existentes
    • En IIS, si está configurada la directiva doc_root, es posible un LFI que incluya y lea C:\Windows\win.ini mediante una ruta como /index.php/..¥..¥..¥windows/win.ini/
    • Si el archivo incluido es ejecutable o contiene código controlable por el usuario, podría derivar en un RCE potencial, pero ese escenario se clasifica más bien como un bug poco común en aplicaciones reales

Dificultades del proceso de divulgación y corrección

  • El equipo de investigación reportó varios problemas en lenguajes de programación, proyectos open source y programas CLI integrados en Windows a cada upstream maintainer
    • La mayor controversia surgió con Argument Splitting
    • Algunos vendors consideran que pasar input del usuario a la línea de comandos ya es una vulnerabilidad en sí mismo
  • La responsabilidad poco clara también fue un problema
    • El código problemático abarca mainCRTStartup(), que se inserta automáticamente durante la compilación, y llamadas internas a la API ANSI de MSVCRT/UCRT
    • Es difícil distinguir si el problema es que el desarrollador no usó wmain() o si el CRT dividió mal la línea de comandos y pasó argumentos incorrectos a main()
    • Algunos proyectos solo proporcionan el código fuente, mientras que los Windows prebuilt executable son distribuidos por voluntarios terceros en Internet
  • La corrección no consiste simplemente en cambiar main() a una versión wide-character
    • Si cambia la firma de la función, las definiciones de variables y la lógica de parseo de argumentos deben reescribirse de char * a una base wchar_t *
    • Este proceso es doloroso y propenso a errores
  • Curl respondió que se trata de una funcionalidad de Windows y que no tiene planes de corregirlo; el Curl portado por Microsoft cambió el entry a wmain(), por lo que el curl.exe integrado en Windows no se ve afectado
    • Los binarios oficiales de Curl sí se ven afectados por el ataque de Argument Splitting
    • El reporte completo se publicó en HackerOne
  • OpenSSL puede procesar argumentos en formato wide character mediante la variable de entorno OPENSSL_WIN32_UTF8
    • Su objetivo original era corregir problemas de visualización de UTF-8 en la UI, pero también mitiga el ataque de Argument Splitting
    • En el uso predeterminado de OpenSSL, muchos desarrolladores no saben que deben configurar esta variable de entorno, y es posible ejecutar código arbitrario usando el argumento -engine
  • La distribución oficial de Perl no proporciona Windows prebuilt executable, y suelen usarse instaladores de terceros como Strawberry Perl y ActiveState Perl
    • Ambas distribuciones se ven afectadas por el ataque de Argument Splitting
    • Tras conversar con los maintainers de Perl, la conclusión fue que “se parece más a un bug de Microsoft que a un bug de Perl”, por lo que actualmente sigue sin resolverse
  • Se reportaron tres casos a Microsoft mediante MSRC, y todos fueron rechazados inicialmente por no alcanzar el umbral de severidad
    • Después de reabrirlos varias veces, solo el caso de Excel fue aceptado tras el tercer intento
    • Los demás casos siguen sin resolverse hasta ahora
    • MSRC respondió que dependen de una vulnerabilidad en la que otra aplicación ejecuta input no confiable al ponerlo en la línea de comandos, y que la técnica que lo vuelve explotable no cumple por sí misma los requisitos para ser considerada una vulnerabilidad
  • También se pidió ayuda a CERT/CC, y meses después Microsoft agregó una advertencia de seguridad a la documentación de GetCommandLineA
    • La advertencia solo se agregó a GetCommandLineA, y aún quedan más API ANSI que requieren precaución

Objetivos afectados reportados y estado

  • Los elementos confirmados y reportados durante el proceso de divulgación son los siguientes
    • 2024/05/07: PHP php-cgi.exeCVE-2024-4577
    • 2024/06/13: Curl Official BuildWon’t Fix
    • 2024/06/13: Apache Subversion svn.exeCVE-2024-45720
    • 2024/06/16: Microsoft Tar tar.exe — Won’t Fix
    • 2024/06/19: Microsoft Excel excel.exeCVE-2024-49026
    • 2024/06/19: Microsoft PhoneBook rasphone.exe — Won’t Fix
    • 2024/06/19: Oracle Java java.exe — Pending Fix
    • 2024/06/19: Perl perl.exe — Won’t Fix
    • 2024/07/15: Perforce p4.exeCVE-2024-8067
    • 2024/08/05: PostgreSQL psql.exe — Won’t Fix
    • 2024/08/08: Putty plink.exe — Fixed
    • 2024/08/19: OpenSSL openssl.exe — Other
    • 2024/08/19: wkhtmltopdf wkhtmltopdf.exe — EOL
    • 2024/08/19: GNU Wget — No Reply

Mitigaciones y superficie de ataque restante

  • Como los ataques WorstFit son un problema a nivel del sistema operativo, es posible que sigan reapareciendo problemas similares hasta que Microsoft active UTF-8 como valor predeterminado en todas las ediciones de Windows
  • La medida que pueden tomar los usuarios es revisar y activar la opción UTF-8 de Windows
    • Esta función todavía aparece marcada como beta, y no está claro si puede tener efectos secundarios
  • Los desarrolladores deben usar la API de caracteres anchos siempre que sea posible
    • El CRT también ofrece versiones de caracteres anchos como _wgetcwd y _wgetenv
    • Si se siguen usando rutas no wide, la implementación interna puede llamar a la API ANSI y quedar expuesta a ataques WorstFit
  • Debido a la compatibilidad hacia atrás de Windows, puede haber más lugares donde la API ANSI esté oculta
    • Por ejemplo, las consultas del Registro de Windows como RegQueryValueA podrían verse afectadas, pero hay que encontrar escenarios vulnerables
    • El equipo de investigación también observó el comportamiento Best-Fit en Active Directory

1 comentarios

 
GN⁺ 2025-01-10
Comentarios de Hacker News
  • Este es un problema bastante complicado. El mapeo de código “best fit” de Microsoft es un mapeador público, pero en la práctica “basado en sensaciones”, que convierte Unicode amplio a ASCII, y está presente en todo el sistema.
    Este mapeador se vincula por defecto en muchísimos lugares y, por la forma en que Microsoft ve la compatibilidad hacia atrás, parece inevitable que siga incluido. Los exploits suelen surgir cuando puntos de código inusuales se mapean “por sensación” a barras, guiones o comillas. Dentro de lenguajes modernos se validan como Unicode correcto, pero cuando pasan a comandos de shell o a la API Win32, después de ceder el control se reducen de otra manera. Como dijo el mantenedor de curl, aquí “curl es la víctima”; la cuestión es quién es el culpable. Si un servidor aplasta de forma distinta los datos de entrada del usuario al validarlos y al pasarlos a una biblioteca del sistema, tarde o temprano habrá problemas. Una opción para desactivar la conversión best fit del lado de Win32 podría ser la solución, aunque no soy especialista en Windows, así que es una conjetura. Aun así, se seguiría interactuando con API oficiales o con software que todavía no la desactivó.

    • La forma de optar por no usarlo es usar la API Unicode de Windows, es decir, funciones que terminan en "w" en vez de "a". Este enfoque también resuelve el problema de rutas de más de 260 caracteres si se agrega el prefijo "\\?\" o se configura correctamente el manifiesto, y está disponible y recomendado desde Windows XP.
      No sé muy bien por qué las API no Unicode se siguen usando tan ampliamente. Cuesta imaginar que sea por el deseo de dar soporte a Windows 98 o Windows 2000.
    • Windows tiene desde Windows XP los archivos de manifiesto, que son una forma de desactivar comportamientos heredados. Recuerdo que, sin un manifiesto, incluso GetWindowsVersion no devolvía la versión actual. Agregar ahí una opción de exclusión y convertirla algún día en el valor predeterminado de Visual Studio no parece demasiado difícil.
      También hace falta algún tipo de linting. En una aplicación moderna normalmente no hay motivo para llamar funciones ANSI de WinAPI. También podría existir el enfoque de configurar la locale como UTF-8 y usar solo funciones de 8 bits, pero no sé qué tan bien funciona. Tengo entendido que también hay algunas configuraciones y headers para hacer que argv, printf y std::cout funcionen en UTF-8, y que solo se usen funciones de conversión UTF-8/UTF-16 para WinAPI sin conversiones raras. Microsoft debería documentar todo ese procedimiento en un solo lugar.
    • Sea o no una vulnerabilidad de seguridad, si curl no maneja correctamente los argumentos Unicode en Windows, también es un bug de curl.
    • La forma de mapear puntos de código a caracteres de manera laxa siempre me molestó en Unicode.
  • Esto es esperable hasta cierto punto, pero incluso para alguien que pasó unos 10 años desarrollando para Windows y hackeando la API de Wine en la época en que surgía la confusión W/A, fue algo nuevo.
    Windows es como el juego de cartas Munchkin: cuando varias funciones encajan por casualidad, pueden combinarse en exploits increíblemente aleatorios y potentes. Es bueno que estén cambiando el subsistema ANSI a UTF-8 y, en teoría, eso podría mitigar muchos de estos problemas. Me pregunto si el equipo de Rust tendrá que hacer otra corrección más en la API de creación de procesos.

    • La biblioteca estándar de Rust casi no usa la API ANSI por defecto. El artículo no mostró un ataque que funcione contra Rust, y si existe uno, definitivamente convendría reportarlo.
      Por supuesto, Rust no puede controlar lo que pasa más allá del límite del proceso. Si una aplicación ejecutada por Rust usa la API ANSI, el problema aparece de ese lado, pero eso es responsabilidad de esa aplicación.
  • “Eliminar gradualmente ANSI y recomendar el uso de la API Wide Character” era, si no recuerdo mal, la postura oficial de Microsoft desde NT 3.5.
    Por desgracia, uno de los grandes obstáculos es la forma en que está implementada la biblioteca de runtime C/C++ de Microsoft, msvcrt.dll. Las funciones wide no estándar como _wfopen() y _wgetenv() usan internamente las funciones W de la Win API, pero las funciones narrow estándar como fopen() y getenv() simplemente usan las funciones A en vez de convertir a sus versiones wide. Y las funciones A normalmente no informan fallos de conversión Unicode, sino que los tapan con best-fit. Quien porta software escrito en C a Windows no quiere cambiar todo el uso de funciones estándar por funciones no portables de Microsoft. A partir de ese punto, en la práctica es una reescritura completa.

    • La impresión que me quedó al leer la documentación de Microsoft durante los últimos dos años fue la contraria: configurar activeCodePage como UTF-8 en el manifiesto de la aplicación y usar solo funciones “ANSI”.
    • En código portable, cuando la build es para Windows se hace #define de funciones estándar como main y fopen a sus equivalentes wide.
      Con eso ya no se pueden usar char* y literales de cadena sin decorar tal cual, así que se define un tipo tchar que es char en Linux y wchar_t en Windows, y una macro _T() para literales de cadena. En general funciona bien sin pensarlo demasiado.
    • Lo que realmente molesta hoy es que, al buscar la API Win32 en Google, siempre aparece primero la variante -A y no la variante -W. No sé si hay algo raro en robots.txt, pero es extraño que una API que recomienda usar la variante -W para código nuevo devuelva por defecto la API heredada.
    • El runtime C/C++ de Microsoft, msvcrt.dll, fue reemplazado por el Universal C Runtime (UCRT)[1], y UCRT cumple con C99.
    • Windows debería haber ofrecido una API que tratara los nombres de ruta simplemente como secuencias de bytes, sin este manejo tonto de codificaciones. Creo que podrían haberlo hecho cuando introdujeron las rutas UNC.
  • Hay dos formas de forzar que la página de códigos “Ansi” sea realmente UTF-8 en aplicaciones propias o en EXE parcheados.
    Una es usar un archivo de manifiesto, y funciona a partir de cierta build de Windows 10. También se puede aplicar a cualquier EXE después de compilarlo, así que permite meter soporte UTF-8 a la fuerza en un programa. Es especialmente útil para programas en modo consola. La otra es usar el tipo de hack que emplean herramientas estilo “App Locale”. Uno de los métodos incluye llamar funciones no documentadas de NTDLL. No sé exactamente qué funciones hacen falta, pero RtlInitNlsTables y RtlResetRtlTranslations podrían estar relacionadas.

  • No estoy seguro de que Microsoft vaya a activar UTF-8 por defecto en todas las ediciones de Windows. Hay muchas aplicaciones antiguas que asumen una página de códigos específica o 1 byte por carácter, y podrían romperse.
    De forma más sutil, también hay aplicaciones que reutilizan buffers existentes asumiendo que, al convertir de caracteres wide a ANSI, la cantidad de bytes no aumenta. Con UTF-8 eso no es así, y como en la mayoría de las páginas de códigos existentes más o menos se cumplía, podrían aparecer nuevas vulnerabilidades. Creo que sería mucho menos disruptivo quitar la lógica Best-Fit de las API Win32 xxxA y reemplazar los caracteres que no se puedan mapear por algo como x, que no tiene un significado meta común.

    • Un ejemplo de una aplicación así es Adobe After Effects[0]. Al menos antes era así; ahora ya no uso Windows.
      [0] https://tambre.ee/blog/adobe_after_effects_windows_utf-8/
    • Si todavía no existe, quizá podrían introducir una versión de API del sistema operativo para que las apps nuevas o actualizadas que apunten a una nueva versión de API o a un SDK nuevo asuman UTF-8 por defecto. Por debajo de cierta versión de API, bastaría con emular un modo legacy. Windows ya tiene el concepto de shim para imitar el comportamiento de varias versiones de Windows.
    • Incluso en Windows antes de UTF-8 ya existía el problema de que las apps se comportaran raro si se cambiaba la página de códigos predeterminada. Así que tiene sentido darle al usuario una opción de UTF-8.
      Viendo los problemas que causa el mapeo Best-Fit, también tiene sentido convertirlo en el valor predeterminado, pero Microsoft debería ayudar a los usuarios a encontrar formas sencillas de ejecutar código antiguo. Una forma menos razonable sería eliminar del mapeo Best-Fit todas las correspondencias hacia caracteres ASCII “especiales”, pero eso no ayuda a las apps que enlazaron estáticamente el CRT. Tampoco corrige la vulnerabilidad, así que no es una buena solución. A veces, una vulnerabilidad de seguridad sirve como motivación para impulsar una ruptura de compatibilidad hacia atrás.
  • Microsoft conocía este problema desde hace al menos un año. Sacó una regla especial de análisis de código llamada CA2101[1], que desaconsejaba explícitamente el uso de mapeos best-fit.
    La descripción de la regla mencionaba vulnerabilidades de seguridad, pero dejaba los detalles deliberadamente vagos.
    [1] https://learn.microsoft.com/en-us/dotnet/fundamentals/code-a...

  • No hace falta cambiar todo de char * a wchar *. Puedes convertir los caracteres wide recibidos a UTF-8, o, si quieres permitir incluso secuencias inválidas como sustitutos sin pareja, convertirlos a algo como WTF-8 de Rust y seguir usando char.
    Por supuesto, hay que tener cuidado de no mezclar cadenas ANSI u OEMCP con cadenas UTF-8, pero si usas solo UTF-8 es sencillo. Ese es el enfoque que recomienda el clásico sitio https://utf8everywhere.org/.

  • En mi computadora personal con Windows tenía activado el modo UTF-8 desde hace unos años, así que estaba evitando este bug por casualidad. Es la configuración que aparece al final del artículo.
    Lo había activado porque algunos juegos extranjeros antiguos mostraban texto corrupto y, aunque aparece marcado como “Beta”, no noté bugs ni efectos secundarios.

    • Es interesante, pero en mi caso esa casilla no hizo más que hacer que muchas apps aleatorias crashearan. Parece que si funciona bien o no depende de cuál sea la página de códigos predeterminada del usuario cuando está desactivada.
    • Acabo de activar la opción “Beta: Use Unicode UTF-8 for worldwide language support”. Va a ser interesante ver cuántas apps se rompen.
  • Me preguntaba si la casilla beta era lo mismo que configurar ActiveCodePage como UTF-8 en el manifiesto, pero la documentación[0] deja claro que GDI no sigue la página de códigos por proceso, sino solo la página de códigos global única que establece esa casilla.
    Es una pena que no puedas hacer opt-in completo a UTF-8 para las API *A desde tu propia app. Aun así, para los problemas que destaca el artículo, creo que sigue siendo una solución alternativa válida o una medida de defensa en profundidad.
    [0] https://learn.microsoft.com/en-us/windows/apps/design/global...

  • Dios mío. Sabía que la API de Windows ofrecía ese tipo de conversión best-fit, pero no sabía que era el comportamiento predeterminado de varias funciones ANSI en mi página de códigos predeterminada, 949[1].
    A esta altura deberían prohibirlo sin más, como gets. [1] Sé que existe la página de códigos UTF-8 65001. Durante mucho tiempo fue prácticamente inutilizable, y aún hoy tiene problemas de compatibilidad.