[Format] OLE #04. HWP - DefaultJScript

[Format] OLE #4. HWP - DefaultJScript

0x00. Intro

이전 Post에서 HWP 파일에서 취약점이 가장 많이 삽입되어 있는 BodyText의 Section을 분석해 보았다. 이번 Post에서는 자주 나오지는 않지만 별도의 취약점을 사용하지 않고 정상적인 HWP 파일의 동작인 JavaScript를 이용한 악성코드를 실제 샘플을 분석해 보며 포맷을 분석해 보겠다. 따라서 이 Post에서는 HWP 파일 중 JavaScript와 관련된 JScriptVersion 과 DefaultJScript 의 포맷에 대해 확인해 본다.

Stream 분석에 앞서 JScriptVersion 과 DefaultJScript는 모두 압축/암호화를 지원하지 않으며 레코드 구조를 따르지 않는다는 점을 기억하기 바란다.

  • 개발 언어 : Python 2.7.x
  • 개발 환경 : ipython notebook
  • HWP Specification : 다운로드
  • 사용 도구 : OffVis, SSView
  • 개발 목표
    • DefaultJScript Stream 내에 저장되어 있는 JavaScript를 추출할 수 있다.
    • 추출된 JavaScript를 동적/정적 분석을 통해 행위를 파악한다.
  • 분석 대상 : [SHA-256] D0361ADB36E81B038C752EA1A7BDC5517B1E44F82909BC2BD27B77B2652667EE
  • 진단 결과 : malwares.com

0x10. 자바 스크립트 버전 (JScriptVersion)

JScriptVersion은 Stream 이름처럼 버전 정보만을 갖고있는 Steam이다. 하지만 특별한 목적은 알 수 없다. ( 파일 포맷 문서상에 오류가 있으므로 "0x80. 스펙 문서상의 수정사항 1" 을 참고하기 바란다. )

In [2]:
from struct import unpack

def read32(buffer, offset):
    return unpack("<L", buffer[offset:offset+4])[0]

def read32Ex(buffer, offset):
    return read32(buffer, offset), offset + 4

jsver_dmp_path = "JScriptVersion.dmp"

jsver = file(jsver_dmp_path, "rb").read()

offset = 0
Major_Version, offset = read32Ex(jsver, offset)
Minor_Version, offset = read32Ex(jsver, offset)

print hex(Major_Version)
print hex(Minor_Version)
0x806463
0x88dff700L

0x20. 자바 스크립트 (DefaultJScript)

DefaultJScript는 헤더, 소스, Pre 소스, Post 소스로 구분된다. Script와 같이 그 길이를 사전에 정확히 알 수 없는 경우이므로 동적으로 길이를 얻어오도록 구조체 내에 각 멤버에 대한 크기를 함께 갖고 있다. ( 파일 포맷 문서상에 오류가 있으므로 "0x80. 스펙 문서상의 수정사항 2"를 참고하기 바란다. )

In [3]:
from struct import calcsize
from collections import namedtuple

def Map(struct_name, buffer, member_name, member_pattern):
    return namedtuple(struct_name, member_name)._make(unpack(member_pattern, buffer))

def MapSize(member_pattern):
    return calcsize(member_pattern)

def_js_path = "DefaultJScript_dec.dmp"

def_js = file(def_js_path, "rb").read()

offset = 0
len1, offset = read32Ex(def_js, offset)  # script header length
size1 = len1 * 2                                # script header
offset += size1

len2, offset = read32Ex(def_js, offset)  # script source length
size2 = len2 * 2                                # script source
offset += size2

len3, offset = read32Ex(def_js, offset)  # script pre source length
size3 = len3 * 2                                # script pre source
offset += size3

size4 = read32(def_js, offset) * 2       # script post source

DEF_SCR_MEMBER = ("HdrLen Header SrcLen Source PreSrcLen PreSource PostSrcLen PostSource EndFlag")
DEF_SCR_SIZE = "=1l%ds1l%ds1l%ds1l%ds1l" % (size1, size2, size3, size4)

defaultjscript = Map("DefaultScript", def_js[0:MapSize(DEF_SCR_SIZE)], DEF_SCR_MEMBER, DEF_SCR_SIZE)

print "Header Length : " + hex(defaultjscript.HdrLen)
print "[ Header ]"
print defaultjscript.Header
Header Length : 0x4f
[ Header ]
 
 
 

분명 오류없이 분석이 완료되었고 Length등은 정상적으로 출력되었지만 Header는 출력되지 않았다. 그 이유를 살펴보기 위해 해당 파일을 확인해보자.

즉, 2Bytes 형태의 Unicode이나 실제 저장된 데이터는 2Bytes로 저장된 Ascii라고 표현하는게 정확할 것이다. 하지만 이러한 경우는 매우 특수하기 때문에 인코딩 타입을 구분하는 다양한 조건이 감안되어야 한다. 따라서 우선 임시 코드로 2Bytes ASCII를 1Byte ASCII로 변환하도록 하겠다.

In [4]:
defaultjscript = defaultjscript._replace(**{"Header": defaultjscript.Header.replace("\x00", "")})
defaultjscript = defaultjscript._replace(**{"Source": defaultjscript.Source.replace("\x00", "")})
defaultjscript = defaultjscript._replace(**{"PreSource": defaultjscript.PreSource.replace("\x00", "")})
defaultjscript = defaultjscript._replace(**{"PostSource": defaultjscript.PostSource.replace("\x00", "")})

print "[ Header ]"
print defaultjscript.Header

print "[ PreSource ]"
print defaultjscript.PreSource

print "[ PostSource ]"
print defaultjscript.PostSource

print "[ Source ]"
print defaultjscript.Source[:100]
[ Header ]
var Documents = XHwpDocuments;
var Document = Documents.Active_XHwpDocument;

[ PreSource ]

[ PostSource ]

[ Source ]
function OnDocument_New()
{
 //todo : 
}
                                             function v

변환해 확인한 결과 Header와 Source 부분에 JavaScript가 존재함을 알 수 있다. 따라서 해당 부분을 분석하면 샘플 파일의 악성 여부를 판단할 수 있다.

In [5]:
file("defaultjscript.tmp", "wb").write(defaultjscript.Header)
file("defaultjscript.tmp", "a+").write(defaultjscript.Source)

0x30. 분석 정보

저장된 파일을 JavaScript의 양식에 맞춰 정렬해 보면 위의 그림과 같이 파일을 생성하고 실행하는 동작을 확인할 수 있다. 파일을 저장할 때 상위 바이너리 (var F)를 디코딩하게 된다. 디코딩은 2가지 과정을 거치게 되지만 해당 코드를 살펴보면 단순히 base64로 인코딩된 것임을 알 수 있다. 따라서 해당 바이너리 (var F)만을 별도로 저장해 base64로 디코딩하면 우리가 원하는 악성파일을 수집할 수 있다.

In [6]:
import base64

var_f = "var_F.txt"
data = file(var_f, "rb").read()

out = base64.decodestring(data)
file("var_F_dec.txt", "wb").write(out)

print out[:2]
MZ

base64로 디코딩된 파일은 PE 파일임을 확인할 수 있다. 따라서 해당 파일을 분석하면 악성 HWP 파일의 목적을 확인할 수 있게 된다.

0x80. 스펙 문서상의 수정사항

1. JScriptVersion 상의 구조체 문제

구조체 상에는 4Bytes 멤버 2개로 구성된 8Bytes 구조체이지만 실제 파일에서는 13Bytes로 이루어진 구조체이다. 따라서 미공개된(추가된) 구조체 멤버가 추가로 있는 것으로 보인다.

2. DefaultJScript 압축 문제

0x00. Intro에서 DefaultJScript는 압축/암호화가 적용되지 않는 영역임을 언급했다. 따라서 별도로 압축/암호화가 적용되지 않았다면 평문으로 저장되어 있기 때문에 우리 눈으로 쉽게 확인할 수 있어야 한다. 과연 그럴까?

위의 그림과 같이 압축 해제시 JavaScript로 보이는 데이터가 확인된다. 이 또한 스펙 문서에 잘못 표기된 부분이다.

0x90. 버그

0x99. 참고

해당 샘플에서 추출한 DefaultJScript 와 DefaultJScript를 zlib로 압축 해제한 파일을 첨부한다.

댓글

가장 많이 본 글