Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Commit

Permalink
HostWriter: Remove signature on MAC host (#8625)
Browse files Browse the repository at this point in the history
In order to conform with Catalina notarization requirements, the template apphost
will be shipped after codesigning.

This commit implements a change to remove the code-signature when customizing the apphost for an app.

Testing:
- Tested the HostModel library using a locally signed version of apphost
- Verified that unsigned host can be re-signed later
- Verified that the original unsigned-apphost, and the signature-removed-apphost are binary equal.
  • Loading branch information
swaroop-sridhar authored and mmitche committed Nov 21, 2019
1 parent 924d3fe commit e77f4a4
Show file tree
Hide file tree
Showing 3 changed files with 376 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;

namespace Microsoft.NET.HostModel.AppHost
{
/// <summary>
/// Additional details about the failure with caused an AppHostMachOFormatException
/// </summary>
public enum MachOFormatError
{
Not64BitExe, // Apphost is expected to be a 64-bit MachO executable
DuplicateLinkEdit, // Only one __LINKEDIT segment is expected in the apphost
DuplicateSymtab, // Only one SYMTAB is expected in the apphost
SignNeedsLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT
SignNeedsSymtab, // CODE_SIGNATURE command must follow the SYMTAB command
LinkEditNotLast, // __LINKEDIT must be the last segment in the binary layout
SymtabNotInLinkEdit, // SYMTAB must within the __LINKEDIT segment!
SignNotInLinkEdit, // Signature blob must be within the __LINKEDIT segment!
SignCommandNotLast, // CODE_SIGNATURE command must be the last command
SignBlobNotLast, // Signature blob must be at the very end of the file
SignDoesntFollowSymtab, // Signature blob must immediately follow the Symtab
MemoryMapAccessFault, // Error reading the memory-mapped apphost
InvalidUTF8 // UTF8 decoding failed
}

/// <summary>
/// The MachO application host executable cannot be customized because
/// it was not in the expected format
/// </summary>
public class AppHostMachOFormatException : AppHostUpdateException
{
public readonly MachOFormatError Error;

public AppHostMachOFormatException(MachOFormatError error)
{
Error = error;
}
}
}

16 changes: 14 additions & 2 deletions src/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,26 @@ void UpdateResources()
}
}

void RemoveSignatureIfMachO()
{
MachOUtils.RemoveSignature(appHostDestinationFilePath);
}

void SetLastWriteTime()
{
// Memory-mapped write does not updating last write time
File.SetLastWriteTimeUtc(appHostDestinationFilePath, DateTime.UtcNow);
}

