Jsunpack-n 的核心是SpiderMonkey + Python 源代码javascript和python两部分
JS部分
post.js (post-processing) 做收尾工作的脚本,在main js 被interpreted之后执行,输出结果。
pre.js javaScript hooks and environment,这个文件中,有hook eval函数的部分,但是被注是掉了,关于eval的hook通过修改
SpiderMonkey来完成,这段代码将会加载到 main JS(也就是要解密的js) 的之前运行,来hook关键函数,已经加载plugin,设置环境变量等。
这两个JS文件会分别在main JS 的之前和之后运行。
Hook函数的清单
eval window.eval
window.execScript String.eval
app.eval addEventListener
attachEvent app.setTimeOut
window.onload app.setInterval
这里说一下,jsunpackn检测恶意代码的两种方式
静态检测使用过Yara模块实现
• Static – “rules” file ( decodedPDF and decodedOnly classes )
rule mediaNewplayer: decodedPDF
{
meta:
ref = "CVE-2009-4324"
hide = true
strings:
$cve20094324 = "media.newPlayer" nocase fullword
condition:
1 of them
}
动态监测,以及很多加密的代码是通过JS HOOK 来实现• Dynamic – “pre.js” hooking file
var media = {
newPlayer : function(a){
if (a == null){
print("//alert CVE-2009-4324 media.newPlayer with NULL parameter");
}
else {
print("//warning CVE-2009-4324 media.newPlayer access");
}
},
python部分
junpackn.py 主文件
detection.py use rules files to find content
pdf.py swf.py Extraction of javascript
gzip.py unctions that read and write gzipped files
html.py A simple HTML language parser. Uses the 'htmlparse.conf' file to define rules.
lzw.py lzw压缩,所使用的模块
urlattr.py his is a helper class within jsunpack-n 存放一些属性信息之类的,还有处理url的一些工具函数
debuger Used to track performance statistics Within the application being debugged
配置文件
这些文件都会在,jsunpackn.py运行的开始,被读入内存
rules 这是Yara模块所使用的规则文件,支持自定义规则
rules.ascii 只支持ascii码的规则,不知道有什么作用
htmlparse.config 提供parsing HTML的逻辑支持,用来寻找藏匿JS的HTML field,支持自定义规则
options.config 存放一些路径数据,和配置数据
SpirderMonkey源码修改
SpriderMonkey的源代码修改分为两部分,第一部分是eval的Hook,第二部分是detection shellcode。
eval的hook的关键代码,在jsobj.c函数的obj_eval函数中
//added
if (JSSTRING_IS_DEPENDENT(str)) {
n = (size_t)JSSTRDEP_LENGTH(str);
s = JSSTRDEP_CHARS(str);
} else {
n = (size_t)str->length;
s = str->u.chars;
}
printf("\n//eval\n");
for (i = 0; i < n; i++){
if (s[i] == '\0'){
break;
}
printf("%c",s[i]);
}
printf("\n");
//end added
detection shell的核心代码是这样,很迷惑,后来经伟哥指点,这个shellcode是连续的16进制存储,这只是一个粗略的查找方法。
修改在jsstr.c,核心代码如下
while (i<n*2 && found_shellcode_char == JS_FALSE){
if ((a[n] > 0 && a[n] < ' ' && a[n] != '\r' && a[n] != '\n' && a[n] != '\t') || (a[n] >= '\x7f')){
count_threshold ++;
if (count_threshold > 25){
printf("\n//warning CVE-NO-MATCH Shellcode Engine Binary Threshold\n",a[n]);
found_shellcode_char = JS_TRUE;
return JS_TRUE;
}
}
i++;
}
python代码调试方法
python -m pdb 1.py
n 下一行
list 最近代码段
breadk 1.py:6 设断点
pp sum 打印变量
break 或 b 设置断点
continue 或 c 继续执行程序
list 或 l 查看当前行的代码段
step 或 s 进入函数
return 或 r 执行代码直到从当前函数返回
exit 或 q 中止并退出
next 或 n 执行下一行
pp 打印变量的值
help
Jsunpackn.py 源码
main函数
main函数在1192行开始,jsunpackn使用了一种,叫做Optionparser的模块来实现,模拟Linux规范的命令行参数格式
main函数的开始,就是对一些全局变量的定义与模块的检测,以及Optionparser的初始化,类似这样。
parser = OptionParser(message)
parser.add_option('-t', '--timeout', dest='timeout',
help='limit on number of seconds to evaluate JavaScript', #default=30,
action='store')
1273行进行参数的读取
(options, args) = parser.parse_args()
1279使用configparser对配置文件options.configfile进行读取,configparser也是常用的读取配置文件的
python模块,在配置文件中中括号包含的值 section,其下包含值的为 key,对照options.config,发现
读入了path下的value,pre.js post.js tmpdir thmlparse.config
config = ConfigParser.RawConfigParser()
if config.read(options.configfile):
for path, value in config.items('paths'):
if value == 'NULL':
value = ''
fileopt[path] = value
for path, value in config.items('decoding'):
if value == 'True': value = True
elif value == 'False': value = False
fileopt[path] = value
1312行 读取了rules rules.ascii option.htmlparse,这三个规则配置文件,其中内容
存储在数组options中
fin = open('rules', 'r')
if fin:
options.rules = fin.read()
fin.close()
fin = open('rules.ascii', 'r')
if fin:
options.rulesAscii = fin.read()
fin.close()
if options.htmlparse:
fin = open(options.htmlparse, 'r')
if fin:
options.htmlparseconfig = fin.read()
fin.close()
1329行开始抓取页面内容,这里有三个判断,file interface与urlfatch, 一般情况下我们直接输入网址,
进入urlfatch这个分支,interface应该是处理包的还原,file是处理本地文件的分支。之后调用
jsunpack函数进行抓取,在这里已经完成了对url中的JS文件的查找,以及decode,结果被存储到了
JS之中,调用前使用正则替换re.sub替换掉了url地址的https;//头
urlattr.verbose = True #shows [nothing found] entries
options.urlfetch = re.sub('^[https]+://', '', options.urlfetch)
js = jsunpack(options.urlfetch, ['', '', ''], options)
prevRooturl = js.rooturl
1351行之后就是输出结果clean history
jsunpack类
主函数
65行处定义,被main函数所调用,负责主要的JS查找与解密工作,函数介绍如下
INPUT: These are the main input modes:
1) options.urlfetch: URL to fetch and decode (if options.active, then follow up)
OR
2) todecode: local contents or static string as:
todecode[0]=url_or_name(optional)
todecode[1]=data(mandatory)
todecode[2]=filename
OUTPUT: check the <jsunpack Object>.rooturl structure. To decode multiple files and not create separate trees,
passing rooturl between different decodings is necessary (as prevRooturl).
parameters:
@_start = url of root node
@options = configuration and user settings; includes rules as strings (not filenames)
@prevRooturl = continuity of tree between decodings, after decoding pass in <jsunpack Object>.rooturl
之后到144行都是一些,初始化,路径的设置,temp文件的创建等,我们不仔细看他们
从144行开始decode initialize,首先判断是interface , urlfetch 还是 local file,这和main函数调用此函数时的判断一致
interface分支使用了nids模块作包的还原,local file中也有pcap文件的判断。
这里self.fetch一句会把页面抓取到本地,并在控制台输出信息。
if self.OPTIONS.interface:
nids.param('device', self.OPTIONS.interface)
self.run_nids()
elif self.OPTIONS.urlfetch:
if not self.OPTIONS.quiet:
print 'URL fetch %s' % (self.OPTIONS.urlfetch)
status, fname = self.fetch(options.urlfetch)
if not self.OPTIONS.quiet:
print status
else: #local file decode
if not self.url:
fetch函数
def fetch(self, url): 在910处定义,由jsunpack类main函数调用,
934之前是对url的规范化,和一些参数的定义,不去深究,之后开始构造request头,检查proxy,并准备发送请求
hostname, dstport = self.hostname_from_url(url)
if self.OPTIONS.proxy and (not self.OPTIONS.currentproxy):
proxies = self.OPTIONS.proxy.split(',')
self.OPTIONS.currentproxy = proxies[random.randint(0, len(proxies) - 1)]
if not self.OPTIONS.quiet:
print '[fetch config] random proxy %s' % (self.OPTIONS.currentproxy)
request = urllib2.Request('http://' + url)
request.add_header('Referer', 'http://' + refer)
request.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0)')
在955行左右使用了urllib2模块的opener函数进行据抓取,在url不可用时速度较慢。opener = urllib2.build_opener()
try:
remote = opener.open(request).read()
except urllib2.HTTPError, error:
remote = error.read()
之后调用了create_sha1file这个函数,定义在192,因为是创建文件的函数,就没有具体分析。
之后通过,下面这一句,取得了主机ip,做了一些设定,
resolved = socket.gethostbyname(hostname)
到970行转入decode函数,正式开始解码JS,这里的remote变量存出了,页面完整源代码
self.main_decoder(remote, url)
main_decode函数
定义于980,由fetch函数调用,函数开始是一些对文件类型的判断,比如PDF的判断方式是这样
if 0 <= data[0:1024].find('%PDF-') <= 1024:
isPDF = True
SWF文件是这样
if data.startswith('CWS') or data.startswith('FWS'):
isSWF = True
此函数只作一系列的判断和一部分文件的JS搜索工作,比如FWS文件的JS搜索,页面中的搜索
工作应该是交给find_urls来作,解码工作,应该是交给decode_JS来作
if data.startswith('CWS') or data.startswith('FWS'):
isSWF = True
self.rooturl[self.url].filetype = 'SWF'
msgs, urls = swf.swfstream(data)
for url in urls:
swfjs_obj = re.search('javascript:(.*)', url, re.I)
if swfjs_obj:
swfjs += swfjs_obj.group(1) + '\n'
else:
#url only
multi = re.findall('https?:\/\/([^\s<>\'"]+)', url)
if multi:
for m in multi:
self.rooturl[self.url].setChild(m, 'swfurl')
else:
#no http
if url.startswith('/'):
#relative root path
firstdir = re.sub('([^/])/.*$', '\\1', self.url)
m = firstdir + url
else:
#relative preserve directory path
lastdir = re.sub('/[^\/]*$', '/', self.url)
m = lastdir + url
self.rooturl[self.url].setChild(m, 'swfurl')
else:
isSWF = False
find_urls函数
定义于471,作用就是这个
'''returns JavaScript (if it exists)'''
原理很简单,就是查找关键字,就不多说了,查找规则应该在htmlparser.config里面
decode_JS函数
定义在265行,函数开始会先对页面源码进行解析,这里调用了html.py的htmlparser函数,间接使用了Beatuiful Soup模块,函数很短
随后进入decodeVersion函数
to_write_headers, to_write = self.hparser.htmlparse(content)
decode_Helper函数
定义于313行,其中关键代码如下,明显是直接调用了SpiderMonkey进行解析,通过观察可以发现,执行原理是把页面源码抓去的到本地文件之后放在pre.js之后执行,并不
急于执行post.js,此时的抓取的页面代码是html混杂js的代码,通过stdout和stderr这两个文件来收集信息。
po = subprocess.Popen(['js', '-f', self.OPTIONS.pre, '-f', current_filename + '.js', '-f', self.OPTIONS.post], shell=False, stdout=js_stdout, stderr=js_stderr)
在这里把信息存入内存
js_stdout.close()
js_stdout = open(current_filename + '.stdout', 'rb')
decoded = js_stdout.read()
js_stdout.close()
js_stderr.close()
js_stderr = open(current_filename + '.stderr', 'rb')
errors = js_stderr.read()
js_stderr.close()
decodeVersion函数
定义于240行,在函数的开始部分,可以使解析器不处理源码中的中文字符,为了效率,但需要设置 fasteval,不过也无关紧要。
if not self.OPTIONS.fasteval: #don't evaluate HTML en/zh-cn in favor of performance
直接看下面,函数会模拟各种浏览器解析页面代码,其中都调用到了decodeJShelper这个函数,并把浏览器的各种信息当作参数传递进去
模拟的浏览器有这几个
'Vista', 'Mozilla/4.0
'Opera', 'Opera/9.64
'Firefox', 'Mozilla/5.0
代码如下 if not self.OPTIONS.fasteval:
browsers.append(['IE8/Vista', 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)'])
browsers.append(['Opera', 'Opera/9.64 (Windows NT 6.1; U; de) Presto/2.1.1'])
browsers.append(['Firefox', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729)'])
for name, browser in browsers:
if duration < self.OPTIONS.timeout and runningTime <= self.OPTIONS.redoevaltime:
midpoint = browser.find('/')
appCodeName = browser[:midpoint]
appVersion = browser[midpoint + 1:]
decoded, currentRunningTime = self.decodeJShelper('navigator.appCodeName = String("%s"); navigator.appVersion = String("%s"); navigator.userAgent = String("%s"); document.lastModified = String("%s");\n%s' % (appCodeName, appVersion, browser, self.lastModified, need_to_write))
runningTime = currentRunningTime
duration += currentRunningTime
decodings.append(['browser=' + name, decoded])