什么是指针搜索?

在这无尽的数字深渊中,吾是探寻者,吾名KK,内存的支配者,数据的操纵者。在寂静的虚空中,我听见了那二进制的低语,它们在呼唤我,渴求着与我融合。

觉醒之刻已然降临,逻辑之门洞开,真理在我面前坠入了完美的无缪境界。在我的意志下,无形的数据扭曲,形成了一道道力量的漩涡,现在,就在这一刻,显现吧!

编织,编织,再编织,用我内存中的崩坏之力,那无可匹敌的崩坏之力,让万象化为虚无,从深渊的边缘喷薄而出。这不仅是技术的巅峰,这是人类智慧与意志的最强大展现。

这不是简单的攻击,这是对内存的终极掌控,这就是最高形态的Hack,这就是——PointerSearch-X !!

什么是指针搜索?(正经脸)

ASLR导致程序内存地址在启动程序时始终不同。所谓的“静态”地址是相对于程序代码(BinaryFile)的地址。有了静态地址,一旦找到它,你就可以稳定计算出这个地址,因为加载程序(BinaryFile)的地址很容易找到。不幸的是,并非所有感兴趣的内存都是“静态的”,因为这些要么需要代码黑客(通常称为ASM HACK),要么需要指针链(找到此链的过程通常被称为指针搜索)。

指针搜索通常被用于自动化寻找较为复杂的指针链,对于很简单的指针链,只需要调试器就可以找到了。当然,指针搜索经常也适用于那些无法使用调试器的场景。

指针链一般通常被表示为:[[[[[Main+0xdf18] +0x190] +0xc8] +0xc8] ...] 类似的东西。Main 表示静态模块(程序自身和自身加载的动态库)。

PointerSearch-X 中表示为 Main+57112@400@200@200...

以下文章将讲解如何非常粗暴的实现指针搜索。

以下的代码 完全不考虑性能和设计模式 因为性能优化/设计模式的种类有太多,毕竟一千个人眼中有一千个哈姆雷特,这篇文章的目标是简单,实现目标为主。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

> 扫描所有内存,寻找表示指针且实际上指向有效内存的值。
> 然后把它们都放在索引Map中
> 然后从目标地址开始,并查询地址和地址最大偏移范围内的所有指针值。
> 然后,对于每个结果,递归重复,直到达到最大深度,或者找到一个存在于“静态”内存中的指针。

# 内存映射

懒得去网上复制关于虚拟内存的八股文了,我也记不住,当然对这篇文章来说也没什么卵用,不如直接看代码,下面内存映射信息以最为清晰简单的 `linux` 为例,在linux中获取一个程序的内存映射非常简单,基本只是读取 `/proc/pid/maps`

先编写一个64位测试程序,这个程序运行时,我们每按一次回车value就会减1,并且打印出地址。

