Java内存马样本分析

Java内存马样本分析

mmj Lv2

拿到手是一个匿名类,反编译后发现调用大量Java反射,方法名几乎全部混淆为无意义字符“lllIlIllII”

经过分析MainActivity将m57重命名为decrypt_name,其余被调用方法根据功能分别重命名,查看逻辑

image.png

难点在于

1
String temp_str = toString(append(new StringBuilder(), getMethodName(getStackTrace(new RuntimeException())[1])));

temp_str的取值。写个Java调用逻辑打印其值

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.lang.reflect.Method;

/**
* 示例:通过反射获取调用栈信息和方法名
*/
public class test1 {

// 使用反射调用 RuntimeException 的 getStackTrace 方法,获取调用栈数组
public static StackTraceElement[] getStackTrace(Object obj) throws Exception {
Method method = RuntimeException.class.getMethod("getStackTrace");
method.setAccessible(true);
return (StackTraceElement[]) method.invoke(obj);
}

// 使用反射调用 StackTraceElement 的 getMethodName 方法,获取方法名
public static String getMethodName(Object obj) throws Exception {
Method method = StackTraceElement.class.getMethod("getMethodName");
method.setAccessible(true);
return (String) method.invoke(obj);
}

// 使用反射调用 StringBuilder 的 append 方法,将字符串追加到 StringBuilder
public static StringBuilder append(Object obj, String str) throws Exception {
Method method = StringBuilder.class.getMethod("append", String.class);
method.setAccessible(true);
return (StringBuilder) method.invoke(obj, str);
}

// 使用反射调用 StringBuilder 的 toString 方法,转为字符串输出
public static String toString(Object obj) throws Exception {
Method method = StringBuilder.class.getMethod("toString");
method.setAccessible(true);
return (String) method.invoke(obj);
}

// 主入口函数,调用 main1
public static void main(String[] args) {
main1(args);
}

// 主功能函数:输出调用栈中所有方法名以及第 2 层方法名
public static void main1(String[] args) {
try {
// 获取当前调用栈
StackTraceElement[] stackTrace = getStackTrace(new RuntimeException());

// 使用 StringBuilder 构建方法名列表字符串
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement stackTraceElement : stackTrace) {
append(stringBuilder, getMethodName(stackTraceElement));
append(stringBuilder, "\n");
}

// 输出所有方法名
System.out.println(toString(stringBuilder));

// 单独输出调用栈中第 2 个方法的名称
String temp_str = toString(
append(new StringBuilder(), getMethodName(getStackTrace(new RuntimeException())[1]))
);
System.out.println(temp_str);

} catch (Exception e) {
e.printStackTrace();
}
}
}

发现temp_str是调用decrypt_name函数的那个方法的方法名。也就是说,

这个函数实现了不同方法调用相同函数,key不同

因此遍历class文件里的方法名

写python解密发现方法名里的不可见字符其实就是\n

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
27
28
29
def decrypt_name(enc, method_name):
# method_name 的长度(减1用于索引)
length = len(method_name) - 1
# enc 为加密字符串,length2 是其最后一个索引
length2 = len(enc) - 1
# 初始化解密后的字符数组
temp = ["" for _ in range(length2 + 1)]

while length2 >= 0:
# 解密当前字符
temp[length2] = chr((ord("k") ^ ord(enc[length2])) ^ ord(method_name[length]))

if length2 - 1 < 0:
break # 避免数组越界

# 解密前一个字符
temp[length2 - 1] = chr(
(ord("P") ^ ord(enc[length2])) ^ ord(method_name[length])
)

# method_name 索引左移
length -= 1
if length < 0:
length = len(method_name) - 1 # 重置索引为末尾,实现循环使用

length2 -= 2 # 每次处理两个字符

return "".join(temp)

接下来一个个解密出来,重命名

从生成method_list_enc的函数中,先调用decrypt_name解密出来method_list_enc中的每一项,然后使用不同的参数进行des解密每一项,最后从method_list中拿到要反射调用的函数名。最后一个个代回去,就可以恢复代码的逻辑

结果发现这一层只是gzip解压了一个加密过的class文件,后续内存马逻辑在另一个class里,分析思路与上述相同,解出来是一个RSA

遇到的主要难点

其大量使用“动态“的不同的key,使得几乎无法进行批量的解混淆工作。对于RuntimeException的调用栈的使用配合方法名的不可打印字符混淆,是阻碍解混淆工作的一大障碍,且利用调用栈和方法名混淆**:** 调用栈是运行时信息,方法名可能包含不可打印字符

后续不同的method使用的DES密钥不同,由下标控制,是解混淆工作的第二重障碍。通过对这个内存马样本的分析,发现增加混淆过程与程序执行流的耦合程度,可以较好地对抗批量解混淆的自动化脚本

  • 标题: Java内存马样本分析
  • 作者: mmj
  • 创建于 : 2025-05-15 05:42:38
  • 更新于 : 2025-05-15 13:54:26
  • 链接: https://samzhaohx.github.io/2025/05/15/某Java内存马样本分析/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
Java内存马样本分析