Linux ELF Binary Canary PoC
Tags: blueteam, canarytoken, intrusiondetection, elf
Published: 2022-12-04 03:09:25

# 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
...
```

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:
```c
#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;
```

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  |................|
```
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:

```python
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)
```

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...
```

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:

```c
#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;
```

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"
```

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"
```

Success!