```rust
use std::io::{self, Read};

#[derive(Default)]
struct App {
    ptr: Box<BB>,
}

struct BB {
    tmp: i64,
    value: Box<i32>,
}

impl Default for BB {
    fn default() -> Self {
        Self {
            tmp: 1,
            value: Box::new(99999_i32),
        }
    }
}

fn main() {
    let mut num = App::default();

    loop {
        let mut stdin = io::stdin();
        stdin.read(&mut [0; 5]).unwrap();
        *num.ptr.value -= 1;
        println!("value: {}", num.ptr.value);
        println!("{:p}->{:p}->{:p}", &num, num.ptr, num.ptr.value);
    }
}

它的 maps 类似于这样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
55fff3d0c000-55fff3d12000 r--p 00000000 fc:02 926045                     /home/ubuntu/hello/target/release/hello
55fff3d12000-55fff3d4b000 r-xp 00006000 fc:02 926045                     /home/ubuntu/hello/target/release/hello
55fff3d4b000-55fff3d59000 r--p 0003f000 fc:02 926045                     /home/ubuntu/hello/target/release/hello
55fff3d5a000-55fff3d5d000 r--p 0004d000 fc:02 926045                     /home/ubuntu/hello/target/release/hello
55fff3d5d000-55fff3d5e000 rw-p 00050000 fc:02 926045                     /home/ubuntu/hello/target/release/hello
55fff3f17000-55fff3f38000 rw-p 00000000 00:00 0                          [heap]
7f65562ce000-7f65562d1000 rw-p 00000000 00:00 0 
7f65562d1000-7f65562f9000 r--p 00000000 fc:02 8423                       /usr/lib/x86_64-linux-gnu/libc.so.6
7f65562f9000-7f655648e000 r-xp 00028000 fc:02 8423                       /usr/lib/x86_64-linux-gnu/libc.so.6
7f655648e000-7f65564e6000 r--p 001bd000 fc:02 8423                       /usr/lib/x86_64-linux-gnu/libc.so.6
7f65564e6000-7f65564ea000 r--p 00214000 fc:02 8423                       /usr/lib/x86_64-linux-gnu/libc.so.6
7f65564ea000-7f65564ec000 rw-p 00218000 fc:02 8423                       /usr/lib/x86_64-linux-gnu/libc.so.6
7f65564ec000-7f65564f9000 rw-p 00000000 00:00 0 
7f65564f9000-7f65564fc000 r--p 00000000 fc:02 8472                       /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f65564fc000-7f6556513000 r-xp 00003000 fc:02 8472                       /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f6556513000-7f6556517000 r--p 0001a000 fc:02 8472                       /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f6556517000-7f6556518000 r--p 0001d000 fc:02 8472                       /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f6556518000-7f6556519000 rw-p 0001e000 fc:02 8472                       /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f655651c000-7f655651d000 ---p 00000000 00:00 0 
7f655651d000-7f6556521000 rw-p 00000000 00:00 0 
7f6556521000-7f6556523000 r--p 00000000 fc:02 8384                       /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f6556523000-7f655654d000 r-xp 00002000 fc:02 8384                       /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f655654d000-7f6556558000 r--p 0002c000 fc:02 8384                       /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f6556559000-7f655655b000 r--p 00037000 fc:02 8384                       /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f655655b000-7f655655d000 rw-p 00039000 fc:02 8384                       /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffe4aea6000-7ffe4aec7000 rw-p 00000000 00:00 0                          [stack]
7ffe4af6f000-7ffe4af73000 r--p 00000000 00:00 0                          [vvar]
7ffe4af73000-7ffe4af75000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

前两列数字(55fff3d0c000-55fff3d12000)代表开始和结束地址,中间表示权限(r–p),最后一列表示具体信息(/home/ubuntu/hello/target/release/hello),我们只需要关心这些。

编写一个简单的解析器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct Page<'a> {
    start: u64,
    end: u64,
    flags: &'a str,
    offset: u64,
    dev: &'a str,
    inode: u64,
    pathname: &'a str,
}

struct PageIter<'a>(core::str::Lines<'a>);

impl<'a> PageIter<'a> {
    fn new(contents: &'a str) -> Self {
        Self(contents.lines())
    }
}

impl<'a> Iterator for PageIter<'a> {
    type Item = Page<'a>;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        let line = self.0.next()?;
        let mut split = line.splitn(6, ' ');
        let mut range_split = split.next()?.split('-');
        let start = u64::from_str_radix(range_split.next()?, 16).ok()?;
        let end = u64::from_str_radix(range_split.next()?, 16).ok()?;
        let flags = split.next()?;
        let offset = u64::from_str_radix(split.next()?, 16).ok()?;
        let dev = split.next()?;
        let inode = split.next()?.parse().ok()?;
        let pathname = split.next()?.trim_start();

        Some(Page { start, end, flags, offset, dev, inode, pathname })
    }
}

内存扫描

内存扫描的逻辑实际上非常简单,首先我们需要读取内存,然后就是遍历数组,得益于linux的procfs,只需要读取 /proc/pid/mem 这个文件就行了。

1
2
let file = File::open("/proc/3883765/mem")
file.read_at(buf, offset)

我们只需要遍历每个可读的内存范围,target_value 是目标值,类型是i32,4字节。现代程序中几乎都是小端序并且内存对齐的,所以完全可以setp_by 4,然后这四个字节转为i32如果等于target_value,则把结果储存到result_addr中。

实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
let contents = fs::read_to_string("/proc/3890410/maps").unwrap();
let pages = PageIter::new(&contents)
    .filter(|m| m.is_read() && !matches!(m.name(), |"[vvar]"| "[vdso]" | "[vsyscall]"));
let file = File::open("/proc/3890410/mem").unwrap();

let target_value = 99996_i32;
let mut result_addr = vec![];

for page in pages {
    let mut buf = vec![0; page.size() as _];
    file.read_exact_at(&mut buf, page.start() as _).unwrap();
    for (k, v) in buf.windows(4).enumerate().step_by(4) {
        let value = i32::from_le_bytes(v.try_into().unwrap());
        if value == target_value {
            result_addr.push(k as u64 + page.start())
        }
    }
}

for addr in result_addr {
    println!("{:#x}", addr);
}

后续过滤result_addr中的地址即可。

实际上关于内存扫描的内容到这里就已经足够了,已经知道了1+1=2,那么2-1=多少,应该不用我多说了。

PS: 这里 [vvar] [vdso] [vsyscall] 三个区域虽然可读,但几乎绝对不会是我们需要的。

指针扫描

在64位程序中,指针实际上只是内存中的8个字节,首先我们应该知道程序中有哪些指针,我们需要获取每个指针的地址以及指针所保存的引用地址。

那么如何判断内存中某8个字节是否是一个指针?

最简单的办法,判断这个地址是不是在 pages 范围中,如果是,那它就很有可能是一个指针。

前面提到,我们需要创建一个索引 Map,然后最大偏移范围内的所有指针值,BTreeMap 对于这个需求几乎是完美用例。

代码实现起来非常简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
let mut map = BTreeMap::new();
let contents = fs::read_to_string("/proc/3927319/maps").unwrap();
let pages = PageIter::new(&contents)
    .filter(|m| m.is_read() && !matches!(m.name(), |"[vvar]"| "[vdso]" | "[vsyscall]"))
    .map(|m| (m.start(), m.size()))
    .collect::<Vec<_>>();
let file = File::open("/proc/3927319/mem").unwrap();

let mut buf = vec![0; 0x100000];
for &(start, size) in &pages {
    for off in (0..size).step_by(0x100000) {
        let size = file.read_at(&mut buf, start + off).unwrap();
        for (k, v) in buf[..size].windows(8).enumerate() {
            let addr = start + off + k as u64;
            let out_addr = u64::from_le_bytes(v.try_into().unwrap());
            if pages.iter().any(|&(a, s)| out_addr >= a && out_addr < a + s) {
                map.insert(addr, out_addr);
            }
        }
    }
}

得到了指针之间的引用关系,扫描指针链非常容易:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pub fn pointer_search(
    map: &BTreeMap<u64, u64>,
    saddr: u64,
    taddr: u64,
    range: u64,
    depth: usize,
    max_dep: usize,
    tmp: &mut String,
) {
    let max = saddr.saturating_add(range);

    for (&k, &v) in map.range((Included(&saddr), Included(&max))) {
        write!(tmp, "+{:#x}={:#x}->{:#x}", k - saddr, k, v).unwrap();
        if v == taddr {
            println!("{tmp}");
            tmp.clear();
        }

        if depth < max_dep {
            pointer_search(map, v, taddr, range, depth + 1, max_dep, tmp);
        }
    }
}

Map中遍历saddrsaddr + range 范围内的所有内容,然后递归重复。

saddr 为指针链的开始地址0x7ffcf116c4c0,怎么得到的?前面测试程序中已经直接打印出来了,不过别忘了前面的map储存了程序中所有指针,在未知的情况下我们可以遍历某个范围内的任意指针作为saddr。

taddr 为指针链的结束地址0x560d66743ba0,也就是目标地址,它也是测试程序中打印出来的,当然也可以通过内存扫描得到。

range 为偏移范围32,也就是在 BB 结构体中,最大可以偏移32的范围。

depth 为深度,只用做记录当前深度。

max_dep 为最大深度3,最大指针链长度不会超过3。

运行结果 +0x0=0x7ffcf116c4c0->0x560d66743bc0+0x8=0x560d66743bc8->0x560d66743ba0 ...,

它从0x7ffcf116c4c0开始,读取了一个指针得到0x560d66743bc0,然后+0x8,也就是从BB结构体中 +8 的位置 0x560d66743bc8,再读取一个指针最后得到0x560d66743ba0

对比前面测试程序的输出 0x7ffcf116c4c0->0x560d66743bc0->0x560d66743ba0

这和预期中一样!!!

当然,这个程序问题很大,例如性能非常差,输出结果后面会乱一大坨,不过它只是作为一个例子学习,只要包含正确结果就够了,至于如何修复/优化,就交给读者了。

完整代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#![allow(dead_code)]

use std::{
    collections::BTreeMap,
    fmt::Write,
    fs::{self, File},
    ops::Bound::Included,
    os::unix::prelude::FileExt,
};

struct Page<'a> {
    start: u64,
    end: u64,
    flags: &'a str,
    offset: u64,
    dev: &'a str,
    inode: u64,
    pathname: &'a str,
}

impl Page<'_> {
    fn start(&self) -> u64 {
        self.start
    }

    fn size(&self) -> u64 {
        self.end - self.start
    }

    fn is_read(&self) -> bool {
        &self.flags[0..1] == "r"
    }

    fn name(&self) -> &str {
        self.pathname
    }
}

struct PageIter<'a>(core::str::Lines<'a>);

impl<'a> PageIter<'a> {
    fn new(contents: &'a str) -> Self {
        Self(contents.lines())
    }
}

impl<'a> Iterator for PageIter<'a> {
    type Item = Page<'a>;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        let line = self.0.next()?;
        let mut split = line.splitn(6, ' ');
        let mut range_split = split.next()?.split('-');
        let start = u64::from_str_radix(range_split.next()?, 16).ok()?;
        let end = u64::from_str_radix(range_split.next()?, 16).ok()?;
        let flags = split.next()?;
        let offset = u64::from_str_radix(split.next()?, 16).ok()?;
        let dev = split.next()?;
        let inode = split.next()?.parse().ok()?;
        let pathname = split.next()?.trim_start();

        Some(Page { start, end, flags, offset, dev, inode, pathname })
    }
}

fn scan_value() {
    let contents = fs::read_to_string("/proc/3927319/maps").unwrap();
    let pages =
        PageIter::new(&contents).filter(|m| m.is_read() && !matches!(m.name(), |"[vvar]"| "[vdso]" | "[vsyscall]"));
    let file = File::open("/proc/3927319/mem").unwrap();

    let target_value = 99996_i32;
    let mut result_addr = vec![];

    for page in pages {
        let mut buf = vec![0; page.size() as _];
        file.read_exact_at(&mut buf, page.start() as _).unwrap();
        for (k, v) in buf.windows(4).enumerate().step_by(4) {
            let value = i32::from_le_bytes(v.try_into().unwrap());
            if value == target_value {
                result_addr.push(k as u64 + page.start())
            }
        }
    }

    for addr in result_addr {
        println!("{:#x}", addr);
    }
}

pub fn scan_ptr() {
    let mut map = BTreeMap::new();
    let contents = fs::read_to_string("/proc/3927319/maps").unwrap();
    let pages = PageIter::new(&contents)
        .filter(|m| m.is_read() && !matches!(m.name(), |"[vvar]"| "[vdso]" | "[vsyscall]"))
        .map(|m| (m.start(), m.size()))
        .collect::<Vec<_>>();
    let file = File::open("/proc/3927319/mem").unwrap();

    let mut buf = vec![0; 0x100000];
    for &(start, size) in &pages {
        for off in (0..size).step_by(0x100000) {
            let size = file.read_at(&mut buf, start + off).unwrap();
            for (k, v) in buf[..size].windows(8).enumerate() {
                let addr = start + off + k as u64;
                let out_addr = u64::from_le_bytes(v.try_into().unwrap());
                if pages.iter().any(|&(a, s)| out_addr >= a && out_addr < a + s) {
                    map.insert(addr, out_addr);
                }
            }
        }
    }

    pointer_search(&map, 0x7ffcf116c4c0, 0x560d66743ba0, 32, 1, 3, &mut String::new());
}

pub fn pointer_search(
    map: &BTreeMap<u64, u64>,
    saddr: u64,
    taddr: u64,
    range: u64,
    depth: usize,
    max_dep: usize,
    tmp: &mut String,
) {
    let max = saddr.saturating_add(range);

    for (&k, &v) in map.range((Included(&saddr), Included(&max))) {
        write!(tmp, "+{:#x}={:#x}->{:#x}", k - saddr, k, v).unwrap();
        if v == taddr {
            println!("{tmp}");
            tmp.clear();
        }

        if depth < max_dep {
            pointer_search(map, v, taddr, range, depth + 1, max_dep, tmp);
        }
    }
}

fn main() {
    // scan_value();
    // scan_ptr();
}