Atlassian License 逆向分析

旧文新发

Java逆向环境搭建

安装JDK

首先安装JDK。目前JDK版本为12.0.1。逆向时有个工具需要jdk8环境,,jdk8已经停止维护不能免费下载,需要注册用户才能下载。

使用brew安装jenv,管理JDK版本。

1
2
3
4
5
6
7
8
9
brew install jenv
echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(jenv init -)"' >> ~/.zshrc
source ~/.zshrc
jenv add /Library/Java/JavaVirtualMachines/jdk-12.0.1.jdk/Contents/Home/
jenv add /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/
jenv global 1.8.0.201
jenv local 1.8.0.201
jenv shell 1.8.0.201

安装反编译工具

此处推荐两个工具:

其中,Luyten仅支持jdk8环境,jdk12 打开会闪退。因此在安装之后编辑启动脚本,将环境切换设置在脚本#!/bin/bash之后:

1
2
3
vim /Applications/Luyten.app/Contents/MacOS/universalJavaApplicationStub.sh
### Use jdk8
export JAVA_HOME=`/usr/libexec/java_home -v 1.8`

逆向license

使用Luyten打开atlassian-extras-3.1.2.jar,导航到com.atlassian.license.decoder.v2,打开Version2LicenseDecoder.class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.atlassian.extras.decoder.v2;// 省略部分代码
public class Version2LicenseDecoder extends AbstractLicenseDecoder
{    // 省略部分代码
try {
  final String pubKeyEncoded = "MIIBuDCCASwGByqG...";
  final KeyFactory keyFactory = KeyFactory.getInstance("DSA");
  PUBLIC_KEY = keyFactory.generatePublic(new X509EncodedKeySpec(Base64.decodeBase64("MIIBuDCCASwG...".getBytes())));
  // 省略部分代码
  } catch (Exception e) {
   // 省略部分代码
  }
}

我们看到公钥被硬编码在程序里面,替换或hook都可以达到修改的目的。

下面分析解码license的代码:

1
2
3
4
5
6
public Properties doDecode(final String licenseString) {
  final String encodedLicenseTextAndHash = this.getLicenseContent(removeWhiteSpaces(licenseString));
  final byte[] zippedLicenseBytes = this.checkAndGetLicenseText(encodedLicenseTextAndHash);
  final Reader licenseText = this.unzipText(zippedLicenseBytes);
  return this.loadLicenseConfiguration(licenseText);
}

