mirror of
https://github.com/JPCERTCC/aa-tools
synced 2026-06-08 11:22:15 +00:00
430 lines
16 KiB
Python
430 lines
16 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# Copyright (C) 2016 JPCERT Coordination Center. All Rights Reserved.
|
|
#
|
|
# LICENSE
|
|
# Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/aa-tools/
|
|
#
|
|
|
|
import re
|
|
import sys
|
|
import os
|
|
import io
|
|
import codecs
|
|
import zipfile
|
|
import tempfile
|
|
import subprocess
|
|
import shutil
|
|
|
|
def usage(decoder_py):
|
|
for s in [
|
|
'Usage:',
|
|
' python ' + decoder_py + ' "sample.jar"',
|
|
' - output decoded strings to stdout',
|
|
'',
|
|
' python ' + decoder_py + ' "sample.jar" "output.jasm"',
|
|
' - output javap disassembly with decoded strings',
|
|
'',
|
|
' python ' + decoder_py + ' "sample.jar" "source folder" "output folder"',
|
|
' - decode strings in decompiled source files etc.',
|
|
' - non-fully qualified method names are not supported.',
|
|
'',
|
|
'Requirements:',
|
|
' javap in JDK (Java Development Kit)',
|
|
' python 2.7 or later is recommended for unzip capability',
|
|
]:
|
|
print(s)
|
|
|
|
def interpret_key(jasm):
|
|
key = {}
|
|
func = None
|
|
jasm.seek(0)
|
|
for line in jasm:
|
|
line = line.decode('U8')
|
|
if re.match('[pfcs]', line[0]):
|
|
jclass = False
|
|
for s in line.split():
|
|
if jclass:
|
|
jclass = s
|
|
break
|
|
jclass = (s == 'class')
|
|
elif re.match(' public static java\.lang\.String .*\(java\.lang\.String\);', line):
|
|
func = jclass.replace('.', '/') + '.' + line.split()[-1].split('(')[0] + ':(Ljava/lang/String;)Ljava/lang/String;'
|
|
key1 = None
|
|
key2 = None
|
|
jstack = []
|
|
jlocal = [0] * 20
|
|
elif func:
|
|
op = line.split()
|
|
if len(op) < 2:
|
|
continue
|
|
try:
|
|
if op[1] == 'new':
|
|
jstack.append([])
|
|
elif op[1] == 'newarray':
|
|
jstack.pop()
|
|
jstack.append([])
|
|
elif op[1] == 'invokespecial':
|
|
if re.match('java/lang/(Throwable|Exception)\."<init>":\(\)V', op[5]):
|
|
jstack.pop()
|
|
elif op[5] == 'java/lang/StringBuffer."<init>":(Ljava/lang/String;)V':
|
|
key1 = [jstack.pop()]
|
|
jstack.pop()
|
|
elif op[1] == 'invokevirtual':
|
|
if re.match('java/lang/(Throwable|Exception)\.getStackTrace:\(\)\[Ljava/lang/StackTraceElement;', op[5]):
|
|
jstack.pop()
|
|
jstack.append([])
|
|
key2 = []
|
|
elif op[5] == 'java/lang/StackTraceElement.getClassName:()Ljava/lang/String;':
|
|
jstack.pop()
|
|
jstack.append('c')
|
|
elif op[5] == 'java/lang/StackTraceElement.getMethodName:()Ljava/lang/String;':
|
|
jstack.pop()
|
|
jstack.append('m')
|
|
elif op[5] == 'java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;':
|
|
key1.append(jstack.pop())
|
|
elif op[5] == 'java/lang/StringBuffer.insert:(ILjava/lang/String;)Ljava/lang/StringBuffer;':
|
|
s = jstack.pop()
|
|
key1.insert(jstack.pop(), s)
|
|
elif op[5] == 'java/lang/String.length:()I':
|
|
jstack.pop()
|
|
jstack.append(0)
|
|
elif op[5] == 'java/lang/String.charAt:(I)C':
|
|
jstack.pop()
|
|
jstack.pop()
|
|
jstack.append(0)
|
|
elif op[1] == 'aaload':
|
|
jstack.pop()
|
|
jstack.pop()
|
|
jstack.append([])
|
|
elif op[1] == 'isub':
|
|
jstack.pop()
|
|
jstack.pop()
|
|
jstack.append(0)
|
|
elif op[1] == 'dup':
|
|
jstack.append(jstack[len(jstack) - 1])
|
|
elif op[1][:5] == 'dup_x':
|
|
jstack.insert(len(jstack) - 1 - int(op[1][5:]), jstack[len(jstack) - 1])
|
|
elif op[1] == 'swap':
|
|
jstack.append(jstack.pop(len(jstack) - 2))
|
|
elif op[1] == 'pop':
|
|
jstack.pop()
|
|
elif op[1] == 'pop2':
|
|
jstack.pop()
|
|
jstack.pop()
|
|
elif op[1][:6] == 'iconst':
|
|
jstack.append(int(op[1].split('_')[1]))
|
|
elif op[1] == 'ishl':
|
|
i = jstack.pop()
|
|
jstack.append(jstack.pop() << i)
|
|
elif op[1] == 'ixor':
|
|
i = jstack.pop()
|
|
jstack.append(jstack.pop() ^ i)
|
|
elif op[1][1:6] == 'store':
|
|
i = op[1].split('_')
|
|
if len(i) >= 2:
|
|
i = i[1]
|
|
else:
|
|
i = op[2]
|
|
jlocal[int(i)] = jstack.pop()
|
|
elif op[1][1:5] == 'load':
|
|
i = op[1].split('_')
|
|
if len(i) >= 2:
|
|
i = i[1]
|
|
else:
|
|
i = op[2]
|
|
i = jlocal[int(i)]
|
|
jstack.append(i)
|
|
if op[1][:1] == 'i' and i > 0:
|
|
key2.append(i)
|
|
elif op[1] == 'castore':
|
|
jstack.pop()
|
|
jstack.pop()
|
|
jstack.pop()
|
|
elif re.match('if[egln]', op[1]):
|
|
jstack.pop()
|
|
elif op[1] == 'areturn':
|
|
key[func] = ((key1[0] + key1[1] == 'cm'), key2)
|
|
func = None
|
|
except:
|
|
func = None
|
|
return key
|
|
|
|
def uchr(i):
|
|
if sys.version_info[0] < 3:
|
|
return unichr(i)
|
|
else:
|
|
return chr(i)
|
|
|
|
def decode_utf8(buf):
|
|
utf = ''
|
|
pos = 0
|
|
end = len(buf)
|
|
while pos < end:
|
|
c = buf[pos]
|
|
if ord(c) < 0x80:
|
|
utf = utf + c
|
|
else:
|
|
bytes = 6
|
|
while bytes >= 2:
|
|
mask = 0x3f00 >> bytes & 0xfc
|
|
if ord(c) & mask == mask:
|
|
bits = ord(c) & ~mask
|
|
for d in buf[pos + 1 : pos + bytes]:
|
|
bits = bits << 6 | ord(d) & 0x3f
|
|
utf = utf + uchr(bits)
|
|
pos = pos + bytes - 1
|
|
break
|
|
bytes = bytes - 1
|
|
pos = pos + 1
|
|
return utf
|
|
|
|
class jasm_str:
|
|
def init(self, jclass):
|
|
pass
|
|
|
|
def decode(self, ss, op, jclass):
|
|
ss = ss.replace('\\t', '\t')
|
|
ss = ss.replace('\\n', '\n')
|
|
ss = ss.replace('\\r', '\r')
|
|
ss = ss.replace('\\"', '\"')
|
|
return ss
|
|
|
|
class jconst_pool_str(jasm_str):
|
|
def __init__(self, jar):
|
|
self.jar = zipfile.ZipFile(jar, 'r')
|
|
self.cp_utf = {}
|
|
self.cp_str = {}
|
|
|
|
def init(self, jclass):
|
|
jb = self.jar.read(jclass.replace('.', '/') + '.class')
|
|
jc = jb.decode('L1')
|
|
if jc[:4] == b'\xca\xfe\xba\xbe'.decode('L1'):
|
|
self.cp_utf[jclass] = {}
|
|
self.cp_str[jclass] = {}
|
|
pos = 10
|
|
idx = 1
|
|
cnt = (ord(jc[8]) << 8) + ord(jc[9])
|
|
while idx < cnt:
|
|
tag = ord(jc[pos])
|
|
pos = pos + 1
|
|
if tag == 1: #Utf8
|
|
end = pos + 2 + (ord(jc[pos]) << 8) + ord(jc[pos + 1])
|
|
pos = pos + 2
|
|
try:
|
|
utf = jb[pos:end].decode('U8')
|
|
except UnicodeDecodeError:
|
|
utf = decode_utf8(jc[pos:end])
|
|
self.cp_utf[jclass][idx] = utf
|
|
pos = end
|
|
elif tag == 8: #String
|
|
self.cp_str[jclass][idx] = (ord(jc[pos]) << 8) + ord(jc[pos + 1])
|
|
pos = pos + 2
|
|
elif tag == 7 or tag == 16: #Class,MethodType
|
|
pos = pos + 2
|
|
elif tag == 15: #MethodHandle
|
|
pos = pos + 3
|
|
elif tag == 3 or tag == 4 or tag == 9 or tag == 10 or tag == 11 or tag == 12 or tag == 18:
|
|
pos = pos + 4
|
|
elif tag == 5 or tag == 6: #Long,Double
|
|
pos = pos + 8
|
|
idx = idx + 1
|
|
idx = idx + 1
|
|
|
|
def decode(self, ss, op, jclass):
|
|
idx = int(op[2][1:].split(';')[0])
|
|
if not idx in self.cp_utf[jclass]:
|
|
idx = self.cp_str[jclass][idx]
|
|
utf = self.cp_utf[jclass][idx]
|
|
return utf
|
|
|
|
def decode_strings(jasm, key, jstr, out):
|
|
cnt = 0
|
|
sstack = []
|
|
jasm.seek(0)
|
|
for line in jasm:
|
|
line = line.decode('U8')
|
|
end = -1 - (len(line) >=2 and line[-2] == '\r')
|
|
func = None
|
|
if re.match('[pfcs]', line[0]):
|
|
caller_class = False
|
|
for s in line.split():
|
|
if caller_class:
|
|
caller_class = s
|
|
jstr.init(caller_class)
|
|
break
|
|
caller_class = (s == 'class')
|
|
elif re.match(' [a-z].*\(.*\).*;', line):
|
|
end = line.rindex('(')
|
|
pos = line[:end].rindex(' ') + 1
|
|
caller_method = line[pos:end]
|
|
if caller_method == caller_class:
|
|
caller_method = '<init>'
|
|
elif line[:end] == ' static {};':
|
|
caller_method = '<clinit>'
|
|
else:
|
|
op = line.split()
|
|
if len(op) >= 5:
|
|
if op[1][0:3] == 'ldc':
|
|
i = line.find('// String ')
|
|
if i >= 0:
|
|
sstack.append(jstr.decode(line[i + 10 : end], op, caller_class))
|
|
if len(sstack) > 20:
|
|
del sstack[0]
|
|
elif op[1] == 'invokestatic':
|
|
func = op[5]
|
|
if not '.' in func.split('(')[0]:
|
|
func = caller_class.replace('.', '/') + '.' + func
|
|
if func[0] == '"':
|
|
i = func.rindex('.')
|
|
func = func[1 : i - 1] + func[i:]
|
|
if func in key:
|
|
if key[func][0]:
|
|
key1 = caller_class + caller_method
|
|
else:
|
|
key1 = caller_method + caller_class
|
|
key2 = key[func][1]
|
|
key1top = len(key1) - 1
|
|
key1idx = key1top
|
|
key2idx = 0
|
|
ss = sstack.pop()
|
|
dec = []
|
|
for c in reversed(ss):
|
|
c = ord(c) ^ ord(key1[key1idx]) ^ key2[key2idx]
|
|
c = uchr(c)
|
|
dec.insert(0, c)
|
|
key1idx = key1idx - key2idx
|
|
if key1idx < 0:
|
|
key1idx = key1top
|
|
key2idx = key2idx + 1 & 1
|
|
out.write(line, func, ''.join(dec), ss)
|
|
cnt = cnt + 1
|
|
else:
|
|
func = None
|
|
out.write(line, func, None, None);
|
|
return cnt
|
|
|
|
class std_writer:
|
|
def write(self, line, func, res, arg):
|
|
if res:
|
|
print(res)
|
|
|
|
class line_writer(std_writer):
|
|
def __init__(self):
|
|
self.num = 1
|
|
|
|
def write(self, line, func, res, arg):
|
|
if res:
|
|
print(str(self.num) + '\t' + res)
|
|
else:
|
|
self.num = self.num + 1
|
|
|
|
class jasm_writer(std_writer):
|
|
def __init__(self, jasm):
|
|
self.out = open(jasm, 'wb')
|
|
|
|
def write(self, line, func, res, arg):
|
|
if func:
|
|
if res:
|
|
res = res.replace('\n', '\\n')
|
|
res = res.replace('\r', '\\r')
|
|
i = line.index(' // Method')
|
|
self.out.write(line[:i].encode('U8'))
|
|
self.out.write(' // STRING '.encode('U8'))
|
|
self.out.write(res.encode('U8'))
|
|
self.out.write(line[i:].encode('U8'))
|
|
else:
|
|
self.out.write(line.encode('U8'))
|
|
|
|
class dict_writer(std_writer):
|
|
def __init__(self):
|
|
self.dic = {}
|
|
|
|
def write(self, line, func, res, arg):
|
|
if res:
|
|
i = func.replace(':(Ljava/lang/String;)Ljava/lang/String;', '').replace('/', '.') + '("' + arg + '")'
|
|
if i in self.dic and self.dic[i] != res:
|
|
raise KeyError(i + ' : ' + self.dic[i] + ' : ' + res)
|
|
self.dic[i] = res
|
|
|
|
def replace_strings(dic, src, dst):
|
|
cnt = 0
|
|
os.mkdir(dst)
|
|
for root, dirs, files in os.walk(src):
|
|
for name in dirs:
|
|
os.mkdir(dst + os.path.join(root, name)[len(src):])
|
|
for name in files:
|
|
out = open(dst + os.path.join(root, name)[len(src):], 'wb')
|
|
for line in open(os.path.join(root, name), 'rb'):
|
|
line = line.decode('U8')
|
|
for esc in re.finditer('[a-zA-Z0-9_.\\\\]+?\(".*?[^\\\\](\\\\\\\\)*?"\)', line):
|
|
esc = esc.group(0)
|
|
raw = esc.encode('U8').decode('unicode_escape')
|
|
if raw in dic:
|
|
line = line.replace(esc, '"' + dic[raw].encode('unicode_escape').decode('U8').replace('"', '\\"') + '"', 1)
|
|
cnt = cnt + 1
|
|
out.write(line.encode('U8'))
|
|
sys.stderr.write(str(cnt) + ' / ')
|
|
|
|
def decode_main(argi, jasm):
|
|
key = interpret_key(jasm)
|
|
i = argi
|
|
if len(sys.argv) > i and zipfile.is_zipfile(sys.argv[i]):
|
|
jstr = jconst_pool_str(sys.argv[i])
|
|
i = i + 1
|
|
else:
|
|
jstr = jasm_str()
|
|
if len(sys.argv) > i + 1 and os.path.isdir(sys.argv[i]) and (os.path.isdir(sys.argv[i + 1]) or not os.path.exists(sys.argv[i + 1])):
|
|
out = dict_writer()
|
|
cnt = decode_strings(jasm, key, jstr, out)
|
|
replace_strings(out.dic, sys.argv[i], sys.argv[i + 1])
|
|
elif len(sys.argv) > i:
|
|
out = jasm_writer(sys.argv[i])
|
|
cnt = decode_strings(jasm, key, jstr, out)
|
|
else:
|
|
if sys.version_info[0] < 3:
|
|
sys.stdout = codecs.getwriter('U8')(sys.stdout)
|
|
else:
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding = 'U8')
|
|
if argi == 1:
|
|
out = std_writer()
|
|
else:
|
|
out = line_writer()
|
|
cnt = decode_strings(jasm, key, jstr, out)
|
|
sys.stderr.write(str(cnt) + ' strings\n')
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
usage(os.path.basename(sys.argv[0]))
|
|
return
|
|
if zipfile.is_zipfile(sys.argv[1]):
|
|
jar = sys.argv[1]
|
|
jdk = os.getenv('JAVA_HOME')
|
|
if jdk:
|
|
javap = os.path.join(jdk, 'bin', 'javap')
|
|
else:
|
|
javap = 'javap'
|
|
javap = [javap, '-c', '-p', '-J-Dfile.encoding=UTF-8']
|
|
jasm = tempfile.TemporaryFile()
|
|
try:
|
|
dtmp = tempfile.mkdtemp()
|
|
jar = zipfile.ZipFile(jar, 'r')
|
|
fmt = '{0}.class'
|
|
cnt = 0
|
|
for i in jar.namelist():
|
|
if i.endswith('.class'):
|
|
tmp = open(os.path.join(dtmp, fmt.format(cnt)), 'wb')
|
|
tmp.write(jar.read(i))
|
|
tmp.close()
|
|
cnt = cnt + 1
|
|
sys.stderr.write(str(cnt) + ' classes ...\n')
|
|
subprocess.check_call(javap + [os.path.join(dtmp, fmt.format(i)) for i in range(cnt)], stdout=jasm)
|
|
finally:
|
|
shutil.rmtree(dtmp)
|
|
decode_main(1, jasm)
|
|
else:
|
|
decode_main(2, open(sys.argv[1], 'rb'))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|