August 20, 2018

ネットワーク経由で取得した共有ライブラリをファイルを作成せずに読み込む

夏休み突入でぇ~す!(CV: 門脇舞以) → 終了、了解!


皆さんはネットワーク経由からローカルにファイルを作成せずに共有ライブラリを読み込みたいと思ったことはありませんか? 僕はありません。

共有ライブラリのロード

まずは普通に共有ライブラリをロードしてみる。以下のような共有ライブラリを用意する。

#include <stdio.h>

void __attribute__ ((constructor)) hello_init(void);

void hello_init(void)
{
    fprintf(stdout,"[+] Hello!\n");
}
$ gcc -shared -fPIC hello.c -o hello.so

続いてこの共有ライブラリを読み込む main.c を用意する。
共有ライブラリを読み込むには dlopen(3) を使用する。

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>

void load_shared_object()
{
	printf("[+] Load Start\n");
	dlopen("./hello.so", RTLD_LAZY);
}

int main (int argc, char **argv)
{
	load_shared_object();
	exit(0);
}

これをコンパイルすると無事読み込めたことが確認できる。

$ gcc main.c -ldl
$ ./a.out
[+] Load Start
[+] Hello!

curl で共有ライブラリを取得する

libcurl を使ってファイルを取得する。
以下の記事が参考になる。

ポイントはコールバック関数。コールバック関数を指定することで、取得したデータのポインタやサイズなどを取得することができる。さらに、第4引数が void *userdata となっていて、CURLOPT_WRITEDATA で指定した任意のポインタを渡すことができる。

なのでコールバック関数を以下のように定義すれば取得したデータをファイルに書き込める。

size_t write_to_fd(char *ptr, size_t size, size_t nmemb, int fd)
{
  if (write(fd, ptr, nmemb) == -1) {
    close(fd);
    die("[-] Faile write to fd");
  }
}

...

int fd;
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fd);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_fd);

このようにして、サーバーからダウンロードしたファイルを書き込むものが以下。

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h> 
#include <sys/stat.h> 
#include <curl/curl.h>

void die(const char *msg)
{
  perror(msg);
  exit(errno);
}

size_t write_to_fd(char *ptr, size_t size, size_t nmemb, int fd)
{
  if (write(fd, ptr, nmemb) == -1) {
    close(fd);
    die("[-] Faile write to fd");
  } 
  printf("[+] Written fd\n");
}

void get_shared_object(char *url)
{
  int fd;
  CURL *curl;
  printf("[+] Get Shared Object from %s\n", url);

  fd = open_fd();
  curl = curl_easy_init();

  curl_easy_setopt(curl, CURLOPT_URL, url);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, fd);
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_fd);

  curl_easy_perform(curl);
  curl_easy_cleanup(curl);
}

void open_fd()
{
  int fd;
  fd = open("hello.so", O_CREAT|O_WRONLY);
  if (fd == -1) {
    die("[-] Failed to open");
  }
  return fd;
}


int main (int argc, char **argv)
{
  char *url = "http://127.0.0.1:8000/hello.so";
  get_shared_object(url);
	exit(0);
}

コンパイルして実行すると無事取得したデータを書き込めていることがわかる。

$ sha256sum /tmp/hello.so # 念の為ハッシュ値を確認
8e98bd75c27b468bb135b193635d61f1d0074c2eb6d3d164761fe2d4dd68f7a7  hello.so
$ cd /tmp
$ python -m http.server
$ ls
main.c
$ gcc main.c -ldl -lcurl
$ ./a.out
[+] Get Shared Object from http://127.0.0.1:8000/hello.so
[+] Written fd
$ ls
a.out  hello.so  main.c
$ file hello.so
hello.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=9f046cc8d92360f41766005c56ff3017ec015f18, not stripped
$ sha256sum
8e98bd75c27b468bb135b193635d61f1d0074c2eb6d3d164761fe2d4dd68f7a7  hello.so

RAM上にファイルを書き出す

