Skip to content

Commit d8ac02e

Browse files
committed
Improved NDR padding capabilities for primitive types
1 parent 0fa8026 commit d8ac02e

2 files changed

Lines changed: 229 additions & 7 deletions

File tree

tests/test_ndr.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import pytest
2+
3+
from windows.rpc import ndr
4+
from .pfwtest import *
5+
6+
from tests.test_rpc import UACParameters
7+
8+
# Memo: Padding byte is 'P' in ndr.py
9+
10+
# 20 Bytes structures alignes on 4 butees
11+
DoubleDwordStructure = ndr.make_structure([ndr.NdrLong] * 5)
12+
13+
# Struct with some specificities
14+
# Start Must be aligned on 4 (even if it begin with a short)
15+
# full size == 13 -> usefull to test afterward padding as well
16+
# Pack format ah follow:
17+
# SSBPLLLLBPSSB
18+
InternalAlignementStructure = ndr.make_structure([ndr.NdrShort, ndr.NdrByte, ndr.NdrLong, ndr.NdrByte, ndr.NdrShort, ndr.NdrByte])
19+
# IDL code:
20+
# typedef struct InternalAlignementStructure
21+
# {
22+
#short sfield0;
23+
#byte bfield1;
24+
#long lfield2;
25+
#byte bfield3;
26+
#short sfield4;
27+
#byte bfield5;
28+
# }InternalAlignementStructure;
29+
30+
31+
# NdrObject, Values, result
32+
NDR_PACK_TEST_CASE = [
33+
# Simple case
34+
(ndr.make_structure([ndr.NdrLong, ndr.NdrLong]), (2, 2), b"\x02\x00\x00\x00\x02\x00\x00\x00"),
35+
# Check alignement on small native types (dword aligned)
36+
(ndr.make_structure([ndr.NdrShort, ndr.NdrByte]), (0x0101, 2), b"\x01\x01\x02"),
37+
# Check alignement on small native types (dword aligned)
38+
(ndr.make_structure([ndr.NdrShort, ndr.NdrShort]), (0x0101, 0x0202), b"\x01\x01\x02\x02"),
39+
# Same check on parameters
40+
(ndr.make_parameters([ndr.NdrShort, ndr.NdrByte]), (0x0101, 2), b"\x01\x01\x02"),
41+
# Test some Hyper
42+
(ndr.make_parameters([ndr.NdrByte, ndr.NdrHyper, ndr.NdrByte, ndr.NdrLong, ndr.NdrHyper]),
43+
(0x01, 0x4141414141414141, 0x42, 0x43434343, 0x4444444444444444 ),
44+
b"\x01PPPPPPPAAAAAAAABPPPCCCCDDDDDDDD"),
45+
# Complexe structure (with 4B alignement of 20B structure)
46+
(ndr.make_parameters([ndr.NdrShort, DoubleDwordStructure]),
47+
(0x0101, [0x41414141, 0x42424242, 0x43434343, 0x44444444, 0x45454545]),
48+
b"\x01\x01PPAAAABBBBCCCCDDDDEEEE"),
49+
# Same check on parameters
50+
(ndr.make_parameters([ndr.NdrShort, DoubleDwordStructure]),
51+
(0x0101, [0x41414141, 0x42424242, 0x43434343, 0x44444444, 0x45454545]),
52+
b"\x01\x01PPAAAABBBBCCCCDDDDEEEE"),
53+
54+
# Check on InternalAlignementStructure before any nested test
55+
(InternalAlignementStructure,
56+
[0x4141, 0x42, 0x43434343, 0x44, 0x4545, 0x46],
57+
b"AABPCCCCDPEEF"),
58+
59+
# Nested alignement
60+
# Alignement with a sub-structure that also have internal alignement
61+
(ndr.make_parameters([ndr.NdrShort, InternalAlignementStructure, ndr.NdrByte]),
62+
(0x0101, [0x4141, 0x42, 0x43434343, 0x44, 0x4545, 0x46], 0x47),
63+
# Verified with an actual RPC server
64+
b"\x01\x01PPAABPCCCCDPEEFG"),
65+
# Nested alignement
66+
# Alignement with a sub-structure that also have internal alignement
67+
# Afterward short should be aligned on 2
68+
(ndr.make_parameters([ndr.NdrShort, InternalAlignementStructure, ndr.NdrShort]),
69+
(0x0101, [0x4141, 0x42, 0x43434343, 0x44, 0x4545, 0x46], 0x4747),
70+
# Verified with an actual RPC server
71+
b"\x01\x01PPAABPCCCCDPEEFPGG"),
72+
73+
]
74+
75+
@pytest.mark.parametrize("ndrobj, values, result", NDR_PACK_TEST_CASE)
76+
def test_ndr_packing(ndrobj, values, result):
77+
assert ndrobj.pack(values) == result
78+
79+
# Check the result of serializing a fixed 'EptMapAuthParameters' call known to works
80+
# It allow to test for packing of real-complexe Parameter without relying on the whole ALPC/RPC stack
81+
def test_ndr_packing_complex_epmapper_call():
82+
# Param from a real UAC endpoint resolution
83+
84+
targetiid = gdef.GUID.from_string("201EF99A-7FA0-444C-9399-19BA84F12A1A")
85+
towerarray = bytearray(b'\x04\x00\x13\x00\r\x9a\xf9\x1e \xa0\x7fLD\x93\x99\x19\xba\x84\xf1*\x1a\x01\x00\x02\x00\x00\x00\x13\x00\r\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x02\x00\x00\x00\x01\x00\x0c\x02\x00\x00\x00\x01\x00\x10\x00\x00')
86+
local_system_psid = gdef.PSID.from_string("S-1-5-18")
87+
context = (0, 0, 0, 0, 0)
88+
nb_response = 1
89+
90+
packed = windows.rpc.epmapper.EptMapAuthParameters.pack([bytearray(targetiid),
91+
(len(towerarray), towerarray),
92+
local_system_psid,
93+
context,
94+
nb_response])
95+
96+
expected_result = b'\x9a\xf9\x1e \xa0\x7fLD\x93\x99\x19\xba\x84\xf1*\x1a@\x00\x00\x00@\x00\x00\x00\x04\x00\x13\x00\r\x9a\xf9\x1e \xa0\x7fLD\x93\x99\x19\xba\x84\xf1*\x1a\x01\x00\x02\x00\x00\x00\x13\x00\r\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x02\x00\x00\x00\x01\x00\x0c\x02\x00\x00\x00\x01\x00\x10\x00\x00\x02\x02\x02\x02\x01\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x05\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00'
97+
assert packed == packed
98+
99+
100+
# Check the result of serializing a fixed 'UAC' call known to works
101+
# It allow to test for packing of real-complexe Parameter without relying on the whole ALPC/RPC stack
102+
def test_ndr_packing_complex_uac_call():
103+
parameters = UACParameters.pack([
104+
r"c:\windows\system32\notepad.exe", # Application Path
105+
"NOT_ALIGNED_STRINGXXX", # Commandline
106+
17, # UAC-Request Flag
107+
gdef.CREATE_UNICODE_ENVIRONMENT, # dwCreationFlags
108+
"", # StartDirectory
109+
"WinSta0\\Default\x00", # Station
110+
# Startup Info
111+
(None, # Title
112+
0, # dwX
113+
0, # dwY
114+
0, # dwXSize
115+
0, # dwYSize
116+
0, # dwXCountChars
117+
0, # dwYCountChars
118+
0, # dwFillAttribute
119+
0, # dwFlags
120+
5, # wShowWindow
121+
# Point structure: Use MonitorFromPoint to setup StartupInfo.hStdOutput
122+
(0, 0)),
123+
0, # Window-Handle to know if UAC can steal focus
124+
0xffffffff]) # UAC Timeout
125+
126+
expected_result = b'\x02\x02\x02\x02 \x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00c\x00:\x00\\\x00w\x00i\x00n\x00d\x00o\x00w\x00s\x00\\\x00s\x00y\x00s\x00t\x00e\x00m\x003\x002\x00\\\x00n\x00o\x00t\x00e\x00p\x00a\x00d\x00.\x00e\x00x\x00e\x00\x00\x00\x02\x02\x02\x02\x16\x00\x00\x00\x00\x00\x00\x00\x16\x00\x00\x00N\x00O\x00T\x00_\x00A\x00L\x00I\x00G\x00N\x00E\x00D\x00_\x00S\x00T\x00R\x00I\x00N\x00G\x00X\x00X\x00X\x00\x00\x00\x11\x00\x00\x00\x00\x04\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00PP\x10\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00W\x00i\x00n\x00S\x00t\x00a\x000\x00\\\x00D\x00e\x00f\x00a\x00u\x00l\x00t\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff'
127+
assert parameters == expected_result

