FXXXXRXXXXX 10.0 注册分析
本帖最后由 benzcomp 于 2019-8-25 12:09 编辑这两天发现FR10的加密方式又改了,那就放出以前版本的注册分析,适用于2019.08.16以前版本!
先说结论:
1、授权文件采用RSA加密,理论上无法PJ,要想PJ只能替换密钥。但是,验证过程使用了解密后的明码比较来验证注册码的合法性,所以,理论上可以通过修改验证函数,输入明码license格式文件,跳过RSA加解密过程;
2、FR10再次修改了关键文件加密方式,9.0版使用了100个类加载器递归解密,只有反编译所有的加载器才能知道授权内容。10.0版把解密功能编译成动态库文件了,所有核心文件使用同一密钥,不再递归了。
3、授权核心代码采用全部存储在com.fr.plugin.A目录下,后缀名为classx,这些文件实际上是加密的class文件,部分注册验证文件解密出来class文件不是授权类,是一个描述授权核心类的内文件,这些字符通过javassist工具动态生成类;
4、PJ方法就是修改验证函数所在类,然后,反向加密成classx文件,替换同名文件。
查找注册验证入口请参考关于9.0的帖子(https://www.52pojie.cn/thread-819149-1-1.html),下面只说代码分析部分:
1、负责解密操作的动态库文件在 fine-core-10.0.jar!\com.fr.jvm.assist下
对应不同系统和java环境,\com\fr\jvm\assist 中共有7个,FineAssist.dylib、FineAssist_x86.dll、FineAssist_x86.so、FineAssist_x86_64.dll、FineAssist_x86_64.so、FineAssist_x86_64_ibm.so、FineAssist_x86_ibm.so;
2、JVM的动态调试不会搞,只好用IDA静态分析,所有文件肯定包含一样的解密算法,随意挑一个最小的文件FineAssist_x86.so就可以;
从函数名称可以看出来MyClassFileLoadHook、loadPlainClass、loadSpecialEncryptClass是关于类加载的函数,MyClassFileLoadHook是Hook、loadPlainClass加载普通类、loadSpecialEncryptClas加载特殊类:
int __cdecl MyClassFileLoadHook(_jvmtiEnv *a1, int a2, int a3, int a4, char *s1, int a6, __int64 a7, unsigned __int8 *a8, unsigned __int8 **a9)//*s1 是全路径名, a7 是文件起始地址
{
unsigned __int8 **v10; //
unsigned __int8 **v11; //
if ( !s1 || strncmp(s1, "com/fr/", 7u) || !strncmp(s1, "com/fr/third/", 0xDu) )
return loadPlainClass(a1, a7, a8, (const unsigned __int8 *)a9, v10);
if ( !strncmp(s1, "com/fr/plugin/engine/", 0x15u) || !strncmp(s1, "com/fr/plugin/A/", 0x10u) )
return loadSpecialEncryptClass(a1, a7, (const unsigned __int8 *)HIDWORD(a7), (int *)a8, a9);
if ( search((const char **)&LIST, s1) != 0 )
return loadSpecialEncryptClass(a1, a7, (const unsigned __int8 *)HIDWORD(a7), (int *)a8, a9);
//验证文件前4个字节是否为 不是就调用普通类加载函数loadPlainClass,是就调用loadSpecialEncryptClass特殊类处理函数。
//也就是说所有被加密处理过的文件头都应该是 ,这一点查看classx文件的HEX码也得到了验证。
if ( *(_BYTE *)HIDWORD(a7) != 3 || *(_BYTE *)(HIDWORD(a7) + 1) != 5 || *(_BYTE *)(HIDWORD(a7) + 2) != 7 || *(_BYTE *)(HIDWORD(a7) + 3) != 9 )
{
return loadPlainClass(a1, a7, a8, (const unsigned __int8 *)a9, v11);
}
return loadSpecialEncryptClass(a1, a7, (const unsigned __int8 *)HIDWORD(a7), (int *)a8, a9);
}
3、loadSpecialEncryptClass:
特殊类加载,其中 a2 是需要解密的 classx 文件长度,a3 是 classx 的起始地址unsigned int __cdecl loadSpecialEncryptClass(_jvmtiEnv *a1, int a2, const unsigned __int8 *a3, int *a4, unsigned __int8 **a5)
{
void *v5; // esp
unsigned __int8 **v7; //
int *v8; //
const unsigned __int8 *v9; //
_jvmtiEnv *v10; //
int i; //
int j; //
__int64 v13; //
unsigned __int8 ***v14; //
unsigned __int8 *v15; //
unsigned int v16; //
v10 = a1;
v9 = a3;
v8 = a4;
v7 = a5;
v16 = __readgsdword(0x14u);
LODWORD(v13) = a2 - 4;
HIDWORD(v13) = a2 - 5;
v5 = alloca(16 * ((a2 - 5 + 16) / 0x10u));
v14 = &v7;
for ( i = 0; i < (signed int)v13; ++i )
*((_BYTE *)v14 + i) = v9; //v9=a3(classx的地址)从第5字节开始,存入v14,从上一步可以知道前4字节只是加密文件标记,不参与解密计算
*v8 = v13;
_jvmtiEnv::Allocate(v10, (signed int)v13, v7);
v15 = *v7;
for ( j = 0; j < (signed int)v13; ++j )
v15 = *((_BYTE *)v14 + j);
x(v15, v13); //x函数就是解密过程
return __readgsdword(0x14u) ^ v16;
}
4、解密函数,其实就是个字节异或处理a1是加密数据,a2是密钥(8字节)
int __cdecl x(unsigned __int8 *a1, int a2)
{
int result; // eax
int i; //
for ( i = 0; ; ++i )
{
result = i;
if ( i >= a2 )
break;
a1 ^= T;//每8个字节为一组
}
return result;
}
至此,就已经完成classx到class的解密了。
5、从以上分析知道,解密classx文件的步骤其实很简单:从classx文件的第5字节开始,8字节为一组,与异或密钥数组计算,就可以得到解密数据。
那么,问题来了,这个异或密钥是什么?
大家知道标准的class头文件前8个字节是固定的 ,所以,只要把classx的第5-12字节拿来逐字节异或运算一下就可以得到密钥了。
6、注册码验证在com.fr.plugin.A.N.A.C.B.classx(不同版本的位置可能会不同),解密得到class文件,反编:
class DynaClass {
public static String D() {
try {
return new String(new byte[] { (byte) 99, (byte) 111, (byte) 109, (byte) 46, (byte) 102, (byte) 114,
(byte) 46, (byte) 108, (byte) 105, (byte) 99, (byte) 101, (byte) 110, (byte) 115, (byte) 101,
(byte) 46, (byte) 115, (byte) 101, (byte) 108, (byte) 101, (byte) 99, (byte) 116, (byte) 111,
(byte) 114, (byte) 46, (byte) 69, (byte) 110, (byte) 99, (byte) 114, (byte) 121, (byte) 112,
(byte) 116, (byte) 101, (byte) 100, (byte) 76, (byte) 105, (byte) 99, (byte) 101, (byte) 110,
(byte) 115, (byte) 101, (byte) 83, (byte) 101, (byte) 108, (byte) 101, (byte) 99, (byte) 116,
(byte) 111, (byte) 114 }, "UTF-8");
} catch (Throwable localThrowable) {
}
return "";
}
public static byte[] E() {
byte[] arrayOfByte = new byte;
arrayOfByte = A(arrayOfByte, getByteCode0());
arrayOfByte = A(arrayOfByte, getByteCode1());
arrayOfByte = A(arrayOfByte, getByteCode2());
arrayOfByte = A(arrayOfByte, getByteCode3());
arrayOfByte = A(arrayOfByte, getByteCode4());
return arrayOfByte;
}
public String A() {
return "dc1473c8b718c3f144d3f443517d2c44";
}
private static byte[] getByteCode0() {
return new byte[] { (byte) -54,…… };
}
private static byte[] getByteCode1() {
return new byte[] { (byte) 101, ……};
}
private static byte[] getByteCode2() {
return new byte[] { (byte) -126, …… };
}
private static byte[] getByteCode3() {
return new byte[] { (byte) 47, …… };
}
private static byte[] getByteCode4() {
return new byte[] { (byte) 0, …… };
}
}
通过byte字节数组拼接出com.fr.license.selector.EncryptedLicenseSelector.class文件,这里拼接出的class文件,其实还是加密过得classx格式文件,再进行一次解密才能得到真正的class。
7、反编com.fr.license.selector.EncryptedLicenseSelector.class就到了真正的license验证入口:
public abstract class EncryptedLicenseSelector
extends AbstractLicenseSelector
{
private static final String _0eca828a_77d6_428a_9a97_91c04b434f4a = "RSA";
private static final int _9703cf65_25a0_49a0_91ed_738941a0738d = 128;
private static final ExecutorService _2e5864b3_2147_4738_9fbf_edfc852e40ae = ;
byte[] getBytes()
{
byte[] arrayOfByte = readRawBytes();
return decrypt(arrayOfByte); //改成 return arrayOfByte 就可以跳过RSA解密,直接使用明码lic文件验证了。
}
private byte[] decrypt(byte[] paramArrayOfByte)
{
try
{
System.getProperties().setProperty("com.ibm.crypto.provider.DoRSATypeChecking", "false");
X509EncodedKeySpec localX509EncodedKeySpec = new X509EncodedKeySpec(LicenseConstants.KEY);
KeyFactory localKeyFactory = KeyFactory.getInstance("RSA");
PublicKey localPublicKey = localKeyFactory.generatePublic(localX509EncodedKeySpec);
Cipher localCipher = Cipher.getInstance("RSA");
localCipher.init(2, localPublicKey);
return dealFragment(paramArrayOfByte, localCipher);
}
catch (Throwable localThrowable)
{
_2e5864b3_2147_4738_9fbf_edfc852e40ae.execute(new EncryptedLicenseSelector.1(this));
decryptFailed();
}
return new byte;
}
protected void decryptFailed()
{
FRLoggerFactory.getLogger().error("Read license failed.");
}
abstract byte[] readRawBytes();
private static byte[] dealFragment(byte[] paramArrayOfByte, Cipher paramCipher)
throws IllegalBlockSizeException, BadPaddingException
{
byte[] arrayOfByte1 = new byte;
for (int i = 0; i < paramArrayOfByte.length; i += 128)
{
byte[] arrayOfByte2 = ArrayUtils.subarray(paramArrayOfByte, i, i + 128);
byte[] arrayOfByte3 = paramCipher.doFinal(arrayOfByte2);
arrayOfByte1 = ArrayUtils.addAll(arrayOfByte1, arrayOfByte3);
}
return arrayOfByte1;
}
}
8、通过class二进制编辑器修改com.fr.license.selector.EncryptedLicenseSelector.class,或者你自己编译一个com.fr.license.selector.EncryptedLicenseSelector.class文件,按照解密的反向步骤:加密com.fr.license.selector.EncryptedLicenseSelector.class——用加密过的com.fr.license.selector.EncryptedLicenseSelector.class的字节流数组替换com.fr.plugin.A.N.A.C.B.class对应代码——加密成classx——替换com.fr.plugin.A.N.A.C.B.classx——破解完成。
9、用如下内容的注册文件注册即可
{"VERSION":"10.0","DEADLINE":4102444799499,"CONCURRENCY":"0"} public static byte[] Xor(byte[] data, byte[] key)
{
for (int i = 0;; i++) {
if (i >= data.length)
break;
data ^= key;
}
return data;
}
{:1_918:}{:1_918:}{:1_918:}{:1_918:} 什么情况 本帖最后由 heiketian10 于 2020-9-9 13:14 编辑
新版本不错 说给自己听列?这样也可以灌水? 临时有事,还没编辑完,明天再发 这个软件加密变来变去,也真是奇葩。期待发出分析 结果。。。 坐等更新哦,楼主{:1_893:} 楼主能给个9的破解版吗。目前只有无限试用版 8月份版本改成 AES-256 加密了,但是每个文件留了一个小尾巴,算是一个小陷阱吧。 Sounds interesting ...
Let's see what's going on there.