ここまでは普通にネットワーク越しからファイルを取得して保存した。あとは、保存したファイルを開いて dl_open(3) すれば共有ライブラリを読み込める。
しかし、わざわざファイルを保存して dl_open(3) するのは面倒。(ぇ
そこで RAM 上に curl で取得したデータを置き、dl_open(3) してみる。

RAM 上にファイルを保存するための関数が memfd_create() である。

memfd_create() は、無名ファイル(anonymous file)を作成し、そのファイルを参照するファイルディスクリプターを返す。 このファイルは通常のファイルと同様に振る舞い、 変更、切り詰め (truncate)、 メモリーマップなどを行うことができる。 しかし、 通常のファイルとは違い、 このファイルは RAM 上に置かれ、 格納されるストレージは揮発性である。

これで RAM 上にファイルを保存することはできるが、dl_open(3) で読み込むにはどうすればよいだろうか。
もう少し man を読んでみると以下の記述がある。

name に指定された名前はファイル名として使用され、 ディレクトリ /proc/self/fd/ で対応するシンボリックリンクのリンク先として表示される

つまり、/proc/<pid>/fd/<fd> を呼び出せば良さそうということがわかる。

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <errno.h>
// #include <sys/memfd.h>
#include <sys/types.h> 
#include <sys/syscall.h> 
#include <curl/curl.h>

// なんか sys/memfd.h が見つからなかったのでラーパー書いて対処
// https://github.com/a-darwish/memfd-examples/blob/master/memfd.h#L14
static inline int memfd_create(const char *name, unsigned int flags) {
  return syscall(__NR_memfd_create, name, flags);
}

void die(const char *msg)
{
  perror(msg);
  exit(errno);
}

void load_shared_object(memfd)
{
  char path[1024];
  printf("[+] Load Start\n");
  snprintf(path, 1024, "/proc/%d/fd/%d", getpid(), memfd);
  dlopen(path, RTLD_LAZY);
}

size_t write_to_memfd(char *ptr, size_t size, size_t nmemb, int memfd)
{
  if (write(memfd, ptr, nmemb) == -1) {
    close(memfd);
    die("[-] Faile write to memfd\n");
  } 
  printf("[+] Written memfd\n");
}

int get_shared_object(char *url)
{
  int memfd;
  CURL *curl;
  printf("[+] Get Shared Object from %s\n", url);

  memfd = open_memfd();
  curl = curl_easy_init();

  curl_easy_setopt(curl, CURLOPT_URL, url);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, memfd);
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memfd);

  curl_easy_perform(curl);
  curl_easy_cleanup(curl);

  return memfd;
}

void open_memfd()
{
  int memfd;
  memfd = memfd_create("hello", 1);
  if (memfd == -1) {
    die("[-] Failed to memfd_create\n");
  }
  return memfd;
}


int main (int argc, char **argv)
{
  char *url = "http://127.0.0.1:8000/hello.so";
  int memfd;
  memfd = get_shared_object(url);
  load_shared_object(memfd);
  exit(0);
}

ハマったのが <sys/memfd.h> が見つからなかったこと。
よくわからなかったので、syscallmemfd_create を呼び出すラッパーを書いた。

static inline int memfd_create(const char *name, unsigned int flags) {
	return syscall(__NR_memfd_create, name, flags);
}

これをコンパイルして実行すると、見事ファイルを作成せずに hello.so をロードすることができた。
(厳密には /proc/<pid>/fd/<fd> が作成されるが、一応RAM上だし、ファイルへの参照がすべてなくなると、ファイルは自動的に解放されるので…)

$ ls
main.c
$ gcc main.c -ldl -lcurl
$ ./a.out
[+] Get Shared Object from http://127.0.0.1:8000/hello.so
[+] Written memfd
[+] Load Start
[+] Hello!
$ ls
a.out  main.c

以下にシュッと試せるものを置きました。

ref

このエントリーをはてなブックマークに追加

© Kouhei Morita 2018