windows/rpc/ndr.py

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ def parse(self, stream):
6464
return None
6565
return self.subcls.parse(stream)
6666

67+
def get_alignment(self):
68+
# 14.3.2 Alignment of Constructed Types
69+
# Pointer alignment is always modulo 4.
70+
return 4
71+
6772
class NdrUnpackNone(object):
6873
@classmethod
6974
def unpack(cls, stream):
@@ -94,6 +99,9 @@ def pack(self, data):
9499
def unpack(self, stream):
95100
return [self.subcls.unpack(stream) for i in range(self.size)]
96101

102+
def get_alignment(self):
103+
return self.subcls.get_alignment()
104+
97105

98106
class NdrSID(object):
99107
@classmethod
@@ -113,6 +121,11 @@ def unpack(cls, stream):
113121
subcount = NdrLong.unpack(stream)
114122
return stream.read(8 + (subcount * 4))
115123

124+
@classmethod
125+
def get_alignment(self):
126+
# Not sur, but it seems to contain an array of long
127+
return 4
128+
116129
class NdrVaryingCString(object):
117130
@classmethod
118131
def pack(cls, data):
@@ -126,6 +139,11 @@ def pack(cls, data):
126139
result += data
127140
return dword_pad(result)
128141

142+
@classmethod
143+
def get_alignment(self):
144+
# Not sur, but size is on 4 bytes so...
145+
return 4
146+
129147
class NdrWString(object):
130148
@classmethod
131149
def pack(cls, data):
@@ -149,6 +167,11 @@ def unpack(cls, stream):
149167
s = stream.read(size1 * 2)
150168
return s.decode("utf-16-le")
151169

