0%

Windows差异化补丁MSDelta之研究

简介

msdelta.dll包含了一系列用于对文件进行补丁操作的API 鉴于网上资料极少且官方文档简陋 记录一下学习过程

Patch(补丁)

对于msdelta.dll中的Patch 它为一个字节流 产生于一个原字节流Source和目标字节流Target 可以用这个Python wrapper来产生一个从源文件到目标文件的补丁文件

整体上它记录了补丁前后内容的差异 一个对Delta_Patch结构体包含信息的描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct _DELTA_HEADER_INFO
{
/** Used file type set. */
DELTA_FILE_TYPE FileTypeSet;

/** Source file type. */
DELTA_FILE_TYPE FileType;

/** Delta flags. */
DELTA_FLAG_TYPE Flags;

/** Size of target file in bytes. */
SIZE_T TargetSize;

/** Time of target file. */
FILETIME TargetFileTime;

/** Algorithm used for hashing. */
ALG_ID TargetHashAlgId;

/** Target hash. */
DELTA_HASH TargetHash;

} DELTA_HEADER_INFO;

用这个Wrapper可以方便地执行获取补丁信息和打补丁的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import ctypes
import os
import datetime
import argparse

HANDLE = ctypes.c_void_p
HMODULE = HANDLE
LPCSTR = LPSTR = ctypes.c_char_p
BOOL = ctypes.c_long
BYTE = ctypes.c_ubyte
SIZE_T = ctypes.c_size_t
DWORD = ctypes.c_ulong
ULONG = ctypes.c_ulong
ALG_ID = ctypes.c_ulong
LPBUFFER = ctypes.POINTER(ctypes.c_char)

# this is the Win32 Epoch time for when Unix Epoch time started. It is in
# hundreds of nanoseconds.
EPOCH_AS_FILETIME = 116444736000000000
# This is the divider/multiplier for converting nanoseconds to
# seconds and vice versa
HUNDREDS_OF_NANOSECONDS = 10000000

class FILETIME(ctypes.Structure):
_fields_ = [("dwLowDateTime", DWORD),
("dwHighDateTime", DWORD)]
@property
def unix_epoch_seconds(self):
val = (self.dwHighDateTime << 32) + self.dwLowDateTime
return (val - EPOCH_AS_FILETIME) / HUNDREDS_OF_NANOSECONDS
def __str__(self):
dt = datetime.datetime.utcfromtimestamp(self.unix_epoch_seconds)
return dt.strftime('%c')
_FILETIME = FILETIME
PFILETIME = ctypes.POINTER(_FILETIME)

def RaiseIfZero(result, func = None, arguments = ()):
if not result:
raise ctypes.WinError()
return result

#
# HMODULE WINAPI LoadLibrary(
# _In_ LPCTSTR lpFileName
# );
#
def LoadLibrary(dll):
_LoadLibraryA = ctypes.windll.kernel32.LoadLibraryA
_LoadLibraryA.argtypes = [LPSTR]
_LoadLibraryA.restype = HMODULE
_LoadLibraryA.errcheck = RaiseIfZero
return _LoadLibraryA(dll)

DELTA_FLAG_TYPE = ctypes.c_ulonglong
DELTA_FILE_TYPE = ctypes.c_ulonglong
DELTA_FLAG_NONE = 0
DELTA_APPLY_FLAG_ALLOW_PA19 = 1

DELTA_MAX_HASH_SIZE = 32

class DELTA_HASH(ctypes.Structure):
_fields_ = [
("HashSize", DWORD),
("HashValue", BYTE * DELTA_MAX_HASH_SIZE)
]

class DELTA_HEADER_INFO(ctypes.Structure):
_fields_ = [
("FileTypeSet", DELTA_FILE_TYPE),
("FileType", DELTA_FILE_TYPE),
("Flags", DELTA_FILE_TYPE),
("TargetSize", SIZE_T),
("TargetFileTime", FILETIME),
("TargetHashAlgId", ALG_ID),
("TargetHash", DELTA_HASH),
]
def __str__(self):
return '\n'.join([
"[+] FileTypeSet : 0x{0:X}".format(self.FileTypeSet),
"[+] FileType : 0x{0:X}".format(self.FileType),
"[+] Flags : 0x{0:X}".format(self.Flags),
"[+] TargetSize : 0x{0:X}".format(self.TargetSize),
"[+] TargetFileTime : {0}".format(str(self.TargetFileTime)),
"[+] TargetHashAlgId : 0x{0:X}".format(self.TargetHashAlgId),
"[+] TargetHash : {0}".format("".join("{0:02X}".format(x) for x in self.TargetHash.HashValue[:self.TargetHash.HashSize])),
])

