Métodos universales para trabajar aspectos básicos con fuentes de texto (.ttf, .otf y .fon).
Aspectos destacables del código
◉ Nombres descriptivos y documentación extensa, no creo que requieran ejemplos de uso (de todas formas no me cabrían en este post).
◉ Ligeras micro optimizaciones para .NET 5+ mediante directiva del preprocesador (#If NETCOREAPP...)
Incluye varios métodos para:
◉ Instalar/desinstalar una fuente solamente para el usuario local, o de forma global. Para esto último es posible requerir permisos de administrador.
◉ Determinar si una fuente está actualmente instalada en el sistema operativo, identificando varios aspectos como si el nombre del archivo o el nombre de la fuente están registradas en el Registro de Windows.
◉ Determinar el formato de un archivo de fuente. Soporta los formatos: TrueType (.ttf), OpenType con contornos TrueType (.ttf), OpenType PostScript (CFF) (.otf), y raster/bitmap (.fon).
◉ Obtener el nombre amistoso completo de una fuente de texto, exactamente tal y como se muestra en la barra de título del visor de fuentes de Windows (FontView.exe).
◉ Obtener el nombre del archivo de recurso de fuente escalable (.FOT) a partir de un archivo de fuente.
En torno a la instalación y desinstalación de fuentes:
◉ Al instalar una fuente permite cargarla en memoria, con lo cual se enviará el mensaje correspondiente a todas las ventanas del sistema operativo para notificar de un cambio (una nueva fuente disponible), de tal forma que otros programas puedan reconocer y utilizar dicha fuente.
◉ Al instalar una fuente se identifica correctamente el formato TrueType u OpenType y se registra apropiadamente en el nombre de la clave de registro correspondiente. Se puede anular este comportamiento mediante un parámetro Boolean para que siempre se añada el sufijo "(TrueType)" al nombre de la clave de registro tal y como lo hace la shell de Windows indiferentemente de si la fuente es OpenType. Esto no se aplica a fuentes raster/bitmap (.fon).
◉ Al desinstalar una fuente, permite eliminar el archivo. Si no se puede eliminar al primer intento, se detiene temporalmente el "Servicio de caché de fuentes de Windows" ('FontCache') para evitar posibles bloqueos y reintentar la eliminación. Al finalizar la desinstalación, se reanuda el servicio.
Diferencias en los nombres de fuentes
Para entrar en contexto y ver las diferencias en perspectiva, y tomando como ejemplo la fuente de texto OpenType PostScript (CFF) "JustBreatheBoldObliqueseven-7vgw.otf" (descarga), estos son los resultados:
◉ Nombre de la clave de registro al instalar la fuente de forma tradicional mediante la shell de Windows 10 (Menú contextual -> Instalar):
Código:
Just Breathe Bold ObliqueSeven (TrueType)
(sí, pone 'TrueType' a pesar de ser una fuente OpenType CFF, sin contornos TrueType.)
◉ Nombre mostrado en la barra de título del visor de fuentes de Microsoft Windows (FontView.exe)
Código:
Just Breathe Bold ObliqueSeven (OpenType)
◉ Nombre devuelto por mi función GetFontFriendlyName, con sufijo:
Código:
Just Breathe Bold ObliqueSeven (OpenType)
(Siempre debería devolver el mismo nombre que en el visor de fuentes de Microsoft Windows, eso sí, sin espacios en blanco adicionales al final del nombre ni antes del paréntesis del sufijo, cosa que FontView.exe no tiene en cuenta, pero mi código sí. Lo he comparado programaticamente con aprox. 14.000 fuentes de texto para asegurarme de su fiabilidad.)
◉ Nombre devuelto por mi función GetFontFriendlyName, sin sufijo:
Código:
Just Breathe Bold ObliqueSeven
◉ Nombre devuelto por mi función GetFontResourceName:
Código:
JustBreatheBdObl7
(A veces, GetFontResourceName devolverá el mismo nombre que GetFontFriendlyName sin sufijo, es decir, el nombre escrito en el recurso de fuente escalable puede ser idéntico.)
◉ Nombre devuelto utilizando una combinación de propiedades de la clase System.Windows.Media.GlyphTypeface:
Código:
Just Breathe BdObl7
El código utilizado:
Código
Dim fontUri AsNew Uri("C:\JustBreatheBoldObliqueseven-7vgw.otf", UriKind.Absolute)
Dim gtf AsNew System.Windows.Media.GlyphTypeface(fontUri)
Dim fontName AsString=String.Join(" "c, gtf.FamilyNames.Values)
Dim fontFaceNames AsString=String.Join(" "c, gtf.FaceNames.Values)
Dim fullName AsString= $"{fontName} {fontFaceNames}"
Console.WriteLine(fullName)
◉ Nombre devuelto por las propiedades System.Drawing.Font.Name y System.Drawing.FontFamily.Name:
Código:
Just Breathe
◉ Nombre devuelto por las propiedades System.Drawing.Font.OriginalName y System.Drawing.Font.SystemName
Código:
NINGUNO (VALOR VACÍO EN ESTE CASO CONCRETO)
Acerca de fontreg.exe
Existe una herramienta por línea de comandos llamada "fontreg.exe" (GitHub) que funciona como un sustituto moderno —aunque ya algo anticuado— del obsoleto fontinst.exe de Microsoft Windows. Sin embargo, no la recomiendo para instalar fuentes de forma programática.
Para un usuario común, esta herramienta será más que suficiente, pero para un programador no es lo ideal por las siguientes razones:
◉ Su funcionamiento requiere que "fontreg.exe" se coloque en el mismo directorio donde se encuentran las fuentes, y al ejecutarlo instalará todas las fuentes del directorio sin permitir seleccionar una instalación de fuentes individuales. ◉ El programa no imprime mensajes de salida que sirvan para depurar la operación de instalación. ◉ No puedes saber si la fuente se instalará solo para el usuario actual (HKCU) o de manera global en el sistema (HKLM).
Además, he detectado varios fallos:
◉ En ocasiones extrae incorrectamente el nombre de la fuente, y, debido a esto, en algunos casos termina escribiendo caracteres ininteligibles en la clave de registro, ej.: "⿻⿷⿸⿹ (TrueType)", y ese es el nombre que verás al listar la fuente en tu editor de texto. ◉ Al igual que la shell de Windows al registrar el nombre de una fuente en el registro de Windows, no hace distinción entre TrueType y OpenType: siempre se añade el sufijo "(TrueType)".
Por estas razones, su uso en entornos programáticos o controlados no es ni productivo, ni confiable.
El código completo semi-completo (he tenido que eliminar mucha documentación XML ya que no me cabía en este post):
Esta función pertenece a la clase 'UtilFonts' del anterior post, lo comparto aquí por que no me cabe en el otro post y por que esta función no depende de ninguna otra...
Código
''' <summary>
''' Retrieves the resource name of a TrueType (.ttf) or OpenType font file (.otf)
''' by creating a temporary scalable font resource file and reading its contents.
''' <para></para>
''' This name may differ from the value of the following properties:
Si alguien se pregunta: "¿Y por qué esa obsesión con las diferentes formas que puede haber para obtener el nombre de una fuente?" "¿Qué más te da un nombre u otro?" pues bueno, por que yo necesitaba hallar la forma de obtener el nombre completo amistoso exactamente tal y como se muestra en el visor de fuentes de texto de Windows (fontview.exe), por que esa es la representación más completa y la más sofisticada que he visto hasta ahora, "¿Pero por qué motivo lo necesitas exactamente?" Pues por que se me metió en la cabeza conseguirlo, y yo soy muy cabezón, sin más, así que básicamente en eso ha consistido mi investigación, con varios días de ensayo y error, junto a treinta consultas a ChatGPT con sus cien respuestas inservibles que me sacan de quicio...
En el post anterior simplemente he recopilado las diferencias que he ido encontrando al probar diversas maneras de obtener el nombre de una fuente (a lo mejor me he olvidado de alguna otra forma, no sé). A penas hay información sobre esto en Internet (sobre como obtener el nombre amistoso COMPLETO) por no decir que prácticamente no hay nada de nada; aunque bueno, una forma sé que sería leyendo las tablas en la cabecera de un archivo de fuente, pero eso es un auténtico coñazo y propenso a errores humanos, sobre todo si no eres un friki erudito... diseñador de fuentes que conoce todos los entresijos y las "variables" a tener en cuenta al analizar la cabecera de estos formatos de archivo, cosa que evidentemente yo no conozco, pero por suerte al final descubrí que la propiedad "Title" de la shell de Windows es suficiente para lograr mi propósito a la perfección, y sin tener que recurrir a experimentos tediosos que me causarían pesadillas por la noche.
Lo de instalar y desinstalar fuentes vino a continuación de lo del nombre, primero necesitaba el nombre amistoso completo, y luego ya teniendo ese nombre -fiel a la representación de Microsoft Windows- podía empezar a desarrollar ideas para hacer cosas más útiles o interesantes. Todos los códigos que he visto por Internet en diferentes lenguajes de programación para instalar un archivo de fuente se quedan muuuy cortos para mis expectativas, carecíendo de las funcionalidades más esenciales, la optimización y los controles de errores más básicos... a diferencia de lo que yo he desarrollado y compartido en el anterior post, que aunque puede que no sea perfecto (por que la perfección absoluta no existe), es mejor que todo lo que he encontrado hasta ahora, y no es por echarme flores ni parecer engreído, pero es la verdad; Me siento sorprendido al no haber descubierto ningún otro programador que haya hecho/compartido un código universal para instalar fuentes de texto de forma más o menos eficiente, confiable y versátil. Quizás lo haya, pero yo no lo encontré. Códigos cortitos y que cumplen la funcionalidad mínima de "instalar una fuente" sin importar ningún factor, de esos hay muchos en Internet, pero como digo un BUEN CÓDIGO no encontré.
Lo próximo que comparta en este hilo puede que sea un método universal que sirva para determinar si un archivo de fuente contiene glifos para representar caracteres específicos (ej. "áéíóú"). Ya tengo algo hecho que funciona... pero no siempre funciona de la forma esperada (da falsos positivos con algunos archivos de fuente). Me falta mucho por aprender del formato TrueType y OpenType. Por suerte existen herramientas especializadas como por ejemplo "otfinfo.exe" (descarga) que sirven para obtener información general de una fuente, imprimir en consola los caracteres de un rango Unicode específico, volcar tablas completas y demás, y tener algo así me ayuda a hacer (y corregir) asunciones al leer este formato de archivo.
👋
« Última modificación: 31 Agosto 2025, 17:17 pm por Eleкtro »
« Respuesta #602 en: 2 Septiembre 2025, 10:12 am »
Métodos universales para trabajar (otros) aspectos básicos con fuentes de texto (.ttf y .otf)...
(AL FINAL DE ESTE POST HE COMPARTIDO UN EJEMPLO DE USO 😏)
◉ Funciones 'UtilFonts.FontHasGlyph', 'UtilFonts.FontHasGlyphs', 'FontExtensions.HasGlyph' y 'FontExtensions.HasGlyphs'
Sirven para determinar si existen glifos en una fuente de texto para un caracter o una serie de caracteres específicos.
Se utilizaría, por ejemplo, con este tipo de fuente que no tiene glifos propios para las vocales con tilde:
◉ Funciones 'UtilFonts.FontGlyphHasOutline' y 'FontExtensions.GlyphHasOutline'
Sirven para determinar si un glifo está vacío (no hay contornos dibujados).
Se utilizaría, por ejemplo, con este tipo de fuentes que no dibujan las vocales con tilde:
Tener en cuenta que esta función solo sirve para determinar si el glifo contiene algo, no puede determinar si el glifo es una figura incompleta como por ejemplo la de esta vocal que solo tiene la tilde:
◉ El código fuente
Imports necesarios
Código
Imports System.ComponentModel
Imports System.Drawing
Imports System.Drawing.Text
Imports System.IO
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
Imports DevCase.Win32
Imports DevCase.Win32.Enums
Imports DevCase.Win32.Structures
Clases secundarias requeridas
(Lo siento pero he tenido que borrar mucha documentación XML -no esencial- para que me quepa todo el código en este post.)
Código
#Region " Constants "
Namespace DevCase.Win32.Common.Constants
<HideModuleName>
Friend Module Constants
#Region " GDI32 "
''' <summary>
''' Error return value for some GDI32 functions.
''' </summary>
PublicConst GDI_ERROR As UInteger =&HFFFFFFFFUI
''' <summary>
''' Error return value for some GDI32 functions.
''' </summary>
PublicReadOnly HGDI_ERROR AsNew IntPtr(-1)
#End Region
End Module
EndNamespace
#End Region
Código
#Region " Enums "
Namespace DevCase.Win32.Enums
''' <remarks>
''' List of System Error Codes: <see href="https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes"/>.
' Dim rect As New Rectangle(0, 0, bmp.Width, bmp.Height)
' Dim bmpData As BitmapData = bmp.LockBits(rect, Imaging.ImageLockMode.ReadOnly, Imaging.PixelFormat.Format32bppArgb)
'
' Try
' Dim ptr As IntPtr = bmpData.Scan0
' Dim bytes As Integer = System.Math.Abs(bmpData.Stride) * bmp.Height
' Dim pixelValues(bytes - 1) As Byte
' Marshal.Copy(ptr, pixelValues, 0, bytes)
'
' ' Iterate through each pixel.
' ' PixelFormat.Format32bppArgb stores pixels as [Blue][Green][Red][Alpha]
' ' i=Blue, i+1=Green, i+2=Red, i+3=Alpha
' For i As Integer = 0 To pixelValues.Length - 1 Step 4
' Dim red As Byte = pixelValues(i + 2)
'
' ' Check if the pixel is darker than nearly-white (threshold 250)
' ' If so, we found a visible pixel, meaning the glyph is drawn.
' If red < 250 Then
' Return True
' End If
' Next
' Finally
' bmp.UnlockBits(bmpData)
'
' End Try
'End Using
'
'' No visible pixels found, meaning the glyph is empty/unsupported by the font.
'Return False
EndFunction
End Module
◉ Modo de empleo
El siguiente ejemplo verifica en los archivos de fuente .ttf de un directorio específico si la tipografía incluye los glifos correspondientes a los caracteres á, é, í, ó y ú. En caso de que falte algún glifo, se imprime un mensaje en consola indicando los glifos ausentes, y finalmente envía el archivo de fuente a la papelera de reciclaje (hay que descomentar las lineas marcadas).
Código
Dim fontFiles As IEnumerable(OfString)=Directory.EnumerateFiles("C:\Fonts", "*.ttf", SearchOption.TopDirectoryOnly)
Dim fontsToDelete AsNew HashSet(OfString)()
Dim chars As Char()="áéíóú".ToCharArray()
ForEach fontFile AsString In fontFiles
Dim missingChars AsNew HashSet(Of Char)()
ForEach ch As Char In chars
IfNot UtilFonts.FontHasGlyph(fontFile, ch) OrElse
Not UtilFonts.FontGlyphHasOutline(fontFile, ch)Then
Por último, quiero comentar que he experimentado estas funciones de forma muy minuciosa, primero con muestras pequeñas de 2 o 3 fuentes... varias veces por cada cambio significativo realizado en el código, y después he probado la versión final con aprox. 14.000 archivos de fuentes de texto, y los resultados han sido muy satisfactorios detectando varios miles de fuentes a los que le faltan los glifos especificados, y, aunque no he podido revisar todos esos miles de fuentes una a una, no he encontrado ningún falso positivo entre varios cientos de fuentes que sí he revisado manualmente.
Eso es todo. 👋
« Última modificación: 3 Septiembre 2025, 01:36 am por Eleкtro »
« Respuesta #603 en: 3 Septiembre 2025, 01:34 am »
Métodos universales para trabajar (los últimos) aspectos básicos con fuentes de texto (.ttf y .otf)...
◉ Funciones 'UtilFonts.GetFontGlyphOutlineData' y 'FontExtensions.GetGlyphOutlineData'
Sirven para obtener los datos crudos de contorno (outline) de un glifo para un carácter específico en una fuente.
Devuelven un array de bytes que representa la forma vectorial del glifo en el formato solicitado (Native o Bezier).
Estos datos se pueden usar como base para comparaciones de glifos.
◉ Funciones 'UtilFonts.FontGlyphOutlinesAreEqual' y 'FontExtensions.GlyphOutlinesAreEqual'
Sirven para comparar si dos fuentes producen los mismos datos de contorno (outline) de un glifo para un carácter específico.
◉ Funciones 'UtilFonts.GetFontGlyphOutlineSimilarity' y 'FontExtensions.GetGlyphOutlineSimilarity'
Sirven para calcular un índice de similitud entre los contornos de un glifo para un carácter específico en dos fuentes distintas.
Se puede usar cuando se quiere medir cuán parecidos son los glifos entre dos fuentes, en lugar de solo saber si son exactamente iguales.
◉ El código fuente
⚠️ Importante: Para poder utilizar este código se requieren algunas definiciones de la API de Windows que he compartido en el post anterior a este. No lo comparto aquí de nuevo para evitar repetir código y evitar que este post quede demasiado grande y tedioso de leer. 🙏
Código
PublicClass UtilFonts
''' <summary>
''' Prevents a default instance of the <see cref="UtilFonts"/> class from being created.
''' </summary>
PrivateSubNew()
EndSub
''' <summary>
''' Retrieves the raw outline data for a given glyph from the specified font file.
''' <para></para>
''' This function calls <see cref="DevCase.Win32.NativeMethods.GetGlyphOutline"/> in background
''' to retrieve outline data with the requested <paramref name="format"/>.
''' </summary>
'''
''' <param name="fontFile">
''' Path to the font file from which the glyph will be obtained.
''' </param>
'''
''' <param name="ch">
''' The character whose glyph outline will be requested.
''' </param>
'''
''' <param name="format">
''' The format in which the glyph outline will be retrieved.
''' <para></para>
''' This value only can be <see cref="GetGlyphOutlineFormat.Native"/> or <see cref="GetGlyphOutlineFormat.Bezier"/>.
''' <para></para>
''' Note: callers must interpret the returned byte array based on the selected format.
''' </param>
'''
''' <param name="matrix">
''' An optional <see cref="GlyphOutlineMatrix2"/> used to transform the glyph outline.
''' <para></para>
''' If no value is provided or default structure is passed, an identity matrix
''' will be used (see: <see cref="GlyphOutlineMatrix2.GetIdentityMatrix()"/>),
''' where the transfromed graphical object is identical to the source object.
''' </param>
'''
''' <returns>
''' A <see cref="Byte"/> array containing the raw glyph outline data with the requested <paramref name="format"/>.
''' <para></para>
''' Returns <see langword="Nothing"/> if the glyph is empty in the specified font.
''' </returns>
'''
''' <exception cref="FileNotFoundException">
''' Thrown when the font file is not found.
''' </exception>
<DebuggerStepThrough>
PublicSharedFunction GetFontGlyphOutlineData(fontFile AsString, ch As Char, formatAs GetGlyphOutlineFormat,
Optional matrix As GlyphOutlineMatrix2 =Nothing)AsByte()
« Respuesta #604 en: 21 Septiembre 2025, 12:19 pm »
Métodos universales para demostrar la vulnerabilidad de validación de firmas en WinVerifyTrust.
— Cómo ocultary ejecutarmalware desde un ejecutable firmado digitalmente —
Recientemente, descubrí el siguiente artículo sobre la vulnerabilidad CVE-2013-3900, conocida como la "Vulnerabilidad de validación de firmas en WinVerifyTrust":
Esta vulnerabilidad afecta a la función WinVerifyTrust de la API de Windows responsable de verificar la autenticidad de las firmas digitales en archivos (exe, dll, etc), y consiste en la capacidad de un atacante para poder modificar un archivo ejecutable firmado, adjuntando código malicioso en la tabla de certificado ¡sin invalidar la firma digital del archivo!, lo que proporciona una técnica de ocultación muy discreta.
La vulnerabilidad se dio a conocer en el año 2013, pero sigue vigente en 2025 (también en Windows 11. De hecho, con más agravio que en versiones anteriores de Windows), y ha sido la forma de ataque a empresas en varias ocasiones (👉 10-Year-Old Windows Vulnerability Exploited in 3CX Attack)
Prueba de indetectabilidad
Vaya por delante que todo esto lo hago con fines educativos. No soy ningún experto en malware, y no experimento con ello. Pero intentaré aportar lo que pueda:
Para ilustrar brevemente la efectividad de esta vulnerabilidad en 2025, podemos usar como ejemplo el EICAR, un archivo de prueba diseñado para evaluar y verificar el funcionamiento del software antivirus. Se trata de un virus simulado que provoca la reacción del motor antivirus, permitiendo demostrar su capacidad para detectar y neutralizar posibles amenazas.
👇 Este es el diagnóstico de VirusTotal de una simple aplicación de consola desarrollada en .NET 4.8, que contiene la representación literal en bytes del archivo eicar_com.zip:
Y, por último, 👇 este es el diagnóstico de VirusTotal de un archivo ejecutable firmado, en este caso el propio y legítimo explorer.exe con certificado digital de Microsoft, al que le he adjuntado la aplicación de consola anterior — cifrada con el packer Enigma — al final de la tabla de certificado:
Y lo mejor de todo, aparte de la reducción en detecciones, es que la firma no se ha invalidado, por lo que a ojos del sistema operativo sigue siendo un archivo legítimo y totalmente confiable 👍:
Cabe mencionar que si solamente adjuntásemos un archivo PE malicioso y sin cifrar a la tabla de certificado, habría muchas detecciones de AVs, y Windows nos advertiría de que la firma no tiene un formato adecuado:
Por lo que yo he experimentado, esta advertencia al examinar la firma digital de un archivo solo se produce al adjuntar archivos PE y sin cifrar a la tabla de certificado. Podemos adjuntar cualquier tipo de documento de texto plano, imágenes y videos, que estén sin cifrar, y Windows no mostrará ningún aviso sobre formato incorrecto.
Por que sí, amigos, aunque esto sería un método descubierto y usado principalmente para ocultar malware, también podríamos darle un uso más didáctico y de utilidad para un mayor número de usuarios, como podría ser la capacidad de ocultar documentos o contraseñas de forma segura donde nadie jamás va a ponerse a mirar: en la tabla de certificado de un archivo PE.
Para un archivo con un certificado corrupto, Windows puede mostrar esto:
Y para un archivo con un certificado digital inválido, Windows muestra este mensaje:
(Esa captura de pantalla la he sacado de Internet y la he editado, sí, pero creanme, he invalidado el certificado varias veces y ponía algo así, "El certificado no es válido.")
Sin más dilación, vamos con el código que he desarrollado...
Características principales del código
Estas son las principales funciones que he desarrollado:
◉ AppendBlobToPECertificateTable: Añade un bloque de datos al final de la tabla de certificado de un archivo PE. ◉ RemoveBlobFromPECertificateTable: Elimina un bloque de datos específico de la tabla de certificado de un archivo PE. ◉ RemoveBlobsFromPECertificateTable: Elimina todos los bloques de datos de la tabla de certificado de un archivo PE. ◉ GetBlobsFromPECertificateTable: Devuelve una colección con todos los bloques de datos presentes en la tabla de certificado de un archivo PE.
Además, también he incluído las siguientes funciones auxiliares de utilidad general:
◉ FileIsPortableExecutable: Determina si un archivo es de facto un archivo PE válido. ◉ FileHasCertificateTable: Determina si un archivo PE contiene una tabla de certificado que no esté vacía. No valida la firma ni el contenido de los certificados; solo verifica la presencia de la tabla. ◉ FileHasCertificate: Determina si un archivo PE contiene un certificado válido que se pueda leer/parsear. No valida la cadena de confianza, expiración ni revocación del certificado. ◉ MarshalExtensions.ConvertToStructure y MarshalExtensions.ConvertToBytes ◉ StreamExtensions.ReadExact y StreamExtensions.CopyExactTo
💡 Al final de este hilo muestro un breve ejemplo de uso para todas las funciones principales 👍
Dim msg AsString= $"Requested {count} bytes, but only {stream.Length - stream.Position} bytes are available from the current position in the source stream."
« Respuesta #605 en: 21 Septiembre 2025, 12:28 pm »
Clase PortableExecutableUtil (1ª PARTE):
Nota: Para que me cupiera el código en este post, he tenido que eliminar TODA la documentación XML en torno a las excepciones de cada método, además de los códigos de ejemplo que había embedidos en la documentación (de todas formas en el siguiente post muestro ejemplos de uso). Disculpas. 🙏
Código
''' <summary>
''' Utility class for working with Portable Executable (PE) files.
''' </summary>
Partial PublicClass PortableExecutableUtil
PrivateSubNew()
EndSub
''' <summary>
''' Appends an arbitrary data blob to the Certificate Table data-directory entry
''' in the Portable Executable (PE) header of the given file.
''' </summary>
'''
''' <param name="inputFilePath">
''' Path to the input —digitally signed— Portable Executable (PE) file (e.g., "C:\Windows\explorer.exe").
''' </param>
'''
''' <param name="outputFilePath">
''' Path to the output file that will be written with the modified Certificate Table.
''' <para></para>
''' Cannot be the same as <paramref name="inputFilePath"/>.
''' </param>
'''
''' <param name="blob">
''' A <see cref="Byte()"/> array containing the arbitrary data blob to append into the certificate table.
''' </param>
'''
''' <param name="markerBegin">
''' Optional. A byte sequence used to mark the beginning of the data blob within the Certificate Table content.
''' <para></para>
''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_BEGIN#</c>" in UTF-8 encoding bytes.
''' <para></para>
''' It is strongly recommended to use a unique and long enough byte pattern
''' to avoid accidental conflicts when identifying/extracting the appended blob.
''' </param>
'''
''' <param name="markerEnd">
''' Optional. A byte sequence used to mark the end of the data blob within the Certificate Table content.
''' <para></para>
''' Cannot be null or empty. Default value is "<c>#CERT_BLOB_END#</c>" in UTF-8 encoding bytes.
''' <para></para>
''' It is strongly recommended to use a unique and long enough byte pattern
''' to avoid accidental conflicts when identifying/extracting the appended blob.
''' </param>
'''
''' <param name="throwIfInvalidCertSize">
''' Optional. Determines whether to allow appending data that will cause to exceed the maximum allowed certificate table size (~100 MB).
''' <para></para>
''' If set to <see langword="True"/>, the method will throw an <see cref="InvalidOperationException"/>
''' if the appended data would cause the certificate table size to exceed the maximum allowed limit,
''' preventing digital signature invalidation.
''' <para></para>
''' If set to <see langword="False"/>, the certificate table size limit can be exceeded (up to ~2 GB) when appending data,
''' but the digital signature will become invalid, as the operating system will
''' not recognize a certificate table greater than the maximum allowed size.
''' Use it at your own risk.
''' <para></para>
''' Default value is <see langword="True"/>.
''' </param>
'''
''' <param name="overwriteOutputFile">
''' If <see langword="False"/> and the output file already exists, the method throws an <see cref="IOException"/>.
''' <para></para>
''' If <see langword="True"/>, any existing output file will be overwritten.
◉ Ejemplo para adjuntar un blob de datos arbitrarios (en este caso solo le adjuntamos el contenido de un archivo de texto plano cualquiera) a un archivo PE. El archivo modificado se escribe en una nueva ubicación.
Recordatorio: el archivo PE original que vayamos a modificar, debe contener un certificado digital.
Código
Dim inputFilePath AsString="C:\original_executable.exe"
Dim outputFilePath AsString="modified_executable.exe"
Dim fileToAppend AsNew FileInfo("C:\My Secret Document.txt")
Dim markerBegin AsByte()= Encoding.UTF8.GetBytes("#CERT_BLOB_BEGIN#")
Dim markerEnd AsByte()= Encoding.UTF8.GetBytes("#CERT_BLOB_END#")
Using blobStream As FileStream = fileToAppend.OpenRead()
◉ Ejemplo para obtener todos los blobs de datos que hayan sido adjuntados en un archivo PE, y seguidamente volcar el contenido del primer blob al disco.
Código
Dim inputFilePath AsString="modified_executable.exe"
Dim markerBegin AsByte()= Encoding.UTF8.GetBytes("#CERT_BLOB_BEGIN#")
Dim markerEnd AsByte()= Encoding.UTF8.GetBytes("#CERT_BLOB_END#")
Dim extractedBlobs As ImmutableArray(Of ArraySegment(OfByte))=
Como habrán podido comprobar, la forma de uso es sencillísima. Además, me gustaría mencionar que el código ha sido desarrollado siendo consciente de la eficiencia del uso de memoria, especialmente en el método AppendBlobToPECertificateTable.
Para probar estos ejemplos pueden usar mismamente el archivo explorer.exe de Windows, que está firmado digitalmente por Microsoft.
También quisiera resaltar otras cuestiones relacionadas y más técnicas a tener en cuenta:
◉ El layout de cada bloque de datos adjunto, es el siguiente:
Verde: Datos originales de la tabla de certificado del PE. Rojo: Marcador de inicio y marcador final del bloque de datos adjunto. Azul: Datos de la estructura CertBlobMeta. Rosa: Datos actuales adjuntados. Amarillo: Padding añadido para ajustar el alineamiento de la tabla de certificado.
Dicho de otra forma: [MarkerBegin][CertBlobMeta][Blob][MarkerEnd][Padding]
◉ La estructura CertBlobMeta almacena "metadatos" que permiten identificar de manera óptima y eficiente el tamaño del blob y cualquier padding que se haya añadido. Esta estructura puede ampliarse para incluir otros datos que necesites, sin que el resto del código requiera modificaciones (siempre y cuando cada campo tenga un tamaño fijo en bytes).
◉ El tamaño máximo para una tabla de certificado válida, es de aproximadamente 100 MB. No he encontrado el valor exacto en la SDK de Windows, sin embargo, bajo un proceso de ensayo y error he llegado a la conclusión de que el límite, al menos en Windows 10, es de '102400000 + 8' bytes (97,65438 MiB). Si la tabla supera este tamaño, el sistema operativo no reconocerá el certificado. La firma digital no se invalidará, simplemente no se reconocerá / no se parseará correctamente. Mi código maneja este límite y -opcionalmente- puede lanzar una excepción para evitar exceder dicho límite.
◉ El código permite trabajar con aproximadamente un búfer de 2 GB de tamaño, dependiendo de la memoria disponible en el sistema, y del tamaño actual del archivo PE y de su tabla de certificado. Sin embargo, y como ya he explicado, una tabla de certificado mayor de ~100 MB quedará irreconocible para el sistema operativo, pero si eso no te supone un inconveniente, pues adelante. Por si sirve de algo, he adjuntado archivos pesados de aprox. 1,80 GB a la tabla de certificado, y el ejecutable modificado ha seguido funcionando correctamente:
◉ Si adjuntamos uno o más bloques de datos a la tabla de certificado de un archivo PE con el método AppendBlobToPECertificateTable, y luego eliminamos todos los bloques adjuntados con el método RemoveBlobFromPECertificateTable / RemoveBlobsFromPECertificateTable, el archivo restaurado será idéntico (byte a byte) al original antes de haberle efectuado ninguna modificación. 👍
◉ Mientras hacía pruebas, me he topado con software comercial de terceros que parecen hacer sus propias comprobaciones de integridad del archivo ejecutable, por lo que al modificar el PE, dan un error genérico al intentar iniciar el programa. En conclusión, hay que verificar que el archivo ejecutable modificado funcione correctamente, sin asumir nada.
Por último, un par de cuestiones a aclarar:
◉ La decisión de haber enfocado la lógica del código en escribir las modificaciones del PE en un nuevo archivo en vez de sobreescribir el archivo actual, ha sido una decisión personal, y no tengo intención de cambiar ese aspecto ya que lo considero una medida de seguridad muy importante.
◉ Los tamaños de los búferes para los FileStream han sido totalmente arbitrarios, se pueden cambiar. Actualmente en mi código de producción los búferes se ajustan de forma dinámica en base a ciertos factores específicos, pero eso no lo puedo mostrar aquí.
◉ En torno a la ejecución reflexiva de código que se abarca en el artículo que compartí de 'DeepInstinct - black hat USA 2016', no es mi intención profundizar en el tema y mostrar ningún ejemplo, pero si a alguien le interesa peudo decirle que en .NET es muy sencillo, siempre y cuando la intención sea ejecutar ensamblados .NET; Basta con realizar una llamada al método System.Reflection.Assembly.Load() para cargar un ensamblado .NET en memoria, y luego simplemente invocar el punto de entrada (entry point) del programa. Un ejemplo rápido:
Código
Dim assemblyBytes AsByte()=File.ReadAllBytes("MyAssembly.exe")
Dim asm AsAssembly=Assembly.Load(assemblyBytes)
Dim entry As MethodInfo = asm.EntryPoint
If entry.GetParameters().Length=0Then
entry.Invoke(Nothing, Nothing)
Else
entry.Invoke(Nothing, NewObject(){...ARGUMENTOS DE INVOCACIÓN...})
EndIf
⚠️Aunque considero haber probado lo suficiente todo el código que he compartido, yo también soy humano y puedo cometer algún que otro error o despiste, así que no me hago responsable de posibles daños causados al intentar modificar un archivo. Hagan siempre una copia de seguridad antes de modificar un archivo. 👍
Y hasta aquí, esto sería todo. 👋
« Última modificación: 21 Septiembre 2025, 16:38 pm por Eleкtro »
Lights Out (o Flip Tiles) es un juego de puzles electrónico que consiste en una cuadrícula de cuadrados iluminados (a veces con bombillas o interruptores) o numerados (originalmente de 5x5). El objetivo es apagar todas las luces de la cuadrícula.
Al principio del juego, se ilumina un patrón de cuadrados (en diferentes estados). Al pulsar uno de los cuadrados, se activa un interruptor, cambiando su estado (de encendido a apagado, de apagado a encendido o de color), al igual que los cuatro cuadrados adyacentes (superiores, derecho, inferior e izquierdo).
El objetivo del juego es apagar (o encender) todas las luces, preferiblemente pulsando el menor número de cuadrados posible.
Este tipo de rompecabezas se puede encontrar en diversas formas pasando desapercibido en muchos video juegos actuales...
La siguiente imagen muestra un rompecabezas tipo 'Lights Out' en el survival horror 'Tormented Souls 2', donde hay un panel/grilla cuadrada de 3x3 cuyas piezas/celdas tienen dos posiciones: vertical y horizontal, y hay que ponerlas todas en vertical para solucionar el rompecabezas:
La siguiente clase contiene:
- La función principal para resolver un rompecabezas. - Una función auxiliar para formatear visualmente un rompecabezas. - Una función auxiliar para formatear textualmente los pasos a seguir para resolver un rompecabezas.
''' <remarks> ''' Provides utility methods related with 'Lights Out' puzzles. ''' <para></para> ''' Lights Out (or Flip Tiles) is an electronic puzzle game consisting of a grid of illuminated ''' (sometimes with light bulbs or switches) or numbered (originally 5x5) squares. ''' The goal is to turn off all the lights in the grid. ''' <para></para> ''' At the start of the game, a pattern of squares is illuminated (in different states). ''' Pressing one of the squares activates a switch, changing its state ''' (from on to off, from off to on, or to a different color), ''' as well as the four adjacent squares (up, right, down, and left). ''' <para></para> ''' The goal of the game is to turn off (or on) all the lights, ''' preferably by pressing as few squares as possible. ''' </remarks> Public NotInheritable Class UtilLightsOut
#Region " Constructors "
''' <summary> ''' Prevents a default instance of the <see cref="UtilLightsOut"/> class from being created. ''' </summary> Private Sub New() End Sub
#End Region
#Region " Public Methods "
''' <summary> ''' Solves a 'Lights Out' puzzle board. ''' <para></para> ''' However, note that not all board configurations have a solution. ''' If all rows have a solution except the last one—and this option can be disabled ''' by clicking only on the last row—the configuration has a solution; ''' otherwise, it is unsolvable. ''' <para></para> ''' Mathematically, solvability depends on the image of the matrix "A". ''' <para></para> ''' This is a visual representation of an unsolvable 3x3 board: ''' <para></para>◼◻◻ 100 ''' <para></para>◻◼◻ 010 ''' <para></para>◻◻◼ 001 ''' </summary> ''' ''' <param name="board"> ''' A two-dimensional <see cref="Byte(,)"/> array representing the initial configuration of the 'Lights Out' board grid. ''' <para></para> ''' Each element corresponds to a cell, where a value of zero indicates the cell is "off", ''' and non-zero indicates the cell is "on". ''' <para></para> ''' For example: ''' <code> ''' Dim board5x5 As Byte(,) = { ''' {1, 1, 0, 1, 1}, ' ◼ ◼ ◻ ◼ ◼ ''' {1, 1, 0, 0, 0}, ' ◼ ◼ ◻ ◻ ◻ ''' {1, 0, 0, 1, 1}, ' ◼ ◻ ◻ ◼ ◼ ''' {0, 1, 1, 0, 0}, ' ◻ ◼ ◼ ◻ ◻ ''' {0, 0, 1, 0, 1} ' ◻ ◻ ◼ ◻ ◼ ''' } ''' </code> ''' </param> ''' ''' <returns> ''' A two-dimensional <see cref="Byte(,)"/> array of the same dimensions as the input board. ''' <para></para> ''' Each element indicates whether the corresponding cell should be pressed to solve the puzzle: ''' 0 means the cell does not need to be pressed, 1 means it should be pressed once. ''' <para></para> ''' The order of pressing does not matter to solve the puzzle. ''' <para></para> ''' Example: ''' <code> ''' ' Input board (1 = on, 0 = off) ''' Dim board5x5 As Byte(,) = { ''' {1, 1, 0, 1, 1}, ' ◼ ◼ ◻ ◼ ◼ ''' {1, 1, 0, 0, 0}, ' ◼ ◼ ◻ ◻ ◻ ''' {1, 0, 0, 1, 1}, ' ◼ ◻ ◻ ◼ ◼ ''' {0, 1, 1, 0, 0}, ' ◻ ◼ ◼ ◻ ◻ ''' {0, 0, 1, 0, 1} ' ◻ ◻ ◼ ◻ ◼ ''' } ''' ''' ' Solution returned by this function ''' Dim solution5x5 As Byte(,) = { ''' {0, 1, 1, 1, 0}, ' ◻ ◼ ◼ ◼ ◻ ''' {0, 1, 1, 1, 0}, ' ◻ ◼ ◼ ◼ ◻ ''' {0, 0, 0, 1, 1}, ' ◻ ◻ ◻ ◼ ◼ ''' {1, 1, 0, 0, 1}, ' ◼ ◼ ◻ ◻ ◼ ''' {0, 1, 0, 0, 0} ' ◻ ◼ ◻ ◻ ◻ ''' } ' ◼ = press once, ''' ' ◻ = do not press ''' </code> ''' </returns> <DebuggerStepperBoundary> Public Shared Function SolveLightsOutPuzzle(board As Byte(,)) As Byte(,)
' Get board dimensions Dim rows As Integer = board.GetLength(0) Dim cols As Integer = board.GetLength(1) Dim cells As Integer = rows * cols ' total number of cells.
' --- 1) Build the influence matrix "A". ' Each row represents the effect of pressing a single cell: ' toggling itself and its immediate neighbors (up, down, left, right). Dim A As BitArray() = Enumerable.Range(0, cells).Select( Function(i) Dim r As Integer = i \ cols Dim c As Integer = i Mod cols Dim row As New BitArray(cells)
' Pressing the cell toggles itself. row(i) = True
' Define neighbor offsets: up, down, left, right. Dim dr As Integer() = {-1, 1, 0, 0} Dim dc As Integer() = {0, 0, -1, 1}
' Toggle neighbors if are inside board bounds. For k As Integer = 0 To 3 Dim nr As Integer = r + dr(k) Dim nc As Integer = c + dc(k) If nr >= 0 AndAlso nr < rows AndAlso nc >= 0 AndAlso nc < cols Then row(nr * cols + nc) = True End If Next k
Return row End Function).ToArray()
' --- 2) Build the initial state vector "b". ' b(i) = True if the cell is "on" (non-zero), False if "off" (0). Dim b As New BitArray(cells) For r As Integer = 0 To rows - 1 For c As Integer = 0 To cols - 1 b(r * cols + c) = (board(r, c) <> 0) Next c Next r
' --- 3) Forward elimination (Gaussian elimination mod 2). ' Solve the linear system: "A * x = b" (mod 2). For i As Integer = 0 To cells - 1 ' If diagonal is zero, try to swap with a row below. If Not A(i)(i) Then For k As Integer = i + 1 To cells - 1 If A(k)(i) Then A(i).Xor(A(k)) ' XOR rows (addition mod 2). b(i) = b(i) Xor b(k) ' XOR corresponding element in "b". Exit For End If Next End If
' Eliminate current column for all rows below. For j As Integer = i + 1 To cells - 1 If A(j)(i) Then A(j).Xor(A(i)) b(j) = b(j) Xor b(i) End If Next j Next i
' --- 4) Back substitution. ' Solve for x starting from bottom row. Dim x As New BitArray(cells) For i As Integer = cells - 1 To 0 Step -1 ' Start with right-hand side value. Dim sum As Boolean = b(i)
' Subtract contributions from already solved variables (XOR is subtraction mod 2). For j As Integer = i + 1 To cells - 1 sum = sum Xor (A(i)(j) AndAlso x(j)) Next
' Assign solution for cell "i". x(i) = sum Next
' --- 5) Build result board (Integer(,)) of 1-based indices to press. Dim result(rows - 1, cols - 1) As Byte For i As Integer = 0 To cells - 1 Dim r As Byte = CByte(i \ cols) Dim c As Byte = CByte(i Mod cols) result(r, c) = CByte(If(x(i), 1, 0)) ' 1 = press, 0 = don't press. Next
Return result End Function
''' <summary> ''' Returns a visual representation of a 'Lights Out' board using custom characters for ON and OFF states. ''' </summary> ''' ''' <param name="board"> ''' A two-dimensional <see cref="Byte"/> array representing the 'Lights Out' board grid. ''' <para></para> ''' Each element corresponds to a cell, where a value of zero indicates the cell is "off", ''' and non-zero indicates the cell is "on". ''' <para></para> ''' For example: ''' <code> ''' Dim board5x5 As Byte(,) = { ''' {1, 1, 0, 1, 1}, ' ◼ ◼ ◻ ◼ ◼ ''' {1, 1, 0, 0, 0}, ' ◼ ◼ ◻ ◻ ◻ ''' {1, 0, 0, 1, 1}, ' ◼ ◻ ◻ ◼ ◼ ''' {0, 1, 1, 0, 0}, ' ◻ ◼ ◼ ◻ ◻ ''' {0, 0, 1, 0, 1} ' ◻ ◻ ◼ ◻ ◼ ''' } ''' </code> ''' </param> ''' ''' <param name="onChar"> ''' Optional.The character used to represent an ON (active) light. ''' <para></para> ''' Default is "◼" ''' </param> ''' ''' <param name="offChar"> ''' Optional. The character used to represent an OFF (inactive) light. ''' <para></para> ''' Default is "◻" ''' </param> ''' ''' <returns> ''' A formatted <see cref="String"/> that visually represents the current 'Lights Out' board. ''' <para></para> ''' Example: ''' <code> ''' ' Input board (1 = on, 0 = off) ''' Dim board5x5 As Byte(,) = { ''' {1, 1, 0, 1, 1}, ''' {1, 1, 0, 0, 0}, ''' {1, 0, 0, 1, 1}, ''' {0, 1, 1, 0, 0}, ''' {0, 0, 1, 0, 1} ''' } ''' ''' Format returned (◼ = on, ◻ = off): ''' ''' ◼ ◼ ◻ ◼ ◼ ''' ◼ ◼ ◻ ◻ ◻ ''' ◼ ◻ ◻ ◼ ◼ ''' ◻ ◼ ◼ ◻ ◻ ''' ◻ ◻ ◼ ◻ ◼ ''' </code> ''' </returns> <DebuggerStepperBoundary> Public Shared Function FormatLightsOutBoard(board As Byte(,), Optional onChar As Char = "◼"c, Optional offChar As Char = "◻"c) As String
Dim rows As Integer = board.GetLength(0) Dim cols As Integer = board.GetLength(1) Dim maxCapacity As Integer = (rows * (cols * 2)) + rows ' (cells + white-spaces + line breaks)
Dim sb As New StringBuilder(capacity:=maxCapacity, maxCapacity:=maxCapacity)
For r As Integer = 0 To rows - 1 For c As Integer = 0 To cols - 1 sb.Append(If(board(r, c) <> 0, onChar, offChar)) If c < (cols - 1) Then sb.Append(" "c) End If Next If r < (rows - 1) Then sb.AppendLine() End If Next
Return sb.ToString() End Function
''' <summary> ''' Returns a human-readable list of steps required to solve a 'Lights Out' puzzle board. ''' </summary> ''' ''' <param name="board"> ''' A two-dimensional <see cref="Byte"/> array representing the 'Lights Out' solution grid. ''' <para></para> ''' Each non-zero cell indicates that the corresponding cell should be pressed once. ''' <para></para> ''' For example: ''' <code> ''' Dim solution5x5 As Byte(,) = { ''' {0, 1, 1, 1, 0}, ' ◻ ◼ ◼ ◼ ◻ ''' {0, 1, 1, 1, 0}, ' ◻ ◼ ◼ ◼ ◻ ''' {0, 0, 0, 1, 1}, ' ◻ ◻ ◻ ◼ ◼ ''' {1, 1, 0, 0, 1}, ' ◼ ◼ ◻ ◻ ◼ ''' {0, 1, 0, 0, 0} ' ◻ ◼ ◻ ◻ ◻ ''' } ' 1/◼ = press once, ''' ' 0/◻ = do not press ''' </code> ''' </param> ''' ''' <returns> ''' A formatted <see cref="String"/> describing which cells to press, grouped by row. ''' <para></para> ''' Includes a total count of all required presses. ''' <para></para> ''' Example: ''' <code> ''' ' Input solution board. ''' Dim solution5x5 As Byte(,) = { ''' {0, 1, 1, 1, 0}, ' ◻ ◼ ◼ ◼ ◻ ''' {0, 1, 1, 1, 0}, ' ◻ ◼ ◼ ◼ ◻ ''' {0, 0, 0, 1, 1}, ' ◻ ◻ ◻ ◼ ◼ ''' {1, 1, 0, 0, 1}, ' ◼ ◼ ◻ ◻ ◼ ''' {0, 1, 0, 0, 0} ' ◻ ◼ ◻ ◻ ◻ ''' } ' 1/◼ = press once, ''' ' 0/◻ = do not press ''' ''' ' Example output: ''' Row 1, Cells: 2, 3, 4 ''' Row 2, Cells: 2, 3, 4 ''' Row 3, Cells: 4, 5 ''' Row 4, Cells: 1, 2, 5 ''' Row 5, Cell: 2 ''' ''' Total presses: 12 ''' </code> ''' </returns> <DebuggerStepperBoundary> Public Shared Function FormatLightsOutSolutionSteps(board As Byte(,)) As String
Dim rows As Integer = board.GetLength(0) Dim cols As Integer = board.GetLength(1)
Dim sb As New StringBuilder() Dim totalPresses As Integer = 0
For r As Integer = 0 To rows - 1 Dim pressedCells As New List(Of Integer)
For c As Integer = 0 To cols - 1 If board(r, c) <> 0 Then pressedCells.Add(c + 1) totalPresses += 1 End If Next
If pressedCells.Count > 0 Then sb.AppendLine($"Row {r + 1}, Cells: {String.Join(", ", pressedCells)}") End If Next
If totalPresses = 0 Then sb.AppendLine("No presses needed — all lights are already off.") Else sb.AppendLine().AppendLine($"Total presses: {totalPresses}") End If
Return sb.ToString() End Function
#End Region
End Class
End Namespace
#End Region
Este código funciona con rompecabezas tradicionales Lights Out que utilizan dos estados binarios: encendido (on) y apagado (off). Tanto cuadrados, como rectangulares.
PD: La implementación matemática subyacente fue generada en su totalidad por ChatGPT.
Dim solvedBoard As Byte(,) = UtilLightsOut.SolveLightsOutPuzzle(board5x5)
Dim formattedSolution As String = UtilLightsOut.FormatLightsOutBoard(solvedBoard) formattedSolution &= String.Concat(Enumerable.Repeat(Environment.NewLine, 2)) & "◼ = Press once" & Environment.NewLine & "◻ = Do not press"
MessageBox.Show(formattedSolution)
Cita de: MessageBox.Show(formattedSolution)
O con un formato más textual, coherente con el estilo técnico de un informe de solución:
Código:
... Dim solvedBoard As Byte(,) = UtilLightsOut.SolveLightsOutPuzzle(board5x5) Dim formattedSolution As String = UtilLightsOut.FormatLightsOutSolutionSteps(solvedBoard)
He actualizado una antigua función que creo haber compartido en el pasado, que toma un valor numérico y lo devuelve como un string hexadecimal con formato específico de VB.NET o C#, opcionalmente con sufijo de número literal y pudiendo elegir entre formato en mayúsculas y minsúculas.
Código
''' <summary>
''' Specifies a .NET document language.
''' </summary>
PublicEnum NetDocumentLanguage AsInteger
''' <summary>
''' C# language.
''' </summary>
CSharp =0
''' <summary>
''' Visual Basic.NET language.
''' </summary>
VbNet =1
EndEnum
Código
''' <summary>
''' Converts a numeric value of type <typeparamref name="T"/> to its .NET hexadecimal string representation.
''' </summary>
'''
''' <example> This is a code example.
''' <code language="VB">
''' Dim syntax As NetDocumentLanguage = NetDocumentLanguage.VbNet
''' Dim addLiteralSuffix As Boolean = True
''' Dim lowerCase As Boolean = False
'''
''' Dim valueByte As Byte = Byte.MaxValue
''' Dim valueSByte As SByte = SByte.MaxValue
''' Dim valueShort As Short = Short.MaxValue
''' Dim valueUShort As UShort = UShort.MaxValue
''' Dim valueInteger As Integer = Integer.MaxValue
''' Dim valueUInteger As UInteger = UInteger.MaxValue
''' Dim valueLong As Long = Long.MaxValue
''' Dim valueULong As ULong = ULong.MaxValue
'''
''' Dim valueByteHex As String = ConvertNumberToHexadecimal(valueByte, syntax, addLiteralSuffix, lowerCase)
''' Dim valueSByteHex As String = ConvertNumberToHexadecimal(valueSByte, syntax, addLiteralSuffix, lowerCase)
''' Dim valueShortHex As String = ConvertNumberToHexadecimal(valueShort, syntax, addLiteralSuffix, lowerCase)
''' Dim valueUShortHex As String = ConvertNumberToHexadecimal(valueUShort, syntax, addLiteralSuffix, lowerCase)
''' Dim valueIntegerHex As String = ConvertNumberToHexadecimal(valueInteger, syntax, addLiteralSuffix, lowerCase)
''' Dim valueUIntegerHex As String = ConvertNumberToHexadecimal(valueUInteger, syntax, addLiteralSuffix, lowerCase)
''' Dim valueLongHex As String = ConvertNumberToHexadecimal(valueLong, syntax, addLiteralSuffix, lowerCase)
''' Dim valueULongHex As String = ConvertNumberToHexadecimal(valueULong, syntax, addLiteralSuffix, lowerCase)