kompiler 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/kompile +136 -29
- data/lib/kompiler/directives.rb +361 -5
- data/lib/kompiler/wrappers/elf_wrapper.rb +2 -2
- data/lib/kompiler/wrappers/macho_wrapper.rb +889 -56
- data/lib/kompiler/wrappers/packed_bytes.rb +18 -4
- metadata +2 -2
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'kompiler/wrappers/packed_bytes'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'digest'
|
2
4
|
|
3
5
|
|
4
6
|
module Kompiler
|
@@ -11,6 +13,29 @@ module MachO
|
|
11
13
|
MH_MAGIC_32 = 0xfeedface
|
12
14
|
MH_MAGIC_64 = 0xfeedfacf
|
13
15
|
|
16
|
+
LC_REQ_DYLD = 0x80000000
|
17
|
+
|
18
|
+
|
19
|
+
# Encodes an unsigned integer into ULEB128 byte sequence
|
20
|
+
def self.encode_uleb value
|
21
|
+
|
22
|
+
result_bytes = ""
|
23
|
+
|
24
|
+
loop do
|
25
|
+
value_bits = value & 0x7f # 7 lower bits
|
26
|
+
value >>= 7 # shift value by 7 bits
|
27
|
+
continue_bit = (value == 0) ? 0 : 1 # "More bytes to come" bit
|
28
|
+
final_byte = value_bits | (continue_bit << 7)
|
29
|
+
result_bytes << final_byte.chr
|
30
|
+
|
31
|
+
break if value == 0 # If nothing else to encode, exit the loop
|
32
|
+
end
|
33
|
+
|
34
|
+
return result_bytes
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
|
14
39
|
|
15
40
|
def self.wrap_obj code, symbols, arch_type: 64, cputype: 0, cpusubtype: 0
|
16
41
|
|
@@ -27,28 +52,427 @@ module MachO
|
|
27
52
|
{
|
28
53
|
name: "__text",
|
29
54
|
vaddr: 0,
|
30
|
-
|
31
|
-
|
32
|
-
|
55
|
+
align: 4,
|
56
|
+
flags: 0x80000400, # section contains some machine instructions
|
57
|
+
content: code,
|
58
|
+
},
|
59
|
+
]
|
60
|
+
}
|
61
|
+
]
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
output = MachO.build(segments: segments, symbols: symbols, prebuilt_lcs: prebuilt_lcs, arch_type: arch_type, cputype: cputype, cpusubtype: cpusubtype, filetype: 1, align_section_contents: true)
|
66
|
+
|
67
|
+
return output
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def self.wrap_exec_static code, symbols, arch_type: 64, cputype: 0, cpusubtype: 0, thread_state: nil, virtual_entry_address: 0x80000, codesign: false
|
73
|
+
|
74
|
+
raise "Thread state must be provided for building a static Mach-O executable" if thread_state == nil
|
75
|
+
|
76
|
+
segments = [
|
77
|
+
{
|
78
|
+
name: "__PAGEZERO",
|
79
|
+
vaddr: 0,
|
80
|
+
vsize: virtual_entry_address - 0x1000,
|
81
|
+
maxprot: 0,
|
82
|
+
initprot: 0,
|
83
|
+
flags: 0,
|
84
|
+
file_offset: 0,
|
85
|
+
filesize: 0,
|
86
|
+
sections: []
|
87
|
+
},
|
88
|
+
{
|
89
|
+
name: "__TEXT",
|
90
|
+
vaddr: virtual_entry_address - 0x1000,
|
91
|
+
maxprot: 7,
|
92
|
+
initprot: 7,
|
93
|
+
flags: 0,
|
94
|
+
file_offset: 0,
|
95
|
+
sections: [
|
96
|
+
{
|
97
|
+
name: "__text",
|
98
|
+
vaddr: virtual_entry_address,
|
99
|
+
align: 0x1000,
|
100
|
+
flags: 0x80000400, # section contains some machine instructions
|
101
|
+
content: code,
|
102
|
+
}
|
103
|
+
]
|
104
|
+
}
|
105
|
+
]
|
106
|
+
|
107
|
+
if codesign
|
108
|
+
segments << {
|
109
|
+
name: "__LINKEDIT",
|
110
|
+
vaddr: 0x200000000,
|
111
|
+
vsize: 0,
|
112
|
+
maxprot: 1,
|
113
|
+
initprot: 1,
|
114
|
+
flags: 0,
|
115
|
+
sections: []
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
uuid_lc = PackedBytes.new
|
121
|
+
uuid_lc.uint32 0x1b
|
122
|
+
uuid_lc.uint32 24
|
123
|
+
uuid_lc.bytes Random.urandom(16) # UUID
|
124
|
+
|
125
|
+
|
126
|
+
unix_thread_lc = PackedBytes.new
|
127
|
+
unix_thread_lc.uint32 0x5 # LC_UNIX_THREAD
|
128
|
+
unix_thread_lc.uint32 thread_state.bytesize + 8 # cmdsize
|
129
|
+
unix_thread_lc.add thread_state
|
130
|
+
|
131
|
+
|
132
|
+
|
133
|
+
prebuilt_lcs = [
|
134
|
+
{
|
135
|
+
bytes: uuid_lc.result,
|
136
|
+
},
|
137
|
+
{
|
138
|
+
bytes: unix_thread_lc.result,
|
139
|
+
},
|
140
|
+
]
|
141
|
+
|
142
|
+
flags = 1
|
143
|
+
|
144
|
+
out = MachO.build segments: segments, symbols: symbols, prebuilt_lcs: prebuilt_lcs, codesign: codesign, arch_type: arch_type, cputype: cputype, cpusubtype: cpusubtype, filetype: 2, flags: flags, align_section_contents: true
|
145
|
+
|
146
|
+
return out
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
# Wrap executable with dynamic linker
|
156
|
+
# Wraps a binary program in Mach-O format using dynamic link commands (especially used on ARM MacOS)
|
157
|
+
#
|
158
|
+
def self.wrap_exec_dylink code, symbols, arch_type: 64, cputype: 0, cpusubtype: 0, virtual_entry_address: 0x80000, codesign: false
|
159
|
+
|
160
|
+
pagezero_size = 0x100000000
|
161
|
+
|
162
|
+
text_segment_vaddr = pagezero_size
|
163
|
+
|
164
|
+
text_section_vaddr = text_segment_vaddr + 0x1000
|
165
|
+
|
166
|
+
virtual_entry_address = text_section_vaddr
|
167
|
+
|
168
|
+
linkedit_vaddr = pagezero_size * 3
|
169
|
+
|
170
|
+
|
171
|
+
|
172
|
+
n_local_sym = symbols.size
|
173
|
+
|
174
|
+
symbols.map! do
|
175
|
+
_1[:value] += text_section_vaddr
|
176
|
+
_1
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
linkedit = PackedBytes.new
|
181
|
+
|
182
|
+
|
183
|
+
symtab, sym_strtab = MachO.build_symtab(symbols, arch_type: arch_type).values
|
184
|
+
|
185
|
+
symtab_offset = linkedit.result.bytesize
|
186
|
+
linkedit.add symtab
|
187
|
+
sym_strtab_offset = linkedit.result.bytesize
|
188
|
+
linkedit.add sym_strtab
|
189
|
+
|
190
|
+
|
191
|
+
segments = [
|
192
|
+
{
|
193
|
+
name: "__PAGEZERO",
|
194
|
+
file_offset: 0,
|
195
|
+
filesize: 0,
|
196
|
+
vaddr: 0,
|
197
|
+
vsize: pagezero_size,
|
198
|
+
flags: 0,
|
199
|
+
maxprot: 0,
|
200
|
+
initprot: 0,
|
201
|
+
sections: [],
|
202
|
+
},
|
203
|
+
{
|
204
|
+
name: "__TEXT",
|
205
|
+
vaddr: text_segment_vaddr,
|
206
|
+
# vsize: 0x4000,
|
207
|
+
maxprot: 7,
|
208
|
+
initprot: 7,
|
209
|
+
flags: 0,
|
210
|
+
file_offset: 0,
|
211
|
+
# filesize: 8192,
|
212
|
+
|
213
|
+
sections: [
|
214
|
+
{
|
215
|
+
name: "__text",
|
216
|
+
vaddr: text_section_vaddr,
|
217
|
+
align: 0x1000,
|
218
|
+
flags: 0x80000400, # section contains some machine instructions
|
33
219
|
content: code,
|
34
220
|
},
|
35
221
|
]
|
222
|
+
},
|
223
|
+
{
|
224
|
+
name: "__LINKEDIT",
|
225
|
+
vaddr: linkedit_vaddr,
|
226
|
+
maxprot: 1,
|
227
|
+
initprot: 1,
|
228
|
+
flags: 0,
|
229
|
+
|
230
|
+
content: linkedit.result,
|
231
|
+
content_align: 0x4000,
|
36
232
|
}
|
37
233
|
]
|
38
234
|
|
235
|
+
entry_point_lc = PackedBytes.new
|
236
|
+
entry_point_lc.uint32 0x80000028 # LC_MAIN
|
237
|
+
entry_point_lc.uint32 24 # cmdsize
|
238
|
+
|
239
|
+
entry_point_lc.uint64 0x80000 # file offset address
|
240
|
+
entry_point_lc.uint64 0 # stack size
|
241
|
+
|
242
|
+
|
243
|
+
|
244
|
+
uuid_lc = PackedBytes.new
|
245
|
+
uuid_lc.uint32 0x1b
|
246
|
+
uuid_lc.uint32 24
|
247
|
+
uuid_lc.bytes Random.urandom(16) # UUID
|
248
|
+
|
249
|
+
|
250
|
+
|
251
|
+
load_dylinker_lc = PackedBytes.new
|
252
|
+
|
253
|
+
load_dylinker_lc.uint32 0xe
|
254
|
+
load_dylinker_lc.uint32 32
|
255
|
+
load_dylinker_lc.bytes "\f\x00\x00\x00/usr/lib/dyld\x00\x00\x00\x00\x00\x00\x00"
|
256
|
+
|
257
|
+
symtab_lc = PackedBytes.new
|
258
|
+
|
259
|
+
symtab_lc.uint32 0x2
|
260
|
+
symtab_lc.uint32 24
|
261
|
+
|
262
|
+
symtab_lc.uint32 0
|
263
|
+
symtab_lc.uint32 symbols.size
|
264
|
+
|
265
|
+
symtab_lc.uint32 0
|
266
|
+
symtab_lc.uint32 sym_strtab.bytesize
|
267
|
+
|
268
|
+
|
269
|
+
dyld_info_lc = PackedBytes.new
|
270
|
+
|
271
|
+
dyld_info_lc.uint32 0x22
|
272
|
+
dyld_info_lc.uint32 12 * 4
|
273
|
+
|
274
|
+
dyld_info_lc.uint32 0 # rebase_off
|
275
|
+
dyld_info_lc.uint32 0 # size
|
276
|
+
|
277
|
+
dyld_info_lc.uint32 0 # bind_off
|
278
|
+
dyld_info_lc.uint32 0 # size
|
279
|
+
|
280
|
+
dyld_info_lc.uint32 0 # weak_bind_off
|
281
|
+
dyld_info_lc.uint32 0 # size
|
282
|
+
|
283
|
+
dyld_info_lc.uint32 0 # lazy_bind_off
|
284
|
+
dyld_info_lc.uint32 0 # size
|
285
|
+
|
286
|
+
dyld_info_lc.uint32 0 # export_off
|
287
|
+
dyld_info_lc.uint32 0 # size
|
288
|
+
|
289
|
+
|
290
|
+
|
291
|
+
code_signature_lc = PackedBytes.new
|
292
|
+
|
293
|
+
code_signature_lc.uint32 0x1d # LC_CODE_SIGNATURE
|
294
|
+
code_signature_lc.uint32 16 # cmdsize
|
295
|
+
|
296
|
+
code_signature_lc.uint32 0 # offset
|
297
|
+
code_signature_lc.uint32 0 # size
|
298
|
+
|
299
|
+
|
300
|
+
|
301
|
+
prebuilt_lcs = [
|
302
|
+
{
|
303
|
+
bytes: symtab_lc.result,
|
304
|
+
relocations: [
|
305
|
+
{
|
306
|
+
content_index: 1,
|
307
|
+
bytefield_offset: 8,
|
308
|
+
bytefield_size: 4,
|
309
|
+
addend: symtab_offset,
|
310
|
+
},
|
311
|
+
{
|
312
|
+
content_index: 1,
|
313
|
+
bytefield_offset: 16,
|
314
|
+
bytefield_size: 4,
|
315
|
+
addend: sym_strtab_offset,
|
316
|
+
}
|
317
|
+
]
|
318
|
+
},
|
319
|
+
{
|
320
|
+
bytes: load_dylinker_lc.result,
|
321
|
+
},
|
322
|
+
{
|
323
|
+
bytes: uuid_lc.result
|
324
|
+
},
|
325
|
+
{
|
326
|
+
bytes: dyld_info_lc.result
|
327
|
+
},
|
328
|
+
{
|
329
|
+
bytes: entry_point_lc.result,
|
330
|
+
relocations: [
|
331
|
+
{
|
332
|
+
content_index: 0,
|
333
|
+
bytefield_offset: 8,
|
334
|
+
bytefield_size: 8
|
335
|
+
}
|
336
|
+
]
|
337
|
+
},
|
338
|
+
]
|
339
|
+
|
340
|
+
|
341
|
+
mh_flags = 0x200000 | 0x4 | 0x1 # MH_PIE | MH_DYLDLINK | MH_NOUNDEFS
|
342
|
+
|
39
343
|
|
40
|
-
output = MachO.build(segments: segments, symbols:
|
344
|
+
output = MachO.build(segments: segments, symbols: nil, prebuilt_lcs: prebuilt_lcs, arch_type: arch_type, codesign: codesign, cputype: cputype, cpusubtype: cpusubtype, filetype: 2, align_section_contents: true, flags: mh_flags)
|
41
345
|
|
42
346
|
return output
|
347
|
+
|
348
|
+
end
|
43
349
|
|
350
|
+
|
351
|
+
def self.build_thread_state_x86_32(r: [0] * 7, esp: 0, ss: 0, eflags: 0, eip: 0, cs: 0, ds: 0, es: 0, fs: 0, gs: 0)
|
352
|
+
raise "r must be an array of 7 elements" if r.size != 7
|
353
|
+
|
354
|
+
thread_state = PackedBytes.new
|
355
|
+
|
356
|
+
thread_state.uint32 1 # flavor = x86_THREAD_STATE_32
|
357
|
+
thread_state.uint32 16 # count = 16 4-byte fields
|
358
|
+
|
359
|
+
r.each do |r_val|
|
360
|
+
thread_state.uint32 r_val
|
361
|
+
end
|
362
|
+
|
363
|
+
thread_state.uint32 esp
|
364
|
+
thread_state.uint32 ss
|
365
|
+
thread_state.uint32 eflags
|
366
|
+
thread_state.uint32 eip
|
367
|
+
thread_state.uint32 cs
|
368
|
+
thread_state.uint32 ds
|
369
|
+
thread_state.uint32 es
|
370
|
+
thread_state.uint32 fs
|
371
|
+
thread_state.uint32 gs
|
372
|
+
|
373
|
+
return thread_state.result
|
44
374
|
end
|
45
375
|
|
46
376
|
|
377
|
+
def self.build_thread_state_x86_64(r: [0] * 16, rip: 0, rflags: 0, cs: 0, fs: 0, gs: 0)
|
378
|
+
|
379
|
+
raise "r must be an array of 16 elements" if r.size != 16
|
47
380
|
|
381
|
+
thread_state = PackedBytes.new
|
382
|
+
|
383
|
+
thread_state.uint32 4 # flavor = x86_THREAD_STATE_64
|
384
|
+
thread_state.uint32 42 # count = 21 8-byte fields / sizeof(int) (4 bytes)
|
385
|
+
|
386
|
+
|
387
|
+
r.each do |r_val|
|
388
|
+
thread_state.uint64 r_val
|
389
|
+
end
|
48
390
|
|
391
|
+
thread_state.uint64 rip
|
392
|
+
|
393
|
+
thread_state.uint64 rflags
|
394
|
+
thread_state.uint64 cs
|
395
|
+
thread_state.uint64 fs
|
396
|
+
thread_state.uint64 gs
|
397
|
+
|
398
|
+
|
399
|
+
return thread_state.result
|
400
|
+
end
|
401
|
+
|
402
|
+
|
403
|
+
|
404
|
+
def self.build_thread_state_arm64(x: [0] * 29, fp: 0, lr: 0, sp: 0, pc: 0, cpsr: 0)
|
405
|
+
|
406
|
+
thread_state = PackedBytes.new
|
407
|
+
|
408
|
+
thread_state.uint32 6 # flavor = ARM_THREAD_STATE64
|
409
|
+
thread_state.uint32 33 * 2 + 2 # count = 33 8-byte fields + 2 4-byte fields / sizeof(int) (4 bytes)
|
410
|
+
|
411
|
+
raise "x must be an array of 29 values." if x.size != 29
|
412
|
+
|
413
|
+
x.each do |x_val|
|
414
|
+
thread_state.uint64 x_val
|
415
|
+
end
|
416
|
+
|
417
|
+
thread_state.uint64 fp
|
418
|
+
thread_state.uint64 lr
|
419
|
+
thread_state.uint64 sp
|
420
|
+
thread_state.uint64 pc
|
421
|
+
|
422
|
+
thread_state.uint32 cpsr
|
423
|
+
|
424
|
+
thread_state.uint32 0 # pad
|
425
|
+
|
426
|
+
return thread_state.result
|
427
|
+
end
|
428
|
+
|
429
|
+
|
430
|
+
|
431
|
+
def self.build_thread_state_arm32(r: [0] * 13, lr: 0, sp: 0, pc: 0, cpsr: 0)
|
432
|
+
|
433
|
+
thread_state = PackedBytes.new
|
434
|
+
|
435
|
+
thread_state.uint32 1 # flavor = ARM_THREAD_STATE
|
436
|
+
thread_state.uint32 17 # count = 17 4-byte fields / sizeof(int) (4 bytes)
|
437
|
+
|
438
|
+
raise "r must be an array of 13 values." if r.size != 13
|
439
|
+
|
440
|
+
r.each do |r_val|
|
441
|
+
thread_state.uint32 r_val
|
442
|
+
end
|
443
|
+
|
444
|
+
thread_state.uint32 lr
|
445
|
+
thread_state.uint32 sp
|
446
|
+
thread_state.uint32 pc
|
447
|
+
|
448
|
+
thread_state.uint32 cpsr
|
449
|
+
|
450
|
+
return thread_state.result
|
451
|
+
end
|
49
452
|
|
50
453
|
|
51
454
|
|
455
|
+
def self.build_thread_state arch: nil, entry_address: 0, stack_pointer: 0
|
456
|
+
raise "arch must be specified" if arch == nil
|
457
|
+
|
458
|
+
case arch
|
459
|
+
when "arm64"
|
460
|
+
return MachO.build_thread_state_arm64(pc: entry_address, sp: stack_pointer)
|
461
|
+
when "arm32"
|
462
|
+
return MachO.build_thread_state_arm32(pc: entry_address, sp: stack_pointer)
|
463
|
+
when "x86-64"
|
464
|
+
r = [0] * 16
|
465
|
+
r[7] = stack_pointer
|
466
|
+
return MachO.build_thread_state_x86_64(rip: entry_address, r: r)
|
467
|
+
when "x86-32"
|
468
|
+
r = [0] * 7
|
469
|
+
return MachO.build_thread_state_x86_32(r: r, eip: entry_address)
|
470
|
+
else
|
471
|
+
raise "Unkown architecture"
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
|
52
476
|
def self.build_macho_header arch_type: 64, cputype: 0, cpusubtype: 0, filetype: 0, ncmds: 0, sizeofcmds: 0, flags: 0
|
53
477
|
|
54
478
|
macho_header = PackedBytes.new
|
@@ -87,8 +511,10 @@ module MachO
|
|
87
511
|
# symbols - list of symbols to be included inside a single symtab (structure below)
|
88
512
|
# prebuilt_lcs - a list of other already built load commands (strings) to be included in the file
|
89
513
|
# arch_type - architecture type, either 32- or 64-bit (default is 64)
|
514
|
+
# codesign - specifies whether to create a code signature. If yes, a __LINKEDIT segment must be present. The code signature will be a code_directory struct with hashes of everything before it. A CODE_SIGNATURE load command will be added automatically.
|
90
515
|
# cputype - Mach-O header cputype field
|
91
516
|
# cpusubtype - Mach-O header cpusubtype field
|
517
|
+
# flags - Mach-O header flags field
|
92
518
|
# filetype - Mach-O file type
|
93
519
|
# align_section_contents - specifies whether to align the contents of each section to its alignment boundary (default is false)
|
94
520
|
#
|
@@ -116,7 +542,25 @@ module MachO
|
|
116
542
|
# sect - symbol's sect field value
|
117
543
|
# desc - symbol's desc field value
|
118
544
|
#
|
119
|
-
|
545
|
+
# Prebuilt load command structure:
|
546
|
+
# bytes - the load command bytes
|
547
|
+
# contents - contents related to the load command
|
548
|
+
# contents_align - alignment boundary in the file for the contents
|
549
|
+
# relocations - a list of relocations to replace some load command's bytes based on the content's file offset
|
550
|
+
#
|
551
|
+
# Prebuilt load command relocation structure:
|
552
|
+
# content_index - index of the contents related to the relocation (contents of segments and symtab will appear last)
|
553
|
+
# bytefield_offset - offset of the to-be-replaced bytes in the load command
|
554
|
+
# bytefield_size - size of the bytefield to be replaced
|
555
|
+
# addend - value to add to the contents' file offset to replace the specified bytefield in the load command with
|
556
|
+
# Relocation logic:
|
557
|
+
# load_command[:bytes][lc_offset...(lc_offset + lc_size)] = int_to_bytes(contents[content_i][:file_offset] + addend, lc_size)
|
558
|
+
#
|
559
|
+
def self.build segments: [], symbols: [], prebuilt_lcs: [], arch_type: 64, cputype: 0, cpusubtype: 0, filetype: 0, align_section_contents: false, flags: 0, codesign: false
|
560
|
+
|
561
|
+
|
562
|
+
n_symtabs = 1
|
563
|
+
n_symtabs = 0 if symbols == nil
|
120
564
|
|
121
565
|
file_content = PackedBytes.new
|
122
566
|
|
@@ -137,24 +581,27 @@ module MachO
|
|
137
581
|
|
138
582
|
symtab_entry_size = 4 * 6
|
139
583
|
|
584
|
+
code_signature_lc_size = 4 * 4
|
140
585
|
|
141
|
-
|
586
|
+
n_code_signature_lcs = codesign ? 1 : 0
|
142
587
|
|
143
|
-
|
588
|
+
n_sections = segments.map{(_1[:sections] || []).size}.sum
|
589
|
+
|
590
|
+
prebuilt_lcs_size = prebuilt_lcs.map{_1[:bytes].bytesize}.sum
|
144
591
|
|
145
|
-
cmds_size = segment_entry_size + section_entry_size * n_sections + symtab_entry_size + prebuilt_lcs_size
|
592
|
+
cmds_size = segment_entry_size * segments.size + section_entry_size * n_sections + symtab_entry_size * n_symtabs + code_signature_lc_size * n_code_signature_lcs + prebuilt_lcs_size
|
146
593
|
|
147
594
|
|
148
|
-
n_cmds = segments.size +
|
595
|
+
n_cmds = segments.size + n_symtabs + n_code_signature_lcs + prebuilt_lcs.size # One segment (with possibly multiple sections), one symtab
|
149
596
|
|
150
597
|
macho_header = build_macho_header(
|
151
|
-
arch_type:
|
598
|
+
arch_type: arch_type,
|
152
599
|
cputype: cputype,
|
153
600
|
cpusubtype: cpusubtype,
|
154
601
|
filetype: filetype,
|
155
602
|
ncmds: n_cmds,
|
156
603
|
sizeofcmds: cmds_size,
|
157
|
-
flags:
|
604
|
+
flags: flags
|
158
605
|
)
|
159
606
|
|
160
607
|
|
@@ -167,14 +614,93 @@ module MachO
|
|
167
614
|
|
168
615
|
|
169
616
|
contents = ""
|
617
|
+
|
618
|
+
|
619
|
+
prebuilt_lc_content_offsets = []
|
620
|
+
|
621
|
+
prebuilt_lcs.each do |lc|
|
622
|
+
if lc.keys.include? :contents
|
623
|
+
|
624
|
+
align = lc[:contents_align] || 1
|
625
|
+
|
626
|
+
file_offset = contents.bytesize + contents_offset
|
627
|
+
|
628
|
+
if file_offset % align != 0
|
629
|
+
pad = align - (file_offset % align)
|
630
|
+
contents << "\0" * pad
|
631
|
+
file_offset += pad
|
632
|
+
end
|
633
|
+
|
634
|
+
prebuilt_lc_content_offsets << file_offset
|
635
|
+
|
636
|
+
contents << lc[:contents]
|
637
|
+
else
|
638
|
+
prebuilt_lc_content_offsets << nil
|
639
|
+
end
|
640
|
+
end
|
170
641
|
|
171
642
|
|
172
643
|
|
644
|
+
contents_offsets = []
|
645
|
+
|
646
|
+
if codesign
|
647
|
+
# Ensure that the __LINKEDIT segment exists, and place it as the last segment to not interfere with other load commands and hash more of the file
|
648
|
+
|
649
|
+
linkedit_segments = segments.each_with_index.filter{|seg, idx| seg[:name] == "__LINKEDIT"}
|
650
|
+
if linkedit_segments.size == 0
|
651
|
+
raise "Segment __LINKEDIT must be present if codesign is true"
|
652
|
+
# segments << {name: "__LINKEDIT", vaddr}
|
653
|
+
else
|
654
|
+
linkedit_segment, linkedit_segment_idx = linkedit_segments[0]
|
655
|
+
|
656
|
+
# Make __LINKEDIT the last segment
|
657
|
+
segments[-1], segments[linkedit_segment_idx] = segments[linkedit_segment_idx], segments[-1]
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
|
662
|
+
|
663
|
+
|
664
|
+
|
173
665
|
segments.each do |segment|
|
174
666
|
|
175
667
|
raise "Segment name is larger than 16 characters" if segment[:name].bytesize > 16
|
176
668
|
|
177
|
-
|
669
|
+
raise "Segment can't contain both the :content and :sections key" if segment.keys.include?(:content) && segment.keys.include?(:sections) && segment[:sections].size > 0
|
670
|
+
|
671
|
+
if segment.keys.include? :content
|
672
|
+
|
673
|
+
segment[:sections] = []
|
674
|
+
|
675
|
+
alignment = segment[:content_align] || 1
|
676
|
+
|
677
|
+
file_offset = contents_offset + contents.bytesize
|
678
|
+
if file_offset % alignment != 0
|
679
|
+
pad = alignment - (file_offset % alignment)
|
680
|
+
contents << "\0" * pad
|
681
|
+
file_offset += pad
|
682
|
+
end
|
683
|
+
|
684
|
+
contents << segment[:content]
|
685
|
+
|
686
|
+
segment[:file_offset] ||= file_offset
|
687
|
+
segment[:filesize] ||= segment[:content].bytesize
|
688
|
+
|
689
|
+
segment[:vaddr] ||= 0
|
690
|
+
segment[:vsize] ||= segment[:filesize]
|
691
|
+
|
692
|
+
segment[:flags] ||= 0
|
693
|
+
|
694
|
+
segment[:maxprot] ||= 0
|
695
|
+
segment[:initprot] ||= 0
|
696
|
+
|
697
|
+
contents_offsets << segment[:file_offset]
|
698
|
+
|
699
|
+
next
|
700
|
+
end
|
701
|
+
|
702
|
+
|
703
|
+
start_offset = contents.bytesize + contents_offset
|
178
704
|
|
179
705
|
segment[:sections].each_with_index do |section, section_i|
|
180
706
|
|
@@ -187,7 +713,7 @@ module MachO
|
|
187
713
|
sec_offset = contents_offset + contents.bytesize
|
188
714
|
|
189
715
|
pad_amount = 0
|
190
|
-
if (sec_offset % section[:align] != 0) &&
|
716
|
+
if (sec_offset % section[:align] != 0) && align_section_contents
|
191
717
|
pad_amount = section[:align] - (sec_offset % section[:align])
|
192
718
|
end
|
193
719
|
|
@@ -197,20 +723,21 @@ module MachO
|
|
197
723
|
section[:file_offset] = sec_offset
|
198
724
|
|
199
725
|
if section_i == 0
|
200
|
-
segment[:file_offset]
|
201
|
-
start_size = contents.bytesize
|
726
|
+
segment[:file_offset] ||= sec_offset
|
202
727
|
end
|
203
728
|
|
729
|
+
contents_offsets << section[:file_offset]
|
730
|
+
|
204
731
|
contents << section[:content]
|
205
732
|
|
206
733
|
section[:vaddr] ||= 0
|
207
734
|
section[:vsize] ||= section[:content].bytesize
|
208
735
|
end
|
209
736
|
|
210
|
-
|
737
|
+
end_offset = contents.bytesize + contents_offset
|
211
738
|
|
212
|
-
segment[:file_offset] ||=
|
213
|
-
segment[:filesize]
|
739
|
+
segment[:file_offset] ||= start_offset
|
740
|
+
segment[:filesize] ||= end_offset - segment[:file_offset]
|
214
741
|
|
215
742
|
segment[:vaddr] ||= 0
|
216
743
|
segment[:vsize] ||= segment[:filesize]
|
@@ -222,48 +749,209 @@ module MachO
|
|
222
749
|
end
|
223
750
|
|
224
751
|
|
225
|
-
|
752
|
+
if codesign
|
226
753
|
|
227
|
-
|
228
|
-
|
229
|
-
|
754
|
+
# The __LINKEDIT segment is last (logic written previously)
|
755
|
+
linkedit_segment = segments.last
|
756
|
+
|
230
757
|
|
231
|
-
|
232
|
-
if symbol.keys.include? :name
|
233
|
-
symbol[:name_offset] = sym_strtab_contents.bytesize
|
234
|
-
sym_strtab_contents << symbol[:name] + "\0"
|
235
|
-
else
|
236
|
-
symbol[:name_offset] = 0
|
237
|
-
end
|
758
|
+
code_directory = PackedBytes.new endianess: :be
|
238
759
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
760
|
+
code_directory.uint32 0xfade0c02 # Magic
|
761
|
+
|
762
|
+
code_directory_length_offset = code_directory.result.bytesize
|
763
|
+
code_directory.uint32 0 # Length (will be replaced)
|
764
|
+
|
765
|
+
code_directory.uint32 0 # Version
|
766
|
+
|
767
|
+
code_directory.uint32 0 # Flags
|
768
|
+
|
769
|
+
code_directory_header_size = 96
|
770
|
+
|
771
|
+
hash_array_offset = code_directory_header_size
|
772
|
+
|
773
|
+
total_hashing_size = linkedit_segment[:file_offset] + linkedit_segment[:filesize]
|
774
|
+
|
775
|
+
hashing_pagesize = 0x1000
|
776
|
+
|
777
|
+
n_hashes = (total_hashing_size / hashing_pagesize.to_f).ceil
|
778
|
+
|
779
|
+
hash_size = 32
|
780
|
+
hash_type = 2 # SHA256
|
781
|
+
|
782
|
+
hash_array_size = hash_size * n_hashes
|
783
|
+
|
784
|
+
|
785
|
+
ident_offset = hash_array_offset + hash_array_size
|
786
|
+
|
787
|
+
|
788
|
+
code_directory.uint32 hash_array_offset # Hash offset
|
789
|
+
|
790
|
+
code_directory.uint32 ident_offset # Identifier string offset
|
791
|
+
|
792
|
+
code_directory.uint32 0 # nSpecialSlots
|
793
|
+
|
794
|
+
code_directory.uint32 n_hashes # nCodeSlots
|
795
|
+
|
796
|
+
code_directory.uint32 total_hashing_size # codeLimit
|
797
|
+
|
798
|
+
|
799
|
+
code_directory.uint8 hash_size
|
800
|
+
code_directory.uint8 hash_type
|
801
|
+
|
802
|
+
code_directory.uint8 0
|
803
|
+
hashing_pagesize_enc = Math.log2(hashing_pagesize)
|
804
|
+
raise "Code signing pagesize is not a power of two" if hashing_pagesize_enc.to_i != hashing_pagesize_enc
|
805
|
+
code_directory.uint8 hashing_pagesize_enc.to_i
|
806
|
+
|
807
|
+
code_directory.uint32 0 # spare2
|
808
|
+
|
809
|
+
code_directory.uint32 0 # scatterOffset (zero for absent)
|
810
|
+
code_directory.uint32 0 # teamIDOffset (zero for absent)
|
811
|
+
|
812
|
+
code_directory.uint32 0 # spare3
|
813
|
+
|
814
|
+
code_directory.uint64 total_hashing_size # codeLimit64
|
815
|
+
|
816
|
+
code_directory.uint64 0 # offset of executable segment
|
817
|
+
code_directory.uint64 0 # limit of executable segment
|
818
|
+
code_directory.uint64 0 # exec segment flags
|
819
|
+
|
820
|
+
code_directory.uint32 0 # runtime
|
821
|
+
code_directory.uint32 0xFFFF # pre-encrypt hash slots offset
|
822
|
+
|
823
|
+
|
824
|
+
|
825
|
+
hash_array_placeholder = "\0" * hash_array_size
|
826
|
+
|
827
|
+
|
828
|
+
code_directory.add hash_array_placeholder
|
829
|
+
|
830
|
+
ident_string = "program-#{SecureRandom.uuid.gsub('-', '')}\0"
|
831
|
+
|
832
|
+
code_directory.add ident_string
|
833
|
+
|
834
|
+
|
835
|
+
code_directory_bytes = code_directory.result
|
836
|
+
code_directory_bytes[code_directory_length_offset...(code_directory_length_offset + 4)] = [code_directory_bytes.bytesize].pack("L>")
|
837
|
+
|
838
|
+
blobs = [
|
839
|
+
{type: 0, content: code_directory.result}
|
840
|
+
]
|
841
|
+
|
842
|
+
# Embedded signature blob (superblob with magic = 0xfade0cc0)
|
843
|
+
superblob = MachO.build_superblob(magic: 0xfade0cc0, blobs: blobs)
|
844
|
+
|
845
|
+
|
846
|
+
# Calculate the offset of the hash array that will be replaced later
|
847
|
+
|
848
|
+
code_directory_offset = MachO.calc_superblob_offset(blobs, 0)
|
849
|
+
superblob_offset = linkedit_segment[:file_offset] + linkedit_segment[:filesize]
|
850
|
+
|
851
|
+
codesign_hash_array_offset = superblob_offset + code_directory_offset + hash_array_offset
|
852
|
+
|
853
|
+
contents << superblob
|
854
|
+
|
855
|
+
# Change the linkedit segment to include the embedded signature blob
|
856
|
+
linkedit_segment[:filesize] += superblob.bytesize
|
857
|
+
linkedit_segment[:vsize] += superblob.bytesize
|
858
|
+
|
859
|
+
# Add a code signature load command
|
860
|
+
code_signature_lc = PackedBytes.new
|
861
|
+
code_signature_lc.uint32 0x1d # LC_CODE_SIGNATURE
|
862
|
+
code_signature_lc.uint32 4 * 4 # cmdsize
|
863
|
+
|
864
|
+
code_signature_lc.uint32 superblob_offset
|
865
|
+
code_signature_lc.uint32 superblob.bytesize
|
866
|
+
|
867
|
+
|
868
|
+
prebuilt_lcs << {
|
869
|
+
bytes: code_signature_lc.result
|
870
|
+
}
|
871
|
+
|
244
872
|
end
|
245
873
|
|
246
874
|
|
247
|
-
symtab_offset = contents.bytesize + contents_offset
|
248
|
-
symtab_align = 8
|
249
875
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
876
|
+
symtab_offset = nil
|
877
|
+
|
878
|
+
|
879
|
+
if symbols != nil
|
880
|
+
|
881
|
+
symtab_contents = PackedBytes.new
|
882
|
+
|
883
|
+
sym_strtab_contents = "\0"
|
884
|
+
|
885
|
+
symbols.each do |symbol|
|
886
|
+
if symbol.keys.include? :name
|
887
|
+
symbol[:name_offset] = sym_strtab_contents.bytesize
|
888
|
+
sym_strtab_contents << symbol[:name] + "\0"
|
889
|
+
else
|
890
|
+
symbol[:name_offset] = 0
|
891
|
+
end
|
892
|
+
|
893
|
+
symtab_contents.uint32 symbol[:name_offset]
|
894
|
+
symtab_contents.uint8 symbol[:type]
|
895
|
+
symtab_contents.uint8 symbol[:sect]
|
896
|
+
symtab_contents.uint16 symbol[:desc]
|
897
|
+
symtab_contents.bytes symbol[:value], flexval_size
|
898
|
+
end
|
899
|
+
|
900
|
+
|
901
|
+
symtab_offset = contents.bytesize + contents_offset
|
902
|
+
symtab_align = 360
|
903
|
+
|
904
|
+
if symtab_offset % symtab_align != 0
|
905
|
+
pad = (symtab_align - (symtab_offset % symtab_align))
|
906
|
+
contents << "\0" * pad
|
907
|
+
symtab_offset += pad
|
908
|
+
end
|
909
|
+
|
910
|
+
# contents_offsets << symtab_offset
|
911
|
+
|
912
|
+
contents << symtab_contents.result
|
913
|
+
|
914
|
+
sym_strtab_offset = contents.bytesize + contents_offset
|
915
|
+
|
916
|
+
contents << sym_strtab_contents
|
917
|
+
|
254
918
|
end
|
255
919
|
|
256
|
-
contents << symtab_contents.result
|
257
920
|
|
258
|
-
sym_strtab_offset = contents.bytesize + contents_offset
|
259
|
-
|
260
|
-
contents << sym_strtab_contents
|
261
921
|
|
922
|
+
contents_offsets << symtab_offset if symtab_offset != nil
|
262
923
|
|
924
|
+
contents_offsets += prebuilt_lc_content_offsets
|
263
925
|
|
264
|
-
load_commands = PackedBytes.new
|
265
926
|
|
266
927
|
|
928
|
+
prebuilt_lcs.each do |lc|
|
929
|
+
|
930
|
+
if lc.keys.include? :relocations
|
931
|
+
lc[:relocations].each do |reloc|
|
932
|
+
|
933
|
+
addr = contents_offsets[reloc[:content_index]]
|
934
|
+
|
935
|
+
raise "Load command contents at index #{reloc[:content_index]} is empty and doesn't have an offset" if addr == nil
|
936
|
+
|
937
|
+
write_val = addr + (reloc[:addend] || 0)
|
938
|
+
|
939
|
+
bytesize = reloc[:bytefield_size]
|
940
|
+
|
941
|
+
bits = (0...(bytesize * 8)).map{|bit_i| write_val[bit_i] }
|
942
|
+
bytes = (0...bytesize).map{|byte_i| bits[(byte_i*8)...(byte_i*8 + 8)] }.map{|byte_bits| byte_bits.each_with_index.map{|bit, bit_i| 2 ** bit_i * bit }.sum }
|
943
|
+
str_bytes = bytes.map(&:chr).join
|
944
|
+
|
945
|
+
lc[:bytes][reloc[:bytefield_offset]...(reloc[:bytefield_offset] + bytesize)] = str_bytes
|
946
|
+
end
|
947
|
+
end
|
948
|
+
|
949
|
+
end
|
950
|
+
|
951
|
+
|
952
|
+
load_commands = PackedBytes.new
|
953
|
+
|
954
|
+
|
267
955
|
segments.each do |segment|
|
268
956
|
|
269
957
|
case arch_type
|
@@ -301,7 +989,11 @@ module MachO
|
|
301
989
|
load_commands.bytes section[:vsize], flexval_size
|
302
990
|
|
303
991
|
load_commands.uint32 section[:file_offset]
|
304
|
-
|
992
|
+
|
993
|
+
align_value = Math.log2 section[:align]
|
994
|
+
raise "Section alignment is not a power of 2" if align_value.to_i != align_value
|
995
|
+
|
996
|
+
load_commands.uint32 align_value
|
305
997
|
|
306
998
|
load_commands.uint32 0 # reloff
|
307
999
|
load_commands.uint32 0 # nreloc
|
@@ -322,27 +1014,168 @@ module MachO
|
|
322
1014
|
|
323
1015
|
# Symtab load command
|
324
1016
|
|
325
|
-
|
326
|
-
|
1017
|
+
if symbols != nil
|
1018
|
+
load_commands.uint32 0x2 # LC_SYMTAB
|
1019
|
+
load_commands.uint32 symtab_entry_size
|
327
1020
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
load_commands.uint32 sym_strtab_offset
|
332
|
-
load_commands.uint32 sym_strtab_contents.bytesize
|
1021
|
+
load_commands.uint32 symtab_offset
|
1022
|
+
load_commands.uint32 symbols.size
|
333
1023
|
|
1024
|
+
load_commands.uint32 sym_strtab_offset
|
1025
|
+
load_commands.uint32 sym_strtab_contents.bytesize
|
1026
|
+
end
|
1027
|
+
|
334
1028
|
|
335
|
-
|
1029
|
+
prebuilt_lcs.each do |lc|
|
1030
|
+
load_commands.bytes lc[:bytes]
|
1031
|
+
end
|
336
1032
|
|
337
1033
|
|
338
1034
|
file_content.add load_commands
|
339
1035
|
|
340
1036
|
|
341
1037
|
file_content.add contents
|
1038
|
+
|
1039
|
+
|
1040
|
+
if codesign
|
1041
|
+
# Compute the hashes and replace them instead of the placeholder
|
1042
|
+
|
1043
|
+
out = file_content.result
|
1044
|
+
|
1045
|
+
hash_array_contents = ""
|
1046
|
+
|
1047
|
+
n_hashes.times do |hash_i|
|
1048
|
+
start_offset = hash_i * hashing_pagesize
|
1049
|
+
# Enf offset is either the end of the page or the code_limit field from code_directory
|
1050
|
+
end_offset = [start_offset + hashing_pagesize, total_hashing_size].min
|
1051
|
+
|
1052
|
+
hashed = Digest::SHA256.digest out[start_offset...end_offset]
|
1053
|
+
|
1054
|
+
hash_array_contents << hashed
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
|
1058
|
+
hash_array_start = codesign_hash_array_offset
|
1059
|
+
hash_array_end = hash_array_start + hash_array_contents.bytesize
|
1060
|
+
|
1061
|
+
out[hash_array_start...hash_array_end] = hash_array_contents
|
1062
|
+
|
1063
|
+
return out
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
|
1067
|
+
return file_content.result
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
|
1071
|
+
|
1072
|
+
# Builds a code signature SuperBlob structure
|
1073
|
+
#
|
1074
|
+
# Arguments:
|
1075
|
+
# magic - SuperBlob's magic field value (default is 0)
|
1076
|
+
# blobs - blobs included in the superblob (structure below)
|
1077
|
+
#
|
1078
|
+
# Blob structure:
|
1079
|
+
# type - blob's type
|
1080
|
+
# content - blob's content
|
1081
|
+
#
|
1082
|
+
def self.build_superblob magic: 0, blobs: []
|
1083
|
+
|
1084
|
+
superblob = PackedBytes.new endianess: :be
|
1085
|
+
|
1086
|
+
superblob.uint32 0xfade0cc0 # magic
|
1087
|
+
|
1088
|
+
superblob_length_offset = superblob.result.bytesize
|
1089
|
+
superblob.uint32 0 # length (will be replaced)
|
1090
|
+
|
1091
|
+
superblob.uint32 1 # Number of entries
|
1092
|
+
|
1093
|
+
|
1094
|
+
index_entry_size = 8
|
1095
|
+
index_entries_size = index_entry_size * blobs.size
|
1096
|
+
|
1097
|
+
superblob_header_size = 4 * 3
|
1098
|
+
|
1099
|
+
blobs_offset = superblob_header_size + index_entries_size
|
1100
|
+
|
1101
|
+
blob_contents = ""
|
1102
|
+
|
1103
|
+
blobs.each do |blob|
|
1104
|
+
blob[:offset] = blobs_offset + blob_contents.bytesize
|
1105
|
+
blob_contents << blob[:content]
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
# Index entries
|
1109
|
+
blobs.each do |blob|
|
1110
|
+
superblob.uint32 blob[:type]
|
1111
|
+
superblob.uint32 blob[:offset]
|
1112
|
+
end
|
1113
|
+
|
1114
|
+
# Blob contents write
|
1115
|
+
blobs.each do |blob|
|
1116
|
+
superblob.add blob[:content]
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
|
1120
|
+
out = superblob.result
|
1121
|
+
out[superblob_length_offset...(superblob_length_offset + 4)] = [out.bytesize].pack("L>")
|
1122
|
+
|
1123
|
+
out
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
|
1127
|
+
# Calculates the offset of the specified blob from the beginning of the superblob
|
1128
|
+
def self.calc_superblob_offset blobs, index
|
1129
|
+
|
1130
|
+
superblob_header_size = 4 * 3
|
1131
|
+
|
1132
|
+
index_entry_size = 8
|
342
1133
|
|
1134
|
+
index_entries_size = index_entry_size * blobs.size
|
1135
|
+
|
1136
|
+
total_offset = superblob_header_size + index_entries_size
|
1137
|
+
|
1138
|
+
blobs[...index].each do |blob|
|
1139
|
+
total_offset += blob[:content].bytesize
|
1140
|
+
end
|
1141
|
+
|
1142
|
+
total_offset
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
|
1146
|
+
|
1147
|
+
|
1148
|
+
# Builds a symtab and a strtab based off input symbols
|
1149
|
+
def self.build_symtab symbols, arch_type: 64
|
343
1150
|
|
344
|
-
|
1151
|
+
case arch_type
|
1152
|
+
when 64
|
1153
|
+
flexval_size = 8
|
1154
|
+
when 32
|
1155
|
+
flexval_size = 4
|
1156
|
+
end
|
1157
|
+
|
1158
|
+
symtab_contents = PackedBytes.new
|
345
1159
|
|
1160
|
+
sym_strtab_contents = "\0"
|
1161
|
+
|
1162
|
+
symbols.each do |symbol|
|
1163
|
+
if symbol.keys.include? :name
|
1164
|
+
symbol[:name_offset] = sym_strtab_contents.bytesize
|
1165
|
+
sym_strtab_contents << symbol[:name] + "\0"
|
1166
|
+
else
|
1167
|
+
symbol[:name_offset] = 0
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
symtab_contents.uint32 symbol[:name_offset]
|
1171
|
+
symtab_contents.uint8 symbol[:type]
|
1172
|
+
symtab_contents.uint8 symbol[:sect]
|
1173
|
+
symtab_contents.uint16 symbol[:desc]
|
1174
|
+
symtab_contents.bytes symbol[:value], flexval_size
|
1175
|
+
end
|
1176
|
+
|
1177
|
+
|
1178
|
+
return {symtab: symtab_contents.result, strtab: sym_strtab_contents}
|
346
1179
|
end
|
347
1180
|
|
348
1181
|
|
@@ -357,7 +1190,7 @@ module MachO
|
|
357
1190
|
def self.labels_to_symbols labels,
|
358
1191
|
out = []
|
359
1192
|
labels.each do |name, value|
|
360
|
-
out << create_symbol(name: name, value: value, type: :sect, external:
|
1193
|
+
out << create_symbol(name: name, value: value, type: :sect, external: true, section_number: 1)
|
361
1194
|
end
|
362
1195
|
return out
|
363
1196
|
end
|