はじめに
背景
これは、先日公開したFirefoxでオレオレEVSSL証明書の番外編です。しかし、SSLの話はほぼ関係ありません。
先の記事では、Firefoxのソースコードにハードコーディングされた情報を変更するため、ブラウザのビルドが必要と結論付けていました。
しかし、Firefoxほどの巨大ソフトウェアとなると、ビルドは大変な手間となりますし、またバージョンが変わったらやり直しになってしまいます。
しかし、私は大きな見落としをしていたのです。実行時パッチを適用するという手段があることを。これであれば、既存のファイルを書き換えることなく、大規模なビルドも必要なく、手軽に誰でもオレオレEVSSLを試すことが可能となるでしょう。1
概要
とは言え、実行時パッチって何よ? という疑問は出るかと思います。
ここでしかし思い返してください。どんなプログラムであっても、実行中はメモリ上にロードされた1データに過ぎないことを。つまり、メモリ上のデータを都合よく書き換えることができれば、それだけで十分パッチになります。
加えて、Linuxで使われているELF2、そのローダであるld.soにはプリロードと言う機能があり、ユーザが指定したライブラリによって処理の差し替えや起動時の処理追加ができるようになっています。これを活用しない手はありません。
…一応、これは私のオリジナルのアイデアじゃなくて、情報処理学会第69回全国大会の動的リンク機構を利用したバイナリコードパッチ機能の設計で言及されていたり、ブログmemologueのついカッとなって実行バイナリにパッチで事例が紹介されている、由緒正しい、いわば枯れた技術です。3
ということで、以下では、オレオレEVSSLを実現するための実行時パッチについてまとめた内容を説明したいと思います。
環境
環境については前回と同じです。
- OS: CentOS 7.5.1804
- ブラウザ: Firefox(ESR)60.1.0-4
Firefoxに実行時パッチ
パッチの内容
さて、先日のFirefoxでオレオレEVSSL証明書でコードに施した修正というのは、つまるところ自分でたてたプライベート認証局の情報を追加するだけのものでした。
ソースファイルとしては、ExtendedValidation.cppその中の配列kEVInfos
で情報が管理されています。
配列の要素数を変えるのは各所影響が大きいので難易度があがりますが、1要素を差し替えるだけならベイビー・サブミッションでできることでしょう。その分、EVとして認識されなくなる認証局が出てしまうのは避けられませんが。
struct EVInfo
{
// See bug 1338873 about making these fields const.
const char* dottedOid;
const char* oidName; // Set this to null to signal an invalid structure,
// (We can't have an empty list, so we'll use a dummy entry)
unsigned char sha256Fingerprint[SHA256_LENGTH];
const char* issuerBase64;
const char* serialBase64;
};
…(略)
static const struct EVInfo kEVInfos[] = {
…(略)
課題と解決
課題一覧
ただしかし、幾つか考慮が必要な点があります。それを挙げてみますと、
- メモリ上のどこにその配列があるのか、どうやって調べるか
- const配列であるためOSのメモリ保護化にあり、通常は書き換えができないがどうするか
- この配列はプログラム起動時にメモリにロードされていないが、どうやって書き換えを行うか
とこれだけあります。それぞれどのように解決するか見ていきます
解決1:メモリ上の場所の調査
一番肝心な問題です。ソースコード中の配列がどのようにメモリに配置されるのか? もちろん、デバッグ情報があればそこから調べる方法はあるはずですが4、それを解析するのは面倒ですし、何よりデバッグ情報が残っている保証もありません。
しかし発想を変えてみます。今回書き換えを行う配列には、既知の認証局のSHA256ハッシュが保存されています。このハッシュと同じデータを偶然作り出すのは現実的に無理ですから、メモリをサーチして同じハッシュ値が見つかれば、そこが目的の配列の一部と判断することができるはずです。
これならデバッグ情報のことなど考える必要はなくなります。
あとはせいぜい、目的の配列がどのファイルにあって、どのようにメモリに配置されるかだけざっくりと把握していれば何とかなるはずです。
解決2:メモリの保護
ちなみに問題の配列ですが、保持されているEV OIDをgrepで検索かけることで、libxul.so
というライブラリに存在することが分かります。
そして、constなデータではあるもののポインタを含んでおり、このポインタ値は実行時のメモリ状況によって値が変わるものですから、.data.rel.ro
というELFセクションに格納されています。
このセクションは、書き込み不可なメモリ領域に配置されます。
つまり、書き換えようとすれば、即セグメンテーションフォルトです。どうすれば良いでしょうか。
…しかし、そんな時のために、ちゃんとメモリのアクセス保護を変更するシステムコールが用意されています。それはmprotect
関数です。
別に書き込み不可といっても、アクセス保護が変更できないと言ってるわけではないので、一時的に書き込み可にしてしまえば良いのです。
解決3:いつ書き換えを行うか
Firefoxのプログラム本体のデータであれば、プリロード時にもアクセスすることができるのですが、配列データのあるライブラリlibxul.so
は、その後からダイナミックロード(dlopen
関数)されるものです。
つまり、いつdlopen
されるかタイミングを掴み、その直後に書き換え処理を行うように調整しなければなりません。
…こう言うと難しく感じるかも知れませんが、プリロードでは既存の関数を置き換えることができるようになっています。
なので、dlopen
自体を置き換えてしまえば良いのです。置き換えたdlopen
では、どんなライブラリがロード対象になったか分かりますから、件のlibxul.so
がちょうど対象になった時にデータ書き換えを行えば良いことになります。
実際の処理
では、実際の処理を解説していきます。C言語ソースoreoreevff.c
については末尾のソースコードにあります。
前処理
プログラム起動の際のプリロード処理時は、ソース内で定義された独自のdlopen
によって、オリジナルのdlopen
が置き換えられます。ただそれだけだとダイナミックロード処理そのものができなくなってしまいますから、constructor
属性を指定した初期化処理initHook
で、オリジナルのdlopen
のアドレスを保存しておきます。
※この保存したアドレスは、独自dlopen
から元のdlopen
を呼ぶために使います。
そして置き換えたdlopen
にlibxul.so
が指定された時に、いよいよdoOreorePatch
により書き換えを行います。
ELFセクションの検索
ELF情報の解析は、ブログ「ほげほげ日記」のELF のセクション名一覧を表示を参考にさせて頂きました。
実際にlibxul.so
を開いて、目的の配列がある.data.rel.ro
セクションの、ファイル内のオフセット、それからセクションサイズを調べておきます。
この処理を担当するのがgetExeInfo
です。
なお、後からファイルを特定するのが容易になるように、デバイス番号5とi-node番号6もfstat
により取得しておきます。
メモリマップの検索
dlopen
された時に、ライブラリがメモリのどこにマップされたか。これは、/proc/プロセス番号/maps
というファイル ( 自プロセスなら/proc/self/maps
) を読むことで調べることができます。
…(略)
561785d44000-561785d74000 r-xp 00000000 fd:00 7993676 /usr/lib64/firefox/firefox
561785f73000-561785f74000 r--p 0002f000 fd:00 7993676 /usr/lib64/firefox/firefox
561785f74000-561785f75000 rw-p 00030000 fd:00 7993676 /usr/lib64/firefox/firefox
…(略)
7fdc0ff7a000-7fdc0ff7b000 rw-p 00050000 fd:00 2673932 /usr/lib64/libssl3.so
7fdc0ff7b000-7fdc0ff7c000 rw-p 00000000 00:00 0
7fdc0ff7c000-7fdc1005a000 r-xp 00000000 fd:00 2330883 /usr/lib64/firefox/libxul.so
7fdc1005a000-7fdc10820000 ---p 000de000 fd:00 2330883 /usr/lib64/firefox/libxul.so
7fdc10820000-7fdc16065000 r-xp 000de000 fd:00 2330883 /usr/lib64/firefox/libxul.so
7fdc16065000-7fdc16264000 ---p 060e9000 fd:00 2330883 /usr/lib64/firefox/libxul.so
7fdc16264000-7fdc16650000 r--p 05922000 fd:00 2330883 /usr/lib64/firefox/libxul.so
7fdc16650000-7fdc16681000 rw-p 05d0e000 fd:00 2330883 /usr/lib64/firefox/libxul.so
7fdc16681000-7fdc166eb000 rw-p 00000000 00:00 0
7fdc166eb000-7fdc166fa000 r-xp 00000000 fd:00 123087 /usr/lib64/libbz2.so.1.0.6
…(略)
このファイルには、マップされたメモリ領域の開始アドレス・終了アドレス、許可属性(読み、書き、実行)、対応するファイルのオフセット、ファイルのあるデバイスのデバイス番号(major,minor)、ファイルのi-node番号、ファイル名が一覧になっています。
※対応するファイルがない場合もあります。
先ほど調べたデバイス番号、i-node番号からlibxul.so
に対応し、かつ許可属性r--
で当該ELFセクションを含む領域をオフセットから見て割り出しておきます。
上の例では、アドレス範囲 0x7fdc16264000-7fdc16650000 がlibxul.so
の.data.rel.ro
セクションを含む領域です。
書き換え位置の決定と書き換え
後はメモリ検索を行って、書き換えを行うだけです。
書き換え対象のデータを検索するキーたるハッシュ値はsacrificedCAhash
に、書き換え後のデータはoreoreCA
に保存しています。
※このoreoreCA
は、FirefoxでオレオレEVSSL証明書で配列に追加したデータそのものです。
その前後でmprotect
を呼んで、許可属性を変更しておきます。
これがdoOreorePatch
の残りのメインの処理となります。
ビルドと実行
以下、実際にプリロードするライブラリをビルドし、Firefoxを実行した時の例です。
-shared
オプションを指定することで、DSOとしてoreoreevff.so
ファイルをビルドします。
そして、環境変数LD_PRELOAD
に指定することでプリロードを行い、Firefoxを起動します。
なお、ビルド時に-DDEBUG
でデバッグ出力を有効にしてますので、セクションの情報やメモリマップ情報が出るようになっています。
$ gcc -std=gnu99 -fPIC -shared -ldl -DDEBUG -o oreoreevff.so oreoreevff.c
$ LD_PRELOAD=./oreoreevff.so /usr/lib64/firefox/firefox https://angel.p57/index.html
**debug: oreoreevff.c(96): section offset: 5922e40, size=3e4938 **
**debug: oreoreevff.c(161): mapped area: start at 0x7fdc16264000, size=3ec000 **
**debug: oreoreevff.c(162): section: start at 0x7fdc16264e40, size=3e4938 **
**debug: oreoreevff.c(168): sacrificed CA's address: 0x7fdc16265b30 **
**debug: oreoreevff.c(177): Oreore-EV patch suceeded! **
Firefoxの画面の様子は特に載せませんが、これで前回と同様にオレオレEVSSL証明書を認識させることができました。
終わりに
まとめ
ということで、プリロードによってメモリを書き換える処理を追加し、ブラウザ本体をビルドし直さずとも、ほぼ同等の効果を得ることができる7ことを検証しました。
これであなたにも、充実したオレオレEVライフを!!
ソースコード
#define _GNU_SOURCE
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <dlfcn.h>
#include <errno.h>
#include <elf.h>
#ifdef DEBUG
#define DEBUG_PRINTF(fmt,...) fprintf(stderr,"**debug: %s(%d): "fmt" **\n",__FILE__,__LINE__,__VA_ARGS__)
#define DEBUG_PERROR(fmt,...) fprintf(stderr,"**debug: %s(%d): "fmt" ( %s ) **\n",__FILE__,__LINE__,__VA_ARGS__,strerror(errno))
#else
#define DEBUG_PRINTF(...)
#define DEBUG_PERROR(...)
#endif
static const char *memstrstr(const char *area,size_t areasz,const char *key,size_t keysz) {
int i=0;
for ( int j=0; j<areasz; j++ ) {
if ( area[j]!=key[i] ) {
i=0;
}
else if ( keysz==++i ) {
return area+j-keysz+1;
}
}
return 0;
}
static bool getExeInfo(const char *path,uint16_t *devp,uint64_t *inop,size_t *offp,size_t *sizep) {
static const char targetsection[]=".data.rel.ro";
int fd;
fd=open(path,O_RDONLY);
if ( fd==-1 ) {
DEBUG_PERROR("failed to open %s",path);
return false;
}
size_t selfsize;
struct stat buf;
int ret;
ret=fstat(fd,&buf);
if ( ret!=0 ) {
DEBUG_PERROR("failed to stat %s",path);
close(fd);
return false;
}
*devp=buf.st_dev;
*inop=buf.st_ino;
selfsize=buf.st_size;
void *head=mmap(NULL,selfsize,PROT_READ,MAP_PRIVATE,fd,0);
if ( head==MAP_FAILED ) {
DEBUG_PERROR("failed to mmap %s",path);
close(fd);
return false;
}
bool found=false;
const Elf64_Ehdr *ehdr=head;
const Elf64_Shdr *shstr=head+ehdr->e_shentsize*ehdr->e_shstrndx+ehdr->e_shoff;
for ( int i=0;!found&&i<ehdr->e_shnum;i++ ) {
const Elf64_Shdr *shdr=head+ehdr->e_shoff+ehdr->e_shentsize*i;
const char *secname=head+shstr->sh_offset+shdr->sh_name;
if ( strcmp(targetsection,secname)==0 ) {
found=true;
*offp=shdr->sh_offset;
*sizep=shdr->sh_size;
}
}
munmap(head,selfsize);
close(fd);
if ( !found ) {
DEBUG_PRINTF("failed to find %s section",targetsection);
return false;
}
return true;
}
static bool getSectionMapInfo(const char *path,void **mapsp,size_t *mapszp,void **secsp,size_t *secszp) {
static const char mapfile[]="/proc/self/maps";
uint16_t selfdev;
uint64_t selfino;
size_t secoff;
if ( !getExeInfo(path,&selfdev,&selfino,&secoff,secszp) ) {
DEBUG_PRINTF("%s","getExeInfo failed");
return false;
}
DEBUG_PRINTF("section offset: %zx, size=%zx",secoff,*secszp);
FILE *fp=fopen(mapfile,"r");
if ( !fp ) {
DEBUG_PERROR("failed to open %s",mapfile);
return false;
}
char buf[4096];
bool found=false;
while ( !found && fgets(buf,sizeof(buf),fp) ) {
int ret;
uintptr_t saddr,eaddr;
uint64_t off,ino;
int dmaj,dmin;
ret=sscanf(buf,"%"SCNxPTR"-%"SCNxPTR" r--%*s %"SCNx64" %x:%x %"SCNu64,&saddr,&eaddr,&off,&dmaj,&dmin,&ino);
if ( ret!=6 ) {
continue;
}
uint16_t dev=dmaj*256+dmin;
if ( dev==selfdev && ino==selfino && off<=secoff && eaddr-saddr+off>secoff ) {
found=true;
*mapsp=(void*)saddr;
*mapszp=eaddr-saddr;
*secsp=(void*)(saddr+secoff-off);
}
}
fclose(fp);
if ( !found ) {
DEBUG_PRINTF("%s","failed to get start address");
return false;
}
return true;
}
struct EVInfo {
const char *oid;
const char *oidname;
char hash[32];
const char *issuerb64;
const char *serialb64;
};
static const struct EVInfo oreoreCA={
"1.3.6.1.4.1.13769.666.666.666.1.500.9.1",
"Oreore EV OID",
{ 0xAB, 0xC2, 0x3A, 0x8E, 0x6E, 0x87, 0x3E, 0xCE, 0xD0, 0xCB, 0x0A,
0xDB, 0x86, 0xDA, 0x04, 0x15, 0x02, 0x5B, 0x23, 0xC1, 0x54, 0xDD,
0xED, 0xBC, 0x41, 0xC6, 0x18, 0xF4, 0x1D, 0x41, 0xCB, 0x4C },
"MFYxGjAYBgNVBAMMEVNpeCBHYXRlcyBUZXN0IENBMRUwEwYDVQQKDAxTb3VrYWkg"
"U3luZC4xFDASBgNVBAgMC05lby1TYWl0YW1hMQswCQYDVQQGEwJKUA==",
"AIKXJVrdaqvM",
};
// sacrifice DigiCert High Assurance EV Root CA
static const char sacrificedCAhash[32]=
{ 0x74, 0x31, 0xe5, 0xf4, 0xc3, 0xc1, 0xce, 0x46, 0x90, 0x77, 0x4f,
0x0b, 0x61, 0xe0, 0x54, 0x40, 0x88, 0x3b, 0xa9, 0xa0, 0x1e, 0xd0,
0x0b, 0xa6, 0xab, 0xd7, 0x80, 0x6e, 0xd3, 0xb1, 0x18, 0xcf };
static void doOreorePatch(const char *lib) {
void *mapstart,*secstart;
size_t mapsize,secsize;
if ( !getSectionMapInfo(lib,&mapstart,&mapsize,&secstart,&secsize) ) {
DEBUG_PRINTF("%s","getSectionMapInfo failed");
return;
}
DEBUG_PRINTF("mapped area: start at %p, size=%zx",mapstart,mapsize);
DEBUG_PRINTF("section: start at %p, size=%zx",secstart,secsize);
const void *pos=memstrstr(secstart,secsize,sacrificedCAhash,sizeof(sacrificedCAhash));
if ( !pos ) {
DEBUG_PRINTF("%s","failed to find the sacrificed CA");
return;
}
DEBUG_PRINTF("sacrificed CA's address: %p",pos);
int ret;
ret=mprotect(mapstart,mapsize,PROT_READ|PROT_WRITE);
if ( ret!=0 ) {
DEBUG_PERROR("failed to mprotect from %p",mapstart);
return;
}
*(struct EVInfo*)(pos-offsetof(struct EVInfo,hash))=oreoreCA;
mprotect(mapstart,mapsize,PROT_READ);
DEBUG_PRINTF("%s","Oreore-EV patch suceeded!");
}
static void *(*dlopen_org)(const char *,int)=0;
__attribute__((constructor))
static void initHook(void) {
dlopen_org=dlsym(RTLD_NEXT,"dlopen");
}
void *dlopen(const char *filename,int flag) {
static const char libxulpath[]="/usr/lib64/firefox/libxul.so";
static bool patchDone=false;
void *ret=dlopen_org(filename,flag);
if ( !patchDone && strcmp(filename,libxulpath)==0 ) {
doOreorePatch(libxulpath);
patchDone=true;
}
return ret;
}
脚注
-
手軽に誰でもオレオレEVSSL: いや、そんなん別に要らんがな、というツッコミはご遠慮ください。 ↩
-
ELF: ご存じ"Executable and Linking Format"の略ですね。実行可能バイナリやライブラリファイルのフォーマットです。 ↩
-
デバッグ情報: デバッガを使う時は、そういう情報を元にデータを見ることができるわけですが…。 ↩
-
デバイス番号: 当該ファイルが存在するファイルシステムのmount元のデバイス(ディスク上のパーティションや、lvmの論理ディスク等)に割り振られた番号です。8bitのmajor,minorの2つを組み合わせた16bitの整数で表すことができます。 ↩
-
i-node番号: ファイルシステム上で、ファイルを一意に識別するための番号です。 ↩
-
ほぼ同等の効果: 厳密には、オレオレ認証局の情報を入れる代わりに、1つ使えない認証局ができたわけですが。 ↩