虚拟机逃逸初探----2018rwctf_station-escape
这里从头开始,一般出现在ctf的虚拟机逃逸会有俩个vmx,一个是原来正常的,另一个是patch过的
使用010editor比较俩个文件的异同
有俩处不同,拖进ida,找到对应的地址看看改了啥
查看被patch过的地址
在0x1893c9(379)指针置null的操作被nop掉了
另一个在0x1893e6(383)处的函数调用中,replay_type&1变成了v7&0x21
+7对应的成员out_msg_buf
如果该句被nop掉,out_msg_buf指针没有被置NULL,其次在第二处patch中,原先被限制的reply type(&0x1)变成了&0x21,也就是说在finish_recv_func(sub_177700)中可以存在free部分再次释放out_msg_buf
这个patch会导致UAF:如果我们在接收完成之后设置了0x21这个位,那么output buffer就会被释放掉,但由于它没有被清零,所以理论上我们可以无限次的将它free掉。可以通过 多次执行 case 5释放 输出缓冲区,最后制造一个 double free的漏洞。然后利用 info-get和 info-set命令,实现堆块的分配。利用RPC的发送命令和接受返回来实现堆块布局
这里的思路借鉴长亭师傅
利用思路
Leak:
1. 开两个channel:A和B,A的output buffer为buf_A,然后A释放buf_A
2. 这时让B准备给guest发output,B会分配一个buffer,我们利用info-set和info-get来控制我们分配的buffer大小,使得B的output buffer: buf_B=buf_A。
3. A再次释放buf_A,这也导致了buf_B被释放。这个时候我们就可以leak出buf_B的fd了,但是这个指针没有什么用,我们想要的是text base。
4. 因此我们再执行命令vmx.capability.dnd_version,这会让host分配一块内存来存放一个obj,通过控制buffer大小我们可以刚好让buf_B被用来存放一个obj。而这个obj里面有vtable,我们可以leak出来计算text base。注意我们一直没有接受B的输出,只是让它做好准备(分配output buffer)。直到这个时候我们才接受它的输出,完成leak
Exploit
有了leak的方法,exploit的也是类似的了。简单来说就是UAF,把tcache的fd改到bss段,然后改函数指针为system,最后弹calculator
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke
这里从头开始,一般出现在ctf的虚拟机逃逸会有俩个vmx,一个是原来正常的,另一个是patch过的
使用010editor比较俩个文件的异同
有俩处不同,拖进ida,找到对应的地址看看改了啥
查看被patch过的地址
在0x1893c9(379)指针置null的操作被nop掉了
另一个在0x1893e6(383)处的函数调用中,replay_type&1变成了v7&0x21
+7对应的成员out_msg_buf
如果该句被nop掉,out_msg_buf指针没有被置NULL,其次在第二处patch中,原先被限制的reply type(&0x1)变成了&0x21,也就是说在finish_recv_func(sub_177700)中可以存在free部分再次释放out_msg_buf
这个patch会导致UAF:如果我们在接收完成之后设置了0x21这个位,那么output buffer就会被释放掉,但由于它没有被清零,所以理论上我们可以无限次的将它free掉。可以通过 多次执行 case 5释放 输出缓冲区,最后制造一个 double free的漏洞。然后利用 info-get和 info-set命令,实现堆块的分配。利用RPC的发送命令和接受返回来实现堆块布局
这里的思路借鉴长亭师傅
利用思路
Leak:
1. 开两个channel:A和B,A的output buffer为buf_A,然后A释放buf_A
2. 这时让B准备给guest发output,B会分配一个buffer,我们利用info-set和info-get来控制我们分配的buffer大小,使得B的output buffer: buf_B=buf_A。
3. A再次释放buf_A,这也导致了buf_B被释放。这个时候我们就可以leak出buf_B的fd了,但是这个指针没有什么用,我们想要的是text base。
4. 因此我们再执行命令vmx.capability.dnd_version,这会让host分配一块内存来存放一个obj,通过控制buffer大小我们可以刚好让buf_B被用来存放一个obj。而这个obj里面有vtable,我们可以leak出来计算text base。注意我们一直没有接受B的输出,只是让它做好准备(分配output buffer)。直到这个时候我们才接受它的输出,完成leak
Exploit
有了leak的方法,exploit的也是类似的了。简单来说就是UAF,把tcache的fd改到bss段,然后改函数指针为system,最后弹calculator
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke
主要看的部分主要是leak()和 expilot()部分,channel_recv_finish2()函数就是把rbx改为0x21最后会执行到free操作
Run_cmd()
发送命令进行执行
Leak()
初始化操作,这里使用 info-set命令发送了0x100的信息,在后续只要使用info-get命令就可以执行malloc(0x100)的操作,这里0x100是因为dnd_version为4时会分配一个0x100的Obj。
第一步:开启通道0,此时通道0分配了buf为0x100
第二步:开启通道1,发送指令分了俩部分,在俩个发送指令之间释放了通道0的内存,此时再发送完指令后,通道1的内存块为buf
第三步,再次通过通道0释放buf(因为指针一直没有置0,就可以一直释放),执行"vmx.capability.dnd_version",在发送“vmx.capability.dnd_version” 命令的时候,对应的处理函数中如果发现当前版本和设置的版本不一致,就会调用函数创建新的 object,把原来的版本的object销毁。再开始时发送指令tools.capability.dnd_version 4,会使得这个分配的大小为0x100。此时的object就会到buf的位置。object里包含vtable的地址,再次通过通道1进行数据的返回(之前一直没有),进而得到数据的基地址
Expilt()
类似于leak,此时的buf安排的大小是0x150
开启四个通道,通道0先malloc出0x150的空间
此时通道0指向buf,第一次0释放,分配1通道,此时1指向buf,buf存在
第二次0释放,buf不存在,此时1通道进行写操作,当内存块不存在时,此时1通道写的位置其实是下一块的地址,pay1是一个在bss段上,且一定存在call的一个地址
Exp中的是FE95C8,可以对应下列的位置
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke
Run_cmd()
发送命令进行执行
Leak()
初始化操作,这里使用 info-set命令发送了0x100的信息,在后续只要使用info-get命令就可以执行malloc(0x100)的操作,这里0x100是因为dnd_version为4时会分配一个0x100的Obj。
第一步:开启通道0,此时通道0分配了buf为0x100
第二步:开启通道1,发送指令分了俩部分,在俩个发送指令之间释放了通道0的内存,此时再发送完指令后,通道1的内存块为buf
第三步,再次通过通道0释放buf(因为指针一直没有置0,就可以一直释放),执行"vmx.capability.dnd_version",在发送“vmx.capability.dnd_version” 命令的时候,对应的处理函数中如果发现当前版本和设置的版本不一致,就会调用函数创建新的 object,把原来的版本的object销毁。再开始时发送指令tools.capability.dnd_version 4,会使得这个分配的大小为0x100。此时的object就会到buf的位置。object里包含vtable的地址,再次通过通道1进行数据的返回(之前一直没有),进而得到数据的基地址
Expilt()
类似于leak,此时的buf安排的大小是0x150
开启四个通道,通道0先malloc出0x150的空间
此时通道0指向buf,第一次0释放,分配1通道,此时1指向buf,buf存在
第二次0释放,buf不存在,此时1通道进行写操作,当内存块不存在时,此时1通道写的位置其实是下一块的地址,pay1是一个在bss段上,且一定存在call的一个地址
Exp中的是FE95C8,可以对应下列的位置
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke
Gdb调试
先正常启动guest 然后在host机中启动
sudo gdb /usr/lib/vmware/bin/vmware-vmx -q
使用ps -aux | grep vmware-vmx得到进程pid
在启动的gdb 中attach上去
(ps:这里有概率失败,我的处理办法是重启再开一次)
获取此时vmware-vmx的基址
输入codebase
此时将断点设在patch的位置
通过ssh远程连接guest执行exp
此是到达了patch中nop的位置
第一次达到,查看堆信息.因为发了0x100字节的信息,再加上堆块的一些size等8字节信息,此时大小未0x110
该地址就是bufA的位置
最后会将基址写到bufA的位置,但这里很奇怪的是,没有挂gdb泄露出的基址是正确的,也可以弹出计算器,但有时gdb一步步调试,泄露出的基地址是不对的,最后执行也会失败,这种情况可以多调试几次。可以确定该位置就是buf,之后所有的操作都是基于这一块内存进行操作
最后就是类似地劫持fd,把tcache的fd改到bss段,然后改函数指针为system,最后弹calculator
到达expilot部分的内存
断点可以设置在很多地方
比如设在codebase+0x1895AF的地方,这里的rdx寄存器存放的就是此时写入的地址,因为每次只能写4个字节,调试消耗的时间比较久
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke
先正常启动guest 然后在host机中启动
sudo gdb /usr/lib/vmware/bin/vmware-vmx -q
使用ps -aux | grep vmware-vmx得到进程pid
在启动的gdb 中attach上去
(ps:这里有概率失败,我的处理办法是重启再开一次)
获取此时vmware-vmx的基址
输入codebase
此时将断点设在patch的位置
通过ssh远程连接guest执行exp
此是到达了patch中nop的位置
第一次达到,查看堆信息.因为发了0x100字节的信息,再加上堆块的一些size等8字节信息,此时大小未0x110
该地址就是bufA的位置
最后会将基址写到bufA的位置,但这里很奇怪的是,没有挂gdb泄露出的基址是正确的,也可以弹出计算器,但有时gdb一步步调试,泄露出的基地址是不对的,最后执行也会失败,这种情况可以多调试几次。可以确定该位置就是buf,之后所有的操作都是基于这一块内存进行操作
最后就是类似地劫持fd,把tcache的fd改到bss段,然后改函数指针为system,最后弹calculator
到达expilot部分的内存
断点可以设置在很多地方
比如设在codebase+0x1895AF的地方,这里的rdx寄存器存放的就是此时写入的地址,因为每次只能写4个字节,调试消耗的时间比较久
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke
在写eBPF程序时,对于bpf_map_lookup_elem()返回的结果,一定要判断是否为NULL,否则就会被验证器拒绝加载。这是因为bpf_map_lookup_elem()运行结果的结果有可能是NULL,这种情况表示没有查找到与key相关的值。如果不进行判断,那么接下来的代码就有可能引用了一个空指针,这是非常危险的操作。
那么,验证器是如何知道我们是否判断以及什么时候判断了结果是否为NULL呢?我们来看相关的实现代码。
通过代码我们可知它通过“*_OR_NULL”类型来表示一个未经NULL判断的指针类型。当寄存器的类型是“*_OR_NULL”时,他只能进行非常有限的操作。只有当类型为“*_OR_NULL”的寄存器做完NULL比较后,才可能变为普通的指针,也就是“PTR_TO_*”类型。
那么,验证器是如何知道我们是否判断以及什么时候判断了结果是否为NULL呢?我们来看相关的实现代码。
/* types of values stored in eBPF registers */
/* Pointer types represent:
* pointer
* pointer + imm
* pointer + (u16) var
* pointer + (u16) var + imm
* if (range > 0) then [ptr, ptr + range - off) is safe to access
* if (id > 0) means that some 'var' was added
* if (off > 0) means that 'imm' was added
*/
enum bpf_reg_type {
NOT_INIT = 0, /* nothing was written into register */
SCALAR_VALUE, /* reg doesn't contain a valid pointer */
PTR_TO_CTX, /* reg points to bpf_context */
CONST_PTR_TO_MAP, /* reg points to struct bpf_map */
PTR_TO_MAP_VALUE, /* reg points to map element value */
PTR_TO_MAP_VALUE_OR_NULL, /* points to map elem value or NULL */
PTR_TO_STACK, /* reg == frame_pointer + offset */
PTR_TO_PACKET_META, /* skb->data - meta_len */
PTR_TO_PACKET, /* reg points to skb->data */
PTR_TO_PACKET_END, /* skb->data + headlen */
PTR_TO_FLOW_KEYS, /* reg points to bpf_flow_keys */
PTR_TO_SOCKET, /* reg points to struct bpf_sock */
PTR_TO_SOCKET_OR_NULL, /* reg points to struct bpf_sock or NULL */
PTR_TO_SOCK_COMMON, /* reg points to sock_common */
PTR_TO_SOCK_COMMON_OR_NULL, /* reg points to sock_common or NULL */
PTR_TO_TCP_SOCK, /* reg points to struct tcp_sock */
PTR_TO_TCP_SOCK_OR_NULL, /* reg points to struct tcp_sock or NULL */
PTR_TO_TP_BUFFER, /* reg points to a writable raw tp's buffer */
PTR_TO_XDP_SOCK, /* reg points to struct xdp_sock */
/* PTR_TO_BTF_ID points to a kernel struct that does not need
* to be null checked by the BPF program. This does not imply the
* pointer is _not_ null and in practice this can easily be a null
* pointer when reading pointer chains. The assumption is program
* context will handle null pointer dereference typically via fault
* handling. The verifier must keep this in mind and can make no
* assumptions about null or non-null when doing branch analysis.
* Further, when passed into helpers the helpers can not, without
* additional context, assume the value is non-null.
*/
PTR_TO_BTF_ID,
/* PTR_TO_BTF_ID_OR_NULL points to a kernel struct that has not
* been checked for null. Used primarily to inform the verifier
* an explicit null check is required for this struct.
*/
PTR_TO_BTF_ID_OR_NULL,
PTR_TO_MEM, /* reg points to valid memory region */
PTR_TO_MEM_OR_NULL, /* reg points to valid memory region or NULL */
PTR_TO_RDONLY_BUF, /* reg points to a readonly buffer */
PTR_TO_RDONLY_BUF_OR_NULL, /* reg points to a readonly buffer or NULL */
PTR_TO_RDWR_BUF, /* reg points to a read/write buffer */
PTR_TO_RDWR_BUF_OR_NULL, /* reg points to a read/write buffer or NULL */
PTR_TO_PERCPU_BTF_ID, /* reg points to a percpu kernel variable */
};
通过代码我们可知它通过“*_OR_NULL”类型来表示一个未经NULL判断的指针类型。当寄存器的类型是“*_OR_NULL”时,他只能进行非常有限的操作。只有当类型为“*_OR_NULL”的寄存器做完NULL比较后,才可能变为普通的指针,也就是“PTR_TO_*”类型。
其中adjust_ptr_min_max_vals()是eBPF验证其用于检验指针加减运算的函数。这段代码使用switch来过滤不支持加减运算的指针类型,比如各种“*_OR_NULL”类型。但是这段代码里却少了很多类型的判断。这意味着,我们可以对这些少了的类型做加减运算,其中就包括一部分“*_OR_NULL”类型。
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke
提权过程:
int spawn_processes(context_t *ctx)
{
for (int i = 0; i < PROC_NUM; i++)
{
pid_t child = fork();
if (child == 0) {
if (prctl(PR_SET_NAME, __ID__, 0, 0, 0) != 0) {
WARNF("Could not set name");
}
uid_t old = getuid();
kill(getpid(), SIGSTOP);
uid_t uid = getuid();
if (uid == 0 && old != uid) {
OKF("Enjoy root!");
system("/bin/sh");
}
exit(uid);
}
if (child < 0) {
return child;
}
ctx->processes[i] = child;
}
return 0;
}
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke
JNI (Java Native Interface,JAVA 本地接口) 允许 Java 代码和其它编程语言编写的代码进行交互,主要为Java和Native层(C/C++)相互调用的接口规范,但是并不妨碍扩展其他语言。 JNI 在 Java1.1 中正式推出,在 Java1.2 中加入了JNI_OnLoad和JNI_OnUnload方法。
JNI 设计交互 Native层 C/C++ 代码的主要优点为:特定功能场景下,提升应用性能;代码层保护,增加反编译难度;Native库文件,可重复使用及不同项目间扩展移植。说完优点那么唯一的一个缺点就是C/C++代码编译生成的动态链接库不可跨平台使用。
正常情况下编写的Java代码是不能直接调用C/C++代码,需要通过JNI标准接口进行间接调用访问。Java代码要想使用JNI调用Native层C/C++代码就需要先向JVM注册Native函数,其注册主要分为静态注册和动态注册两大类,通过注册可实现 Java Native 方法与 C/C++ 方法的对应关系。静态和动态注册主要区别在于查找效率:静态注册,首次调用Java Native方法时,会按照JNI命名规则去寻找;而动态注册,由于存在一张映射表JNINativeMethod,因此查找效率高。最终,无论静态注册方法,还是动态注册方法,都需要将相应的C/C++文件编译成平台所需要的动态库。
利用 javah 生成编译native库所需的相应 .h 头文件,javah 会自动识别 class 文件里声明的 java native 本地方法并进行解析处理生成相应 .h 头文件 (也可以根据JNI静态注册规则手动编写头文件)
查看 javah 处理生成的 IntSum.h 头文件并进行分析,其中定义了一个JNI Native调用的 C/C++方法Java_org_qftm_learn_jni_demo1_IntSum_sums,可以看到方法结构与Java方法类似,同样包含方法名、参数、返回类型、修饰符
默认javah解析生成的JNI头文件中,C/C++ 函数都为JNI Native对应的静态注册函数,其通过 JNIEXPORT 和 JNICALL 两个宏定义声明。除了两个宏定义外,函数还存在返回类型和方法名及函数参数,其中静态注册函数名遵循特定JNI命名规范,即为Java_<PackageName>_<ClassName>_<MethodName>,这里需要注意下,如果Java代码声明的本地方法名称中本来就包含下划线,那么该部分将使用下划线加数字替换。另外会发现函数多了两个类型参数JNIEnv *和jobject,这两个参数由javah自动添加,并且其参数值由虚拟机自动传入。
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke
JNI 设计交互 Native层 C/C++ 代码的主要优点为:特定功能场景下,提升应用性能;代码层保护,增加反编译难度;Native库文件,可重复使用及不同项目间扩展移植。说完优点那么唯一的一个缺点就是C/C++代码编译生成的动态链接库不可跨平台使用。
正常情况下编写的Java代码是不能直接调用C/C++代码,需要通过JNI标准接口进行间接调用访问。Java代码要想使用JNI调用Native层C/C++代码就需要先向JVM注册Native函数,其注册主要分为静态注册和动态注册两大类,通过注册可实现 Java Native 方法与 C/C++ 方法的对应关系。静态和动态注册主要区别在于查找效率:静态注册,首次调用Java Native方法时,会按照JNI命名规则去寻找;而动态注册,由于存在一张映射表JNINativeMethod,因此查找效率高。最终,无论静态注册方法,还是动态注册方法,都需要将相应的C/C++文件编译成平台所需要的动态库。
package org.qftm.learn.jni.demo1;
/**
* Created by IntelliJ IDEA.
* User: Qftm
* Date: 2022/5/11
* Time: 17:36
*/
public class IntSum {
// 声明 native 本地函数
public native int sums(int num1, int num2);
// 静态代码块,加载动态链接库
static {
System.loadLibrary("IntSum");
}
public static void main(String[] args) {
// 实例化调用 native 本地函数
System.out.println((new IntSum()).sums(10, 5));
}
}利用 javah 生成编译native库所需的相应 .h 头文件,javah 会自动识别 class 文件里声明的 java native 本地方法并进行解析处理生成相应 .h 头文件 (也可以根据JNI静态注册规则手动编写头文件)
查看 javah 处理生成的 IntSum.h 头文件并进行分析,其中定义了一个JNI Native调用的 C/C++方法Java_org_qftm_learn_jni_demo1_IntSum_sums,可以看到方法结构与Java方法类似,同样包含方法名、参数、返回类型、修饰符
默认javah解析生成的JNI头文件中,C/C++ 函数都为JNI Native对应的静态注册函数,其通过 JNIEXPORT 和 JNICALL 两个宏定义声明。除了两个宏定义外,函数还存在返回类型和方法名及函数参数,其中静态注册函数名遵循特定JNI命名规范,即为Java_<PackageName>_<ClassName>_<MethodName>,这里需要注意下,如果Java代码声明的本地方法名称中本来就包含下划线,那么该部分将使用下划线加数字替换。另外会发现函数多了两个类型参数JNIEnv *和jobject,这两个参数由javah自动添加,并且其参数值由虚拟机自动传入。
/网站渗透 /黑客编程技术
/软件渗透 /提取数据
/网站权限 /网站入侵破解
有需求的找我 @MoA_Ke