class DELTA_INPUT(ctypes.Structure):
_fields_ = [
("lpStart", LPBUFFER),
("uSize", ULONG),
("Editable", BOOL)]

class DELTA_OUTPUT(ctypes.Structure):
_fields_ = [
("lpStart", LPBUFFER),
("uSize", ULONG)]

#
#BOOL WINAPI ApplyDeltaB(
# DELTA_FLAG_TYPE ApplyFlags,
# DELTA_INPUT Source,
# c Delta,
# LPDELTA_OUTPUT lpTarget
# );
#
def ApplyDeltaB(source, delta, flags=DELTA_APPLY_FLAG_ALLOW_PA19):
_ApplyDeltaB = ctypes.windll.msdelta.ApplyDeltaB
_ApplyDeltaB.argtypes = [DELTA_FLAG_TYPE, DELTA_INPUT, DELTA_INPUT, ctypes.POINTER(DELTA_OUTPUT)]
_ApplyDeltaB.restype = BOOL
_ApplyDeltaB.errcheck = RaiseIfZero
dsource = DELTA_INPUT()
dsource.lpStart = ctypes.create_string_buffer(source)
dsource.uSize = len(source)
dsource.Editable = False
ddelta = DELTA_INPUT()
ddelta.lpStart = ctypes.create_string_buffer(delta)
ddelta.uSize = len(delta)
ddelta.Editable = False
out = DELTA_OUTPUT()
_ApplyDeltaB(flags, dsource, ddelta, ctypes.byref(out))
return out

#
#BOOL WINAPI GetDeltaInfoA(
# LPCSTR lpDeltaName,
# LPDELTA_HEADER_INFO lpHeaderInfo
# );
#
# Note: This doesn't work with file distributed inside KB as there is a
# checksum at the start of the file
# msdelta!compo::CheckBuffersIdentityFactory::CheckBuffersIdentityComponent::InternalProcess+0x84:
# 00007ffe`8d4d6894 e8aa5b0300 call msdelta!memcmp (00007ffe`8d50c443)
#
def GetDeltaInfo(delta):
_GetDeltaInfoA = ctypes.windll.msdelta.GetDeltaInfoA
_GetDeltaInfoA.argtypes = [LPCSTR, ctypes.POINTER(DELTA_HEADER_INFO)]
_GetDeltaInfoA.restype = BOOL
_GetDeltaInfoA.errcheck = RaiseIfZero
info = DELTA_HEADER_INFO()
_GetDeltaInfoA(delta, ctypes.byref(info))
return info

#
# BOOL WINAPI GetDeltaInfoB(
# DELTA_INPUT Delta,
# LPDELTA_HEADER_INFO lpHeaderInfo
# );
#
def GetDeltaInfoB(source):
_GetDeltaInfoB = ctypes.windll.msdelta.GetDeltaInfoB
_GetDeltaInfoB.argtypes = [DELTA_INPUT, ctypes.POINTER(DELTA_HEADER_INFO)]
_GetDeltaInfoB.restype = BOOL
_GetDeltaInfoB.errcheck = RaiseIfZero
input = DELTA_INPUT()
input.lpStart = ctypes.create_string_buffer(source)
input.uSize = len(source)
input.Editable = False
info = DELTA_HEADER_INFO()
_GetDeltaInfoB(input, ctypes.byref(info))
return info

def get_delta_info(source):
buf = open(source, "rb").read()
buf = buf[4:] # remove CRC
x = GetDeltaInfoB(buf)
return x

def apply_delta(source, delta, outfile):
bufs = open(source, "rb").read()
bufd = open(delta, "rb").read()
bufd = bufd[4:] # remove CRC
out = ApplyDeltaB(bufs, bufd)
open(outfile, "wb").write(out.lpStart[:out.uSize])

LoadLibrary(b"msdelta.dll")

if __name__ == '__main__':
parser = argparse.ArgumentParser(description="MSdelta patch applier")
ACTIONS = ["info", "apply"]
actions = parser.add_subparsers(help="Action to perform: {}".format(",".join(ACTIONS)), dest="action")
info = actions.add_parser("info", help="Print delta patch information")
info.add_argument("delta_file", action="store", default="", help="delta patch file")
apply = actions.add_parser("apply", help="Apply delta patch")
apply.add_argument("delta_file", action="store", default="", help="delta patch file")
apply.add_argument("input_file", action="store", default="", help="input file")
apply.add_argument("output_file", action="store", default="", help="output file")
args = parser.parse_args()
if args.action == "info":
info = get_delta_info(args.delta_file)
print(info)
elif args.action == "apply":
apply_delta(args.input_file, args.delta_file, args.output_file)

