您的位置:首页 > Web前端 > JavaScript

在江北研究jsunpackn(源码分析)

2012-10-18 10:37 429 查看
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
 > 0 && a
 < ' ' &&  a
 != '\r' && a
 != '\n' && a
 != '\t') || (a
 >= '\x7f')){
                count_threshold ++;
                if (count_threshold > 25){
                    printf("\n//warning CVE-NO-MATCH Shellcode Engine Binary Threshold\n",a
);
                    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])
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: