[Research] Windows Patch Diffing 맛보기 Part 2
안녕하세요 여러분! L0ch입니다. 지난 Windows Patch Diffing글의 분량 조절 실패로 파트 1에 이어 파트 2로 돌아왔습니다!
[Research]WIndows Patch Diffing 맛보기 Part 1
업데이트 패키지 추출도 했으니 이제 진짜 패치 diffing을 시작…하기 전에! 해야 할게 한 가지 더 남아있습니다.
아니 그럼 diffing은 대체 언제 함?
윈도우에서 패치를 할 때 어떤 방식으로 하는지를 먼저 알아봐야 합니다. 그래야 diffing 할 버전 별 바이너리를 생성할 수 있어요 ㅎㅎ
패치 파일 유형
이전 글에서 추출한 null, forward, reverse differential에 대해 간단하게 살펴봤었죠. 이 diff 유형의 파일들은 diffing 할 버전별 바이너리를 생성하는 데 아래와 같은 역할을 합니다.
Forward diff
- 기본 바이너리 (.1)를 특정 패치 수준까지 가져옵니다.
Reverse diff
- 패치가 적용된 바이너리를 기본 바이너리 (.1)로 롤백합니다.
NULL diff
패치에서 새 파일이 추가될 경우 사용됩니다.
</br>
diff 파일을 이용한 Windows의 패치 과정입니다.
- 기존 바이너리 버전에 맞는 reverse diff 파일로 델타 패치해 기본 버전으로 롤백
- 패치 패키지에서 추출한 forward diff 파일로 델타 패치
NULL diff일 경우 패치할 경로에 복사
</br>
우리가 diffing 할 바이너리를 구하는 과정은 다음과 같습니다.
C:\system32\WinSxS
에서 diffing 할 바이너리를 찾습니다. WinSxS에는 diff 파일뿐만 아니라 해당 버전의 바이너리도 함께 들어있습니다.- 해당 버전의 reverse diff로 델타 패치해 기본 버전 바이너리로 롤백합니다.
- 추출한 패치 파일에서 패치할 버전의 forward diff로 델타 패치를 합니다.
- 패치된 바이너리의 hash를 구하고 manifest hash와 비교해 정상적으로 패치되었는지 확인합니다.
패치 적용
ntoskrnl.exe
의 버전 별 바이너리를 생성해서 diffing 해보도록 하겠습니다!
diffing 대상 선정은 취약점 정보와 해당 취약점의 패치 릴리즈 버전을 보고 판단하면 되겠습니다.
powershell에서 다음 명령어를 입력하면 WinSxS에 업데이트된 버전별 differential 파일들을 확인할 수 있습니다.
Get-ChildItem -Recurse C:\windows\WinSxS\ | ? {$_.Name -eq "ntoskrnl.exe"}
환경마다 누적된 업데이트가 달라서 결과는 다를 수 있습니다. 현재 제 윈도우 시스템의 ntoskrnl.exe
버전인 10.0.18362.1082를 기준으로 진행해 볼게요.
현재 ntoskrnl.exe 버전 - 10.0.18362.1082
추출한 ntoskrnl.exe 버전 - 10.0.18362.1016
1082 버전은 제 시스템에 이미 있으니 1016 버전을 델타 패치로 생성해보겠습니다!
Patch Script
diff 패치에는 ms에서 지원하는 patch API인 msdelta.dll
를 사용합니다.
Vista 이상 Windows에서는 msdelta.dll
이 기본적으로 내장되어 있기 때문에 가져다가 쓰기만 하면 되죠 ㅎㅎ
아래는 파이썬을 사용해 작성한 msdelta 패치 스크립트입니다.
from ctypes import (windll, wintypes, c_uint64, cast, POINTER, Union, c_ubyte,
LittleEndianStructure, byref, c_size_t)
import zlib
import sys
import base64
import hashlib
import argparse
import struct
# types and flags
DELTA_FLAG_TYPE = c_uint64
DELTA_FLAG_NONE = 0x00000000
DELTA_APPLY_FLAG_ALLOW_PA19 = 0x00000001
# struct
class DELTA_INPUT(LittleEndianStructure):
class U1(Union):
_fields_ = [('lpcStart', wintypes.LPVOID),
('lpStart', wintypes.LPVOID)]
_anonymous_ = ('u1',)
_fields_ = [('u1', U1),
('uSize', c_size_t),
('Editable', wintypes.BOOL)]
class DELTA_OUTPUT(LittleEndianStructure):
_fields_ = [('lpStart', wintypes.LPVOID),
('uSize', c_size_t)]
ApplyDeltaB = windll.msdelta.ApplyDeltaB
ApplyDeltaB.argtypes = [DELTA_FLAG_TYPE, DELTA_INPUT, DELTA_INPUT,
POINTER(DELTA_OUTPUT)]
ApplyDeltaB.rettype = wintypes.BOOL
DeltaFree = windll.msdelta.DeltaFree
DeltaFree.argtypes = [wintypes.LPVOID]
DeltaFree.rettype = wintypes.BOOL
gle = windll.kernel32.GetLastError
def apply_patches(buf, buf_len, patch_path):
with open(patch_path, 'rb') as p:
patch = p.read()
if patch[:2] != b"PA":
patch_offset = patch.find(b"PA")
if patch_offset != 4:
raise Exception("Invalid Patch")
# Check CRC
crc = int.from_bytes(patch[:4], byteorder = "little")
patch_contents = patch[4:]
if zlib.crc32(patch_contents) != crc:
raise Exception("CRC check failed")
flag = DELTA_FLAG_NONE
d_dest = DELTA_INPUT()
d_src = DELTA_INPUT()
d_out = DELTA_OUTPUT()
d_src.lpcStart = buf
d_src.uSize = buf_len
d_src.Editable = False
d_dest.lpcStart = cast(patch_contents, wintypes.LPVOID)
d_dest.uSize = len(patch_contents)
d_dest.Editable = False
status = ApplyDeltaB(flag, d_src, d_dest, byref(d_out))
if status == 0:
raise Exception("Patch "+ patch_path + " failed with Error code "+str(gle()))
return (d_out.lpStart, d_out.uSize)
if __name__ == '__main__':
ap = argparse.ArgumentParser()
mode = ap.add_mutually_exclusive_group(required=True)
mode.add_argument("-i", "--input", help="revese/forward diff file path")
mode.add_argument("-n", "--null", action="store_true", default=False, help="Create file from null diff")
ap.add_argument("-o", "--output", required=True , help="write patched file")
ap.add_argument("-p", "--patches", required=True, nargs='+', help="Patches path to apply")
args = ap.parse_args()
with open(args.input, 'rb') as r:
inbuf = r.read()
buf_len = len(inbuf)
buf = cast(inbuf, wintypes.LPVOID) # cast to void * pointer
alloc_list = []
for patch in args.patches:
buf, buf_len = apply_patches(buf, buf_len, patch)
alloc_list.append(buf)
output_buf = bytes((c_ubyte*buf_len).from_address(buf))
with open(args.output, 'wb') as w:
w.write(output_buf)
for alloc in alloc_list:
DeltaFree(alloc)
hash = hashlib.sha256(output_buf)
print(str(len(args.patches)) + " pathces successfully applied.")
print("hash : " + base64.b64encode(hash.digest()).decode())
옵션 별 기능은 다음과 같습니다.
-i : reverse 또는 forward differential 패치 할 바이너리 지정
-n : null differential
-o : 패치 된 결과 파일 생성
-p : 패치 diff파일 지정
</br>
python delta_patch.py -i C:\\Windows\\WinSxS\\amd64...10.0.18362.1082\\ntoskrnl.exe ^
-o result_ntoskrnl.exe -p C:\\Windows\\WinSxS\\amd64...10.0.18362.1082\\r\\ntoskrnl.exe
1082 버전의 ntoskrnl.exe
를 같은 버전의 reverse diff 파일로 패치해 기본 버전으로 롤백한 결과입니다.
사진과 같이 successfully applied 메시지와 hash 값이 뜨면 패치가 정상적으로 이루어진 것을 확인할 있어요.
</br>
속성에서 확인해 보면 10.0.18362.1 기본 버전으로 롤백이 잘 됐네요!
이제 우리가 업데이트 패키지에서 추출한 ntoskrnl
의 1016버전 forward diff로 패치를 해보겠습니다
</br>
python delta_patch.py -i result_ntoskrnl.exe -o ntoskrnl_1016.exe ^
-p [EXTRACTED PATH]\\amd64_microsoft...10.0.18362.1016...\\f\\ntoskrnl.exe
버전은 1016으로 잘 올라간 것 같군요. hash도 비교해보죠.
</br>
JUNK의 ntoskrnl manifest에서 참고한 hash도 일치하네요! 이로써 패치 스크립트로 정상적으로 패치 된 것을 확인할 수 있습니다.
Binary Diffing
(글을 쓰고 있는 지금 내 상태)
바이너리 패치가 이렇게 힘들 일인가..? 격하게 아무것도 안하고 싶어졌지만 기왕 쓰기 시작한 거 끝은 봐야 하지 않겠습니까 으아악 빨리 끝내고 술 먹으러 가야지
마지막! diffing tool과 분석 방법입니다.
Diffing은 IDA에서 진행하며 BinDiff라는 플러그인을 사용합니다. BinDiff는 무료로 배포되는 코드 분석 IDA 플러그인입니다. 유명한 만큼 성능 하나는 확실한 친구죠?
BinDiff는 아래 링크에서 설치할 수 있습니다.
IDA 7.2, BinDiff 5(BinExport 10)에서 진행했습니다.
IDA가 설치된 경로를 입력하고 설치하면 되며 정상적으로 설치가 되었다면 IDA의 Edit → Plugins에서 BinDiff를 보실 수 있습니다.
우선 1016, 1082를 각각 을 열고 Edit→Plugins→BinExport10을 실행합니다.
그리고 BinExport v2 Binary Export로 각각 Export 해주면 준비는 끝!
이제 BinDiff를 실행해서 Workspace를 만들고 Diff→New Diff에서 Export 한 두 바이너리를 선택합니다.
</br>
Export한 파일을 선택하고 Diff하면..!
</br>
이렇게 Overview를 볼 수 있습니다. Similarity는 유사도를 나타내는데, 1에 가까울수록 차이가 없다는 뜻입니다.
</br>
아래로 내려보면 Similarity가 낮은 항목을 볼 수 있는데, 이 중 한 항목을 우클릭→Open Flow Graph를 실행하면
이렇게 그래프로 보여줍니다. 각 노드 색별 의미는 다음과 같습니다.
초록색 - 변경점 없음
노란색 - 변경점 있음
빨간색 - 패치에 의해 제거됨
회색 - 패치에 의해 추가됨
좀 더 자세히 볼까요? 보려는 노드를 선택해보겠습니다.
</br>
이렇게 어셈블리 코드까지 볼 수 있습니다. 이런 방식으로 변경점을 트레이싱하면 어떤 부분이 패치되었는지 자세히 볼 수 있겠죠!
마치며..
패치 스크립트를 만들어 뒀으니 원데이 분석할 때 유용하게 쓸 수 있을 것 같네요 ㅎㅎ 이제 취약점.. 취약점만 찾으면 되는데….
아마 안될거야..
다음엔 다른 주제로 찾아오겠습니다. 조금만 놀구요 ㅎ (망할 코로나 얼른 꺼져줬으면 ^^!)
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.