打补丁

由于官方没有给出patch过程的技术细节 我也没逆清楚( 所以就归纳一下多次尝试得出的有关patch过程的结论:

1
2
3
4
5
6
补丁作用于源文件得到的目标文件是固定不变的
1.若源文件和目标文件中有一个为空则这个补丁可以作用于任何一个另外提供的源文件
2.若源文件和目标文件均不为空时 补丁必须作用于除了有差异的位置以外和目标文件完全一致的源文件
e.g.
delta <= from b'unpatch' to b'__patched'
delta can be applied to any Source b'??patch??'(? for any)

如果不满足这些条件 根据调用规定ApplyDelta API在返回后在RAX中存放错误码 比较常见的是0xD:无效补丁 至于无效补丁的成因 先说明补丁包含的内容中哪些对打补丁的结果有作用

以AmateursCTF2024的一道题为例:

AmateursCTF2024/rev/patchflag

题目的要求就是成功利用给出的补丁 dump出这个补丁:

image-20240428232703227

正常的补丁开头的4bytes是补丁的CRC32 题目给出的补丁抹去了这个信息 PA30是生成补丁的标准 但是这两个内容不影响补丁结果 这里直接插入4bytes的0得到补丁的信息:

image-20240428233021811

还有两个不影响打补丁的内容(小端序) 分别标志了两个时间:

image-20240428233218208

后续的一个字0x23 18用处暂不明确 再后续一个字0x8 36 8 [:4]与校验结果使用的hash算法有关 下文会给出说明 [4:24]是目标文件的字节长度 [24:]不确定详细含义 但是与生成补丁时的选项标志位有关

接下来的一个字0x8004为校验使用的hash算法ID 根据实验其只支持md系列算法以及文档中给出的两种特殊标志:

image-20240428234442506

所使用的hash校验算法会影响上面说的4位标志位 已经试出来的有:

1
2
3
1.当使用md系列hash时     flag = 0b1000
2.使用CRC32时 flag = 0b0010
3.不进行hash校验时 flag = 0b0001

再接下来的一个字猜测与hash的结果长度有关 这些字节都会影响打补丁的失败与否 接下来的hash_size个字节为预期目标文件的hash校验码 进行补丁后需要计算结果hash与这个hash进行对比 如果校验失败则返回0xD错误码 后续的字节就是经过压缩的差异信息

对于这一题 hash校验值明显已经被魔改 可以再次魔改补丁让其不进行hash校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from test import get_patch_info
from delta_patch import apply_patch_to_buffer

patch = bytes([0x50, 0x41, 0x33, 0x30, 0xC0, 0x08, 0x97, 0xFC, 0xFD, 0x3C,
0xDA, 0x01, 0x18, 0x23, 0x68, 0x83, 0x04, 0x80, 0x52, 0x00,
0x6C, 0x61, 0x72, 0x72, 0x79, 0x2D, 0x6B, 0x69, 0x6C, 0x6C,
0x65, 0x64, 0x2D, 0x74, 0x68, 0x69, 0x73, 0x21, 0x21, 0x21,
0x01, 0xCA, 0x00, 0xB7, 0x03, 0x88, 0x69, 0xB3, 0xFA, 0xF4,
0x89, 0x36, 0xA5, 0xDD, 0x8C, 0x01, 0xD1, 0xDA, 0x4D, 0x88,
0x11, 0x69, 0x4C, 0xBB, 0x71, 0x7D, 0xDA, 0x75, 0x6A, 0x37,
0x2A, 0xD2, 0x88, 0x11, 0x91, 0x22, 0x4E, 0x66, 0xDE, 0xA0,
0x31, 0x3D, 0x22, 0xCC, 0x9B, 0xD6, 0xAE, 0x47, 0xB4, 0x39,
0xB1, 0x56, 0x01])

buf = b"amateursCTF{" + b"_" * 41 + b"}"

try:
out = apply_patch_to_buffer(patch, buf)
print(out)
except Exception as e:
print(e)

info = get_patch_info(patch)
hexinfo = []
for x in info:
try:
hexinfo.append(hex(x))
except:
hexinfo.append(x)

newheader = patch[:14] + b'h\x13\x02' + patch[16 + 4 + 0x14:]
try:
out = apply_patch_to_buffer(buf, newheader)
print(out)
except Exception as e:
print(e)
# Patch file is invalid
# b'amateursCTF{suff3r_th3_p41n_of_a_m1ll10n_wind0ws_d3v5}'

另外一个方法就是在获取目标hash值(compo::PullcapiContext::GetHash)前获取到目标字节流