170+
@classmethod
171+
def get_alignment(self):
172+
# Not sur, but size is on 4 bytes so...
173+
return 4
174+
152175
class NdrCString(object):
153176
@classmethod
154177
def pack(cls, data):
@@ -162,6 +185,11 @@ def pack(cls, data):
162185
result += data
163186
return dword_pad(result)
164187

188+
@classmethod
189+
def get_alignment(self):
190+
# Not sur, but size is on 4 bytes so...
191+
return 4
192+
165193
# @classmethod
166194
# def unpack(self, stream):
167195
# maxcount, offset, count = stream.partial_unpack("<3I")
@@ -180,6 +208,10 @@ def unpack(self, stream):
180208
stream.align(4)
181209
return stream.partial_unpack("<I")[0]
182210

211+
@classmethod
212+
def get_alignment(self):
213+
return 4
214+
183215
class NdrHyper(object):
184216
@classmethod
185217
def pack(cls, data):
@@ -190,6 +222,10 @@ def unpack(self, stream):
190222
stream.align(8)
191223
return stream.partial_unpack("<Q")[0]
192224

225+
@classmethod
226+
def get_alignment(self):
227+
return 8
228+
193229
class NdrShort(object):
194230
@classmethod
195231
def pack(cls, data):
@@ -199,6 +235,10 @@ def pack(cls, data):
199235
def unpack(self, stream):
200236
return stream.partial_unpack("<H")[0]
201237

238+
@classmethod
239+
def get_alignment(self):
240+
return 2
241+
202242

203243
class NdrByte(object):
204244
@classmethod
@@ -209,6 +249,10 @@ def pack(self, data):
209249
def unpack(self, stream):
210250
return stream.partial_unpack("<B")[0]
211251

252+
@classmethod
253+
def get_alignment(self):
254+
return 1
255+
212256