流程如下:

  1. licenseString中去掉空白字符;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    private static String removeWhiteSpaces(final String licenseData) {
      if (licenseData == null || licenseData.length() == 0) {
        return licenseData;
      }
      final char[] chars = licenseData.toCharArray();
      final StringBuffer buf = new StringBuffer(chars.length);
      for (int i = 0; i < chars.length; ++i) {
        if (!Character.isWhitespace(chars[i])) {
          buf.append(chars[i]);
        }
      }
      return buf.toString();
    }
    
  2. licenseString中,从最后一个’X’的位置后3位开始到licenseString结束,该值以31进制转码为int类型,是license的长度,从licenseString头部取出该长度的string;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    private String getLicenseContent(final String licenseString) {
      final String lengthStr = licenseString.substring(licenseString.lastIndexOf(88) + 3);
      try {
        final int encodedLicenseLength = Integer.valueOf(lengthStr, 31);
        return licenseString.substring(0, encodedLicenseLength);
      }    catch (NumberFormatException e) {
        throw new LicenseException("Could NOT decode license length <" + lengthStr + ">", e);
      }
    }
    
  3. 使用base64转码第二步返回的string。前4byte定义了一个无符号整数,大端对齐,此处定义了License文本内容的长度,以此长度为分隔,前面是压缩后的License文本内容,后面是对文本内容进行签名的hash,公钥去验证License签名是否正确;

     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
    
    private byte[] checkAndGetLicenseText(final String licenseContent) {
      byte[] licenseText;
      try {
        final byte[] decodedBytes = Base64.decodeBase64(licenseContent.getBytes());
        final ByteArrayInputStream in = new ByteArrayInputStream(decodedBytes);
        final DataInputStream dIn = new DataInputStream(in);
        final int textLength = dIn.readInt();
        licenseText = new byte[textLength];
        dIn.read(licenseText);
        final byte[] hash = new byte[dIn.available()];
        dIn.read(hash);
        try {
            final Signature signature = Signature.getInstance("SHA1withDSA");
            signature.initVerify(Version2LicenseDecoder.PUBLIC_KEY);
            signature.update(licenseText);
            if (!signature.verify(hash)) {
              throw new LicenseException("Failed to verify the license.");
            }
          } catch (InvalidKeyException e) {
            throw new LicenseException(e);
          } catch (SignatureException e2) {
            throw new LicenseException(e2);
          } catch (NoSuchAlgorithmException e3) {
            throw new LicenseException(e3);
          }
        } catch (IOException e4) {
          throw new LicenseException(e4);
        }
        return licenseText;
      }
    
  4. 跳过licenseText前5个byte,将后面内容解压,输出以UTF-8编码的license内容;

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    private Reader unzipText(final byte[] licenseText) {
      final ByteArrayInputStream in = new ByteArrayInputStream(licenseText);
      in.skip(Version2LicenseDecoder.LICENSE_PREFIX.length);
      final InflaterInputStream zipIn = new InflaterInputStream(in, new Inflater());
      try {
        return new InputStreamReader(zipIn, "UTF-8");
      } catch (UnsupportedEncodingException e) {
        throw new LicenseException(e);
      }
    }
    
  5. 最后通过明文的license实例化证书。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    private Properties loadLicenseConfiguration(final Reader text) {
      try {
        final Properties props = new Properties();
        new DefaultPropertiesPersister().load(props, text);
        return props;
      } catch (IOException e) {
        throw new LicenseException("Could NOT load properties from reader", e);
      }
    }
    

使用node实现证书解析

按照之前的分析写代码就可以了,实现如下:

 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
var zlib = require('zlib');
let licenseString = `AAABYg0ODAoPeNp1UstugzAQvPsr8gElgqTkUclSU/CBFggC2lOlyiWbxBUYuoao6dfXJETN82DJu+Odndk1cUFlKKpalJI+e/GsFwEuS8zg/aHHNjxveAsRB2F3cXkNdGBaU8O0DXNEnFLWPKtDXgAtxHZsTsbDx/I359/9rCxI1ZF9VHmzErLviwykgnRbwa7EmQcBix1v5h+YWMBFfo3qXwxd8lzBBXdSc6wBb6CaWmyA1tgAyfcq3gBVSzcguqesQXKZAfupBG47m1Ntc6SdkjmuuBRq3z5lSUoSwA2g59Knl3lqWFPTNdzZJDDCJAxIwkKqj+FbQ9u+H9v2hZywKT4B58tXpUVQwyJfAvll9mbCyRulzYblAhS1zNPyczDSg5E6bifeij+KSbcRbcT33FPNHXR9IlGD2ZorOP8QKwSQ67LShg/bZguxmxsLUxZHsZewk1dHm7kjtz9Ij/wBpr7pzTAsAhRU3M1pirOrl/o6iXQ1sMiQwlVTDwIUWTs/PMWbkDk2dnXJ+bqKk9n4YgU=X02hd`;
function removeWhiteSpaces(str) {
  let cleanStr = '';
  for (let index = 0; index < str.length; index++) {
    const element = str[index];
    if (/\\S/.test(element)) {
      cleanStr = cleanStr + element;
    }
  }
  return cleanStr;
}
function getLicenseContent(str) {
  let lengthStr = str.substring(str.lastIndexOf('X') + 3);
  let length = parseInt(lengthStr, 31);
  let licenseStr = str.substring(0, length);
  return licenseStr;
}
function base64_decode(str) {
  let buf = new Buffer.from(str, 'base64');
  let length = buf.readUIntBE(0, 4);
  let licenseBuf = buf.slice(4, 4 + length);
  return licenseBuf;
}
function unzip(buf) {
  let licenseText = zlib.unzipSync(buf.slice(5)).toString();
  return licenseText;
}
let licenseText = unzip(
  base64_decode(getLicenseContent(removeWhiteSpaces(licenseString)))
);
console.log(licenseText);
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy