Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Checking code integrity in iOS

How could I guarantee the integrity of the code of an iOS app? I've been taking a look to Apple's Security Overview document, would code signing be enough? Is there any other recommended mechanism to guarantee the code integrity?

Thanks in advance

like image 868
AppsDev Avatar asked Apr 13 '15 05:04

AppsDev


People also ask

How do you verify the integrity of an app iOS?

To use the app, connect to the Internet and tap the Verify App button. After you verify an app for the first time, your iPhone, iPad, or iPod touch must reverify the app developer's certificate periodically to maintain trust. If you can't reverify, you may see a message that verification will expire soon.

What is integrity in iOS?

Among the different mobile platforms available, in this post, we will talk about Android and iOS. Both of these platforms provide their specific implementations and tools to validate their app's integrity, which means to verify that your servers are interacting with the original and unmodified version of the apps.

How do I enable app integrity?

Go to APIs and services and select enable APIs and services. Search for Play Integrity API. Select it and then select Enable.


1 Answers

I had a same problem. This is easy on OS X but somewhat difficult in iOS because iOS doesn't have API like SecStaticCodeCheckValidity.

There are two sections in mach-o binary that you can use to ensure integrity of the app.

  1. LC_ENCRYPTION_INFO
  2. LC_CODE_SIGNATURE

1. LC_ENCRYPTION_INFO

First, LC_ENCRYPTION_INFO stores informations about 'app store encryption'. Once an app is uploaded to app store, app is encrypted before it is released to users.

binary before uploading to appstore or decrypted

otool -l [binary] | grep LC_ENCRYPTION_INFO -A5
          cmd LC_ENCRYPTION_INFO
      cmdsize 20
     cryptoff 16384
    cryptsize 5783552
      cryptid 0
--
          cmd LC_ENCRYPTION_INFO_64
      cmdsize 24
     cryptoff 16384
    cryptsize 6635520
      cryptid 0
          pad 0

binary after uploading to appstore (encrypted)

otool -l [binary] | grep LC_ENCRYPTION_INFO -A5
          cmd LC_ENCRYPTION_INFO
      cmdsize 20
     cryptoff 16384
    cryptsize 5783552
      cryptid 1
--
          cmd LC_ENCRYPTION_INFO_64
      cmdsize 24
     cryptoff 16384
    cryptsize 6635520
      cryptid 1
          pad 0

As you can see, 'cryptid' is set to 1 when app is uploaded. So checking 'cryptid' bit will tell us if the binary is encrypted or not.

You may think that this can be bypassed easily by just setting the bit to 1, but then OS will try to decrypt the binary which will make the codes to unrecognizable bytes.

bool isBinaryEncrypted()
{
    // checking current binary's LC_ENCRYPTION_INFO
    const void *binaryBase;
    struct load_command *machoCmd;
    const struct mach_header *machoHeader;

    NSString *path = [[NSBundle mainBundle] executablePath];
    NSData *filedata = [NSData dataWithContentsOfFile:path];
    binaryBase = (char *)[filedata bytes];

    machoHeader = (const struct mach_header *) binaryBase;

    if(machoHeader->magic == FAT_CIGAM)
    {
        unsigned int offset = 0;
        struct fat_arch *fatArch = (struct fat_arch *)((struct fat_header *)machoHeader + 1);
        struct fat_header *fatHeader = (struct fat_header *)machoHeader;
        for(uint32_t i = 0; i < ntohl(fatHeader->nfat_arch); i++)
        {
            if(sizeof(int *) == 4 && !(ntohl(fatArch->cputype) & CPU_ARCH_ABI64)) // check 32bit section for 32bit architecture
            {
                offset = ntohl(fatArch->offset);
                break;
            }
            else if(sizeof(int *) == 8 && (ntohl(fatArch->cputype) & CPU_ARCH_ABI64)) // and 64bit section for 64bit architecture
            {
                offset = ntohl(fatArch->offset);
                break;
            }
            fatArch = (struct fat_arch *)((uint8_t *)fatArch + sizeof(struct fat_arch));
        }
        machoHeader = (const struct mach_header *)((uint8_t *)machoHeader + offset);
    }
    if(machoHeader->magic == MH_MAGIC)    // 32bit
    {
        machoCmd = (struct load_command *)((struct mach_header *)machoHeader + 1);
    }
    else if(machoHeader->magic == MH_MAGIC_64)   // 64bit
    {
        machoCmd = (struct load_command *)((struct mach_header_64 *)machoHeader + 1);
    }
    for(uint32_t i=0; i < machoHeader->ncmds && machoCmd != NULL; i++){
        if(machoCmd->cmd == LC_ENCRYPTION_INFO)
        {
            struct encryption_info_command *cryptCmd = (struct encryption_info_command *) machoCmd;
            return cryptCmd->cryptid;
        }
        if(machoCmd->cmd == LC_ENCRYPTION_INFO_64)
        {
            struct encryption_info_command_64 *cryptCmd = (struct encryption_info_command_64 *) machoCmd;
            return cryptCmd->cryptid;
        }
        machoCmd = (struct load_command *)((uint8_t *)machoCmd + machoCmd->cmdsize);
    }
    return FALSE; // couldn't find cryptcmd
}

2. LC_CODE_SIGNATURE

LC_CODE_SIGNATURE is the section that /usr/bin/codesign actually refers when checking validity of the binary. But parsing the section is a little bit more difficult than parsing LC_ENCRYPTION_INFO, because it's undocumented and there are no types like signature_info_command.

LC_CODE_SIGNATURE contains hashes of all of the binary except the section itself, and hashes are adjusted whenever it's re-signed.

I ported the codes of /usr/bin/codesign to parse this section. check here and SecStaticCode::validateExecutable defined in here

CodeSigning.h

#ifndef CodeSigning_h
#define CodeSigning_h

#include <stdio.h>

// codes from https://opensource.apple.com/source/Security/Security-55179.1/libsecurity_codesigning/lib/cscdefs.h

enum {
    CSMAGIC_REQUIREMENT = 0xfade0c00,       /* single Requirement blob */
    CSMAGIC_REQUIREMENTS = 0xfade0c01,      /* Requirements vector (internal requirements) */
    CSMAGIC_CODEDIRECTORY = 0xfade0c02,     /* CodeDirectory blob */
    CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0, /* embedded form of signature data */
    CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc1, /* multi-arch collection of embedded signatures */

    CSSLOT_CODEDIRECTORY = 0,               /* slot index for CodeDirectory */
};
/*
 * Structure of an embedded-signature SuperBlob
 */
typedef struct __BlobIndex {
    uint32_t type;                  /* type of entry */
    uint32_t offset;                /* offset of entry */
} CS_BlobIndex;

typedef struct __SuperBlob {
    uint32_t magic;                 /* magic number */
    uint32_t length;                /* total length of SuperBlob */
    uint32_t count;                 /* number of index entries following */
    CS_BlobIndex index[];           /* (count) entries */
    /* followed by Blobs in no particular order as indicated by offsets in index */
} CS_SuperBlob;


/*
 * C form of a CodeDirectory.
 */
typedef struct __CodeDirectory {
    uint32_t magic;                 /* magic number (CSMAGIC_CODEDIRECTORY) */
    uint32_t length;                /* total length of CodeDirectory blob */
    uint32_t version;               /* compatibility version */
    uint32_t flags;                 /* setup and mode flags */
    uint32_t hashOffset;            /* offset of hash slot element at index zero */
    uint32_t identOffset;           /* offset of identifier string */
    uint32_t nSpecialSlots;         /* number of special hash slots */
    uint32_t nCodeSlots;            /* number of ordinary (code) hash slots */
    uint32_t codeLimit;             /* limit to main image signature range */
    uint8_t hashSize;               /* size of each hash in bytes */
    uint8_t hashType;               /* type of hash (cdHashType* constants) */
    uint8_t spare1;                 /* unused (must be zero) */
    uint8_t pageSize;               /* log2(page size in bytes); 0 => infinite */
    uint32_t spare2;                /* unused (must be zero) */
    /* followed by dynamic content as located by offset fields above */
} CS_CodeDirectory;

static inline const CS_CodeDirectory *findCodeDirectory(const CS_SuperBlob *embedded)
{
    if (embedded && ntohl(embedded->magic) == CSMAGIC_EMBEDDED_SIGNATURE) {
        const CS_BlobIndex *limit = &embedded->index[ntohl(embedded->count)];
        const CS_BlobIndex *p;
        for (p = embedded->index; p < limit; ++p)
            if (ntohl(p->type) == CSSLOT_CODEDIRECTORY) {
                const unsigned char *base = (const unsigned char *)embedded;
                const CS_CodeDirectory *cd = (const CS_CodeDirectory *)(base + ntohl(p->offset));
                if (ntohl(cd->magic) == CSMAGIC_CODEDIRECTORY){
                    return cd;
                }
                else{
                    break;
                }
            }
    }
    // not found
    return NULL;
}

//
unsigned char validateSlot(const void *data, size_t length, size_t slot, const CS_CodeDirectory *codeDirectory);
#endif /* CodeSigning_h */

CodeSigning.c

#include "CodeSigning.h"
#include <stdio.h>
#include <string.h>
#import <CommonCrypto/CommonDigest.h>
unsigned char validateSlot(const void *data, size_t length, size_t slot, const CS_CodeDirectory *codeDirectory)
{
    uint8_t digest[CC_SHA1_DIGEST_LENGTH + 1] = {0, };
    CC_SHA1(data, (CC_LONG)length, digest);
    return (memcmp(digest, (void *)((char *)codeDirectory + ntohl(codeDirectory->hashOffset) + 20*slot), 20) == 0);
}

parsing the section

void checkCodeSignature(void *binaryContent){
    struct load_command *machoCmd;
    const struct mach_header *machoHeader;

    machoHeader = (const struct mach_header *) binaryContent;
    if(machoHeader->magic == FAT_CIGAM){
        unsigned int offset = 0;
        struct fat_arch *fatArch = (struct fat_arch *)((struct fat_header *)machoHeader + 1);
        struct fat_header *fatHeader = (struct fat_header *)machoHeader;
        for(uint32_t i = 0; i < ntohl(fatHeader->nfat_arch); i++)
        {
            if(sizeof(int *) == 4 && !(ntohl(fatArch->cputype) & CPU_ARCH_ABI64)) // check 32bit section for 32bit architecture
            {
                offset = ntohl(fatArch->offset);
                break;
            }
            else if(sizeof(int *) == 8 && (ntohl(fatArch->cputype) & CPU_ARCH_ABI64)) // and 64bit section for 64bit architecture
            {
                offset = ntohl(fatArch->offset);
                break;
            }
            fatArch = (struct fat_arch *)((uint8_t *)fatArch + sizeof(struct fat_arch));
        }
        machoHeader = (const struct mach_header *)((uint8_t *)machoHeader + offset);
    }
    if(machoHeader->magic == MH_MAGIC)    // 32bit
    {
        machoCmd = (struct load_command *)((struct mach_header *)machoHeader + 1);
    }
    else if(machoHeader->magic == MH_MAGIC_64)   // 64bit
    {
        machoCmd = (struct load_command *)((struct mach_header_64 *)machoHeader + 1);
    }
    for(uint32_t i=0; i < machoHeader->ncmds && machoCmd != NULL; i++){
        if(machoCmd->cmd == LC_CODE_SIGNATURE)
        {
            struct linkedit_data_command *codeSigCmd = (struct linkedit_data_command *) machoCmd;

            const CS_SuperBlob *codeEmbedded = (const CS_SuperBlob *)&((char *)machoHeader)[codeSigCmd->dataoff];
            void *binaryBase = (void *)machoHeader;

            const CS_BlobIndex curIndex = codeEmbedded->index[0];
            const CS_CodeDirectory *codeDirectory = (const CS_CodeDirectory *)((char *)codeEmbedded + ntohl(curIndex.offset));

            size_t pageSize = codeDirectory->pageSize ? (1 << codeDirectory->pageSize) : 0;
            size_t remaining = ntohl(codeDirectory->codeLimit);
            size_t processed = 0;
            for(size_t slot = 0; slot < ntohl(codeDirectory->nCodeSlots); ++slot){
                size_t size = MIN(remaining, pageSize);
                if(!validateSlot(binaryBase+processed, size, slot, codeDirectory)){
                    return;
                }
                processed += size;
                remaining -= size;
            }
            printf("[*] Code is valid!");
        }
    }
    machoCmd = (struct load_command *)((uint8_t *)machoCmd + machoCmd->cmdsize);
}
like image 193
cd80 Avatar answered Sep 30 '22 06:09

cd80