213257
class NdrGuid(object):
214258
@classmethod
@@ -251,21 +295,30 @@ def pack(cls, data):
251295
raise ValueError("NdrStructure packing number elements mismatch: structure has <{0}> members got <{1}>".format(len(cls.MEMBERS), len(data)))
252296
conformant_size = []
253297
res = []
298+
res_size = 0
254299
pointed = []
300+
outstream = NdrWriteStream()
255301
for i, (member, memberdata) in enumerate(zip(cls.MEMBERS, data)):
256302
if hasattr(member, "pack_in_struct"):
257303
x, y = member.pack_in_struct(memberdata, i)
258-
res.append(x)
304+
outstream.align(member.get_alignment())
305+
outstream.write(x)
306+
# res.append(x)
307+
# res_size += len(x)
259308
if y is not None:
260309
pointed.append(y)
261310
elif hasattr(member, "pack_conformant"):
262311
size, data = member.pack_conformant(memberdata)
312+
outstream.align(member.get_alignment())
313+
outstream.write(data)
263314
conformant_size.append(size)
264-
res.append(data)
315+
# res.append(data)
316+
# res_size += len(data)
265317
else:
266318
packed_member = member.pack(memberdata)
267-
res.append(packed_member)
268-
return dword_pad(b"".join(conformant_size)) + dword_pad(b"".join(res)) + dword_pad(b"".join(pointed))
319+
outstream.align(member.get_alignment())
320+
outstream.write(packed_member)
321+
return dword_pad(b"".join(conformant_size)) + outstream.get_data() + dword_pad(b"".join(pointed))
269322

270323
@classmethod
271324
def unpack(cls, stream):
@@ -305,6 +358,10 @@ def unpack(cls, stream):
305358
def post_unpack(cls, data):
306359
return data
307360

361+
@classmethod
362+
def get_alignment(self):
363+
return max([x.get_alignment() for x in self.MEMBERS])
364+
308365

309366

310367
class NdrParameters(object):
@@ -320,11 +377,17 @@ def pack(cls, data):
320377
print(" * data {0}".format(data))
321378
print(" * members = {0}".format(cls.MEMBERS))
322379
raise ValueError("NdrParameters packing number elements mismatch: structure has <{0}> members got <{1}>".format(len(cls.MEMBERS), len(data)))
323-
res = []
380+
381+
382+
outstream = NdrWriteStream()
324383
for (member, memberdata) in zip(cls.MEMBERS, data):
384+
alignment = member.get_alignment()
385+
outstream.align(alignment)
325386
packed_member = member.pack(memberdata)
326-
res.append(packed_member)
327-
return b"".join(dword_pad(elt) for elt in res)
387+
outstream.write(packed_member)
388+
return outstream.get_data()
389+
390+
328391

329392
@classmethod
330393
def unpack(cls, stream):
@@ -334,6 +397,9 @@ def unpack(cls, stream):
334397
res.append(unpacked_member)
335398
return res
336399

400+
def get_alignment(self):
401+
raise ValueError("NdrParameters should always be top type in NDR description")
402+
337403

338404
class NdrConformantArray(object):
339405
MEMBER_TYPE = None
@@ -364,6 +430,11 @@ def unpack_conformant(cls, stream, size):
364430
stream.align(4)
365431
return res
366432

433+
@classmethod
434+
def get_alignment(self):
435+
# TODO: test on array of Hyper
436+
return max(4, self.MEMBER_TYPE.get_alignment())
437+
367438

368439
class NdrConformantVaryingArrays(object):
369440
MEMBER_TYPE = None
@@ -406,6 +477,9 @@ def unpack(cls, stream):
406477
def _post_unpack(cls, result):
407478
return result
408479

480+
def get_alignment(self):
481+
# TODO: test on array of Hyper
482+
return max(4, self.MEMBER_TYPE.get_alignment())
409483

410484
class NdrWcharConformantVaryingArrays(NdrConformantVaryingArrays):
411485
MEMBER_TYPE = NdrShort
@@ -487,6 +561,27 @@ def align(self, size):
487561
# print("align {0}: 0".format(size))
488562
return 0
489563

564+
class NdrWriteStream(object):
565+
def __init__(self):
566+
self.data_parts = []
567+
self.data_size = 0
568+
569+
def get_data(self):
570+
data = b"".join(self.data_parts)
571+
assert len(data) == self.data_size
572+
return data
573+
574+
def write(self, data):
575+
self.data_parts.append(data)
576+
self.data_size += len(data)
577+
return None
578+
579+
def align(self, alignement):
580+
if self.data_size % alignement == 0:
581+
return
582+
topadsize = (alignement) - (self.data_size % alignement)
583+
self.write(b"P" * topadsize)
584+
return
490585

491586
def make_parameters(types, name=None):
492587
class NdrCustomParameters(NdrParameters):

0 commit comments

Comments
 (0)