try
{
RetryUtil.RetryOnIOError(RewriteAppHost);

RetryUtil.RetryOnWin32Error(UpdateResources);

// Memory-mapped write does not updating last write time
RetryUtil.RetryOnIOError(() => File.SetLastWriteTimeUtc(appHostDestinationFilePath, DateTime.UtcNow));
RetryUtil.RetryOnIOError(RemoveSignatureIfMachO);

RetryUtil.RetryOnIOError(SetLastWriteTime);
}
catch (Exception ex)
{
Expand Down
319 changes: 319 additions & 0 deletions src/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices;
using System.Text;

namespace Microsoft.NET.HostModel.AppHost
{
internal static class MachOUtils
{
// The MachO Headers are copied from
// https://opensource.apple.com/source/cctools/cctools-870/include/mach-o/loader.h
//
// The data fields and enumerations match the structure definitions in the above file,
// and hence do not conform to C# CoreFx naming style.

enum Magic : uint
{
MH_MAGIC = 0xfeedface,
MH_CIGAM = 0xcefaedfe,
MH_MAGIC_64 = 0xfeedfacf,
MH_CIGAM_64 = 0xcffaedfe
}

enum FileType : uint
{
MH_EXECUTE = 0x2
}

#pragma warning disable 0649
struct MachHeader
{
public Magic magic;
public int cputype;
public int cpusubtype;
public FileType filetype;
public uint ncmds;
public uint sizeofcmds;
public uint flags;
public uint reserved;

public bool Is64BitExecutable()
{
return magic == Magic.MH_MAGIC_64 && filetype == FileType.MH_EXECUTE;
}

public bool IsValid()
{
switch (magic)
{
case Magic.MH_CIGAM:
case Magic.MH_CIGAM_64:
case Magic.MH_MAGIC:
case Magic.MH_MAGIC_64:
return true;

default:
return false;
}
}
}

enum Command : uint
{
LC_SYMTAB = 0x2,
LC_SEGMENT_64 = 0x19,
LC_CODE_SIGNATURE = 0x1d,
}

struct LoadCommand
{
public Command cmd;
public uint cmdsize;
}

// The linkedit_data_command contains the offsets and sizes of a blob
// of data in the __LINKEDIT segment (including LC_CODE_SIGNATURE).
struct LinkEditDataCommand
{
public Command cmd;
public uint cmdsize;
public uint dataoff;
public uint datasize;

public void ZeroInit()
{
cmd = 0;
cmdsize = 0;
dataoff = 0;
datasize = 0;
}
}

struct SymtabCommand
{
public uint cmd;
public uint cmdsize;
public uint symoff;
public uint nsyms;
public uint stroff;
public uint strsize;
};

unsafe struct SegmentCommand64
{
public Command cmd;
public uint cmdsize;
public fixed byte segname[16];
public ulong vmaddr;
public ulong vmsize;
public ulong fileoff;
public ulong filesize;
public int maxprot;
public int initprot;
public uint nsects;
public uint flags;

public string SegName
{
get
{
fixed (byte* p = segname)
{
int len = 0;
while (*(p + len) != 0 && len++ < 16) ;

try
{
return Encoding.UTF8.GetString(p, len);
}
catch(ArgumentException)
{
throw new AppHostMachOFormatException(MachOFormatError.InvalidUTF8);
}
}
}
}
}

#pragma warning restore 0649

private static void Verify(bool condition, MachOFormatError error)
{
if (!condition)
{
throw new AppHostMachOFormatException(error);
}
}

/// <summary>
/// This Method is a utility to remove the code-signature (if any)
/// from a MachO AppHost binary.
///
/// The tool assumes the following layout of the executable:
///
/// * MachoHeader (64-bit, executable, not swapped integers)
/// * LoadCommands
/// LC_SEGMENT_64 (__PAGEZERO)
/// LC_SEGMENT_64 (__TEXT)
/// LC_SEGMENT_64 (__DATA)
/// LC_SEGMENT_64 (__LINKEDIT)
/// ...
/// LC_SYMTAB
/// ...
/// LC_CODE_SIGNATURE (last)
///
/// * ... Different Segments ...
///
/// * The __LINKEDIT Segment (last)
/// * ... Different sections ...
/// * SYMTAB
/// * (Some alignment bytes)
/// * The Code-signature
///
/// In order to remove the signature, the method:
/// - Removes (zeros out) the LC_CODE_SIGNATURE command
/// - Adjusts the size and count of the load commands in the header
/// - Truncates the size of the __LINKEDIT segment to the end of SYMTAB
/// - Truncates the apphost file to the end of the __LINKEDIT segment
///
/// </summary>
/// <param name="filePath">Path to the AppHost</param>
/// <returns>
/// True if
/// - The input is a MachO binary, and
/// - It is a signed binary, and
/// - The signature was successfully removed
/// False otherwise
/// </returns>
/// <exception cref="AppHostMachOFormatException">
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
/// </exception>
unsafe public static bool RemoveSignature(string filePath)
{
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite))
{
uint signatureSize = 0;
using (var mappedFile = MemoryMappedFile.CreateFromFile(stream,
mapName: null,
capacity: 0,
MemoryMappedFileAccess.ReadWrite,
HandleInheritability.None,
leaveOpen: true))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
byte* file = null;
RuntimeHelpers.PrepareConstrainedRegions();
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
Verify(file != null, MachOFormatError.MemoryMapAccessFault);

MachHeader* header = (MachHeader*)file;

if (!header->IsValid())
{
// Not a MachO file.
return false;
}

Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);

file += sizeof(MachHeader);
SegmentCommand64* linkEdit = null;
SymtabCommand* symtab = null;
LinkEditDataCommand* signature = null;

for (uint i = 0; i < header->ncmds; i++)
{
LoadCommand* command = (LoadCommand*)file;
if (command->cmd == Command.LC_SEGMENT_64)
{
SegmentCommand64* segment = (SegmentCommand64*)file;
if (segment->SegName.Equals("__LINKEDIT"))
{
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
linkEdit = segment;
}
}
else if (command->cmd == Command.LC_SYMTAB)
{
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
symtab = (SymtabCommand*)command;
}
else if (command->cmd == Command.LC_CODE_SIGNATURE)
{
Verify(i == header->ncmds - 1, MachOFormatError.SignCommandNotLast);
signature = (LinkEditDataCommand*)command;
break;
}

file += command->cmdsize;
}

if (signature != null)
{
Verify(linkEdit != null, MachOFormatError.SignNeedsLinkEdit);
Verify(symtab != null, MachOFormatError.SignNeedsSymtab);

var symtabEnd = symtab->stroff + symtab->strsize;
var linkEditEnd = linkEdit->fileoff + linkEdit->filesize;
var signatureEnd = signature->dataoff + signature->datasize;
var fileEnd = (ulong)stream.Length;

Verify(linkEditEnd == fileEnd, MachOFormatError.LinkEditNotLast);
Verify(signatureEnd == fileEnd, MachOFormatError.SignBlobNotLast);

Verify(symtab->symoff > linkEdit->fileoff, MachOFormatError.SymtabNotInLinkEdit);
Verify(signature->dataoff > linkEdit->fileoff, MachOFormatError.SignNotInLinkEdit);

// The signature blob immediately follows the symtab blob,
// except for a few bytes of padding.
Verify(signature->dataoff >= symtabEnd && signature->dataoff - symtabEnd < 32, MachOFormatError.SignBlobNotLast);

// Remove the signature command
header->ncmds--;
header->sizeofcmds -= signature->cmdsize;

signature->ZeroInit();

// Remove the signature blob (note for truncation)
signatureSize = (uint)(fileEnd - symtabEnd);

// Adjust the __LINKEDIT segment load command
linkEdit->filesize -= signatureSize;

// codesign --remove-signature doesn't reset the vmsize.
// Setting the vmsize here makes the output bin-equal with the original
// unsigned apphost (and not bin-equal with a signed-unsigned-apphost).
linkEdit->vmsize = linkEdit->filesize;
}
}
finally
{
if(file != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
}

if (signatureSize != 0)
{
// The signature was removed, update the file length
stream.SetLength(stream.Length - signatureSize);
return true;
}

return false;
}
}
}
}

0 comments on commit e77f4a4

Please sign in to comment.