DEV Community

Cover image for Linux ELF Binary Canary PoC
fx2301
fx2301

Posted on

Linux ELF Binary Canary PoC

Why?

You want to know when your defenses have failed and an attacker is running commands in your environment.

When?

You are defending a Linux host which has egress to the internet. You can hook into provisioning infrastructure automation to instrument the host. Notification via canarytokens.org is sufficient. Binary patching is allowed (i.e. binary hash differences won't trigger other forms of alerting noise). You have identified high signal-to-noise commands to hook into (i.e. ones that you expect only a hacker to run, e.g. nc, whoami, or even bash).

How?

This PoC is not automated, and relies on patching entries to include our canary library by overwriting the .dynamic section DEBUG entry, so it relies on binaries already using dynamic libraries.

Once we have patched a binary to load our canary library, all we need to do is ensure that our canary library exercises the canary token on startup.

We're using a 64-bit nc binary for this example.

Inspecting ELF

First, we inspect our binary with readelf:

$ readelf -d `which nc`

Dynamic section at offset 0x8ba8 contains 29 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libbsd.so.0]
...
 0x0000000000000015 (DEBUG)              0x0
...
Enter fullscreen mode Exit fullscreen mode

There are three things of note here:

  • 0x8ba8 is the start of the .dynamic section,
  • libbsd.so.0 is the first entry, and
  • a DEBUG entry exists.

Patching ELF

The corresponding code from elf.h in Linux is:

#define DT_NEEDED   1
#define DT_DEBUG    21

typedef struct {
  Elf64_Sxword d_tag;           /* entry tag value */
  union {
    Elf64_Xword d_val;
    Elf64_Addr d_ptr;
  } d_un;
} Elf64_Dyn;
Enter fullscreen mode Exit fullscreen mode

We want to find entries by their d_tag, override a d_tag of DEBUG (21, i.e 0x15) with NEEDED (1, i.e. 0x01), and provide a valid d_ptr value.

We find both easily enough with hexdump:

$ hexdump -C `which nc` | grep -EA1 '^00008[bcde]..  .*  (01|15)'
00008ba0  70 4b 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |pK..............|
00008bb0  af 02 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
00008bc0  bb 02 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
00008bd0  ca 02 00 00 00 00 00 00  0c 00 00 00 00 00 00 00  |................|
--
00008c80  18 00 00 00 00 00 00 00  15 00 00 00 00 00 00 00  |................|
00008c90  00 00 00 00 00 00 00 00  03 00 00 00 00 00 00 00  |................|
Enter fullscreen mode Exit fullscreen mode

Our first entry has a d_tag of 01 00 00 00 00 00 00 00 and a d_ptr of af 02 00 00 00 00 00 00 (resolving to "libbsd.so.0").

Our debug entry has a d_tag of 15 00 00 00 00 00 00 00 and d_ptr of 00 00 00 00 00 00 00 00.

We can patch our binary with this python code:

with open('nc.patched', 'rb+') as f:
    f.seek(0x8ba8)
    dt_needed_entry = bytearray(f.read(0x10))
    dt_needed_entry[8] = dt_needed_entry[8]+3 # eliminate "lib" prefix
    f.seek(0x8c88)
    f.write(dt_needed_entry)
Enter fullscreen mode Exit fullscreen mode

We copy nc with: cp $(which nc) nc.patched, and patch it with python3 patch_nc.py.

ldd will now show us that nc.patched loads bsd.so.0:

$ ldd nc.patched
...snip...
        bsd.so.0 => not found
...snip...
Enter fullscreen mode Exit fullscreen mode

So far so good!

Creating our canary shared library

Having create a "Web bug / URL token" at canarytokens.org, we embed a minimal HTTP GET in our shared library:

#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>

const char *canary_token_host = "canarytokens.com";
const char *canary_token = "YOUR_TOKEN";

void init(int argc, char **argv, char **envp) {
  int sock;

  if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
    return;
  }

  struct sockaddr_in target;
  target.sin_family = AF_INET;

  struct hostent *h = gethostbyname(canary_token_host);
  char *target_ip = inet_ntoa(*((struct in_addr *)h->h_addr));

  if (inet_pton(AF_INET, target_ip, (void *)(&(target.sin_addr.s_addr))) <= 0) {
    return;
  }
  target.sin_port = htons(80);

  if(connect(sock, (struct sockaddr *)&target, sizeof(struct sockaddr)) < 0) {
    return;
  }

  char payload[300];
  sprintf(payload,
    "GET /tags/articles/%s/index.html HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\n\r\n",
    canary_token, canary_token_host, argv[0]);
  send(sock, payload, strlen(payload), 0);

  char buf[100];
  read(sock, buf, 100);

  close(sock);
}

__attribute__((section(".init_array"))) typeof(init) *__init = init;
Enter fullscreen mode Exit fullscreen mode

We verify it works correctly with a manual LD_PRELOAD:

$ gcc -shared -fPIC canary.c -o canary.so
$ LD_PRELOAD=`pwd`/canary.so ls
...snip...
$ curl -s 'http://canarytokens.org/download?fmt=incidentlist_json&token=YOUR_TOKEN&auth=YOUR_AUTH' | jq '.[].useragent' | tail -n 1
"ls"
Enter fullscreen mode Exit fullscreen mode

Excellent! (The above URL is the Export link on the Manage Your Token page)

Wiring it all together for the PoC

We put our canary.so in place under bsd.so.0 and fire up nc.patched!

$ sudo cp canary.so /lib/x86_64-linux-gnu/bsd.so.0
$ ./nc.patched -h
...snip...
$ curl -s 'http://canarytokens.org/download?fmt=incidentlist_json&token=YOUR_TOKEN&auth=YOUR_AUTH' | jq '.[].useragent' | tail -n 1
"nc.patched"
Enter fullscreen mode Exit fullscreen mode

Success!

Oldest comments (0)