commit 54f1f3eb388b7d0ae0fe6c77970f2f21262bf412 Author: nganhkhoa Date: Wed May 31 16:17:03 2023 +0700 add old go tooling diff --git a/macho-go/.gitignore b/macho-go/.gitignore new file mode 100644 index 0000000..f2d9c32 --- /dev/null +++ b/macho-go/.gitignore @@ -0,0 +1,178 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/python,vim,vscode +# Edit at https://www.toptal.com/developers/gitignore?templates=python,vim,vscode + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# profiling data +.prof + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### vscode ### +.vscode/ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# End of https://www.toptal.com/developers/gitignore/api/python,vim,vscode + +bin/ +pkg/protomodel diff --git a/macho-go/Makefile b/macho-go/Makefile new file mode 100644 index 0000000..e08d340 --- /dev/null +++ b/macho-go/Makefile @@ -0,0 +1,11 @@ +build: + protoc -I=proto --go_out=. proto/*.proto + go build -o bin/ios-wrapper ./cmd/ios-wrapper + go build -o bin/extract-section ./cmd/extract-section + +build-linux: + protoc -I=proto --go_out=. proto/*.proto + GOOS=linux GOARCH=amd64 go build -o bin/ios-wrapper ./cmd/ios-wrapper + +module: + go get -u google.golang.org/protobuf/cmd/protoc-gen-go diff --git a/macho-go/azure-pipelines.yml b/macho-go/azure-pipelines.yml new file mode 100644 index 0000000..c8162bd --- /dev/null +++ b/macho-go/azure-pipelines.yml @@ -0,0 +1,25 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +trigger: +- master + +pool: + vmImage: ubuntu-latest + +steps: +- task: GoTool@0 + inputs: + version: '1.13' +- bash: | + wget "https://github.com/protocolbuffers/protobuf/releases/download/v3.13.0/protoc-3.13.0-linux-x86_64.zip" + unzip "protoc-3.13.0-linux-x86_64.zip" -d $HOME/protobuf + export PATH=$PATH:$HOME/protobuf/bin + export GOPATH=$HOME/go + export GOBIN=$GOPATH/bin + export PATH=$PATH:$GOPATH:$GOBIN + make install + make + displayName: 'Build' diff --git a/macho-go/cloud-shield.py b/macho-go/cloud-shield.py new file mode 100644 index 0000000..9462ca7 --- /dev/null +++ b/macho-go/cloud-shield.py @@ -0,0 +1,155 @@ +import os +import shutil +import plistlib +import subprocess + +import tempfile +import stat + +joinp = os.path.join + +wrapper_folder = os.path.expanduser("~/bcell/ios-wrapper-stable/bin") +sdk_root_folder = os.path.expanduser("~/bcell/ios-sdk") + +class Worker: + @property + def __backup_folder(self): + # return tempfile.TemporaryDirectory(prefix="ios-wrapper") + return "/tmp/ios-wrapper-backup" + + @property + def ios_wrapper(self): + return joinp(wrapper_folder, "ios-wrapper") + + @property + def bcell_framework(self): + if self.encryption_required: + return joinp(sdk_root_folder, "release", "bazel-bin", + "bcell", "bcell.framework_archive-root", "bcell.framework/") + else: + return joinp(sdk_root_folder, "bazel-bin", + "bcell", "bcell.framework_archive-root", "bcell.framework/") + + @property + def encryptor(self): + return joinp(sdk_root_folder, "release", "generated", "encryptor/encryptor.py") + + @property + def binary_modified(self): + return joinp(self.__backup_folder, self.binary_name + "_modified") + + @property + def bcell_raw(self): + return joinp(self.__backup_folder, "bcell_raw.dat") + + @property + def bcell_unencrypted(self): + return joinp(self.__backup_folder, "bcell_unencrypted.dat") + + @property + def bcell_encrypted(self): + return joinp(self.__backup_folder, "bcell_encrypted.dat") + + def __load_binary(self): + self.binary = self.infile + self.binary_name = os.path.basename(self.infile) + + def __init__(self, **kwargs): + for key in kwargs: + setattr(self, key, kwargs[key]) + + # prepare backup folder + shutil.rmtree(self.__backup_folder, ignore_errors=True) + os.mkdir(self.__backup_folder) + + # load file based on file type + self.__load_binary() + if self.binary_name is None: + raise Exception("Cannot determine binary name") + + def wrap(self): + if self.encryption_required: + subprocess.run([self.ios_wrapper, "wrap", + "-c", self.config, + "-s", "-b", self.bcell_raw, + "-o", self.binary_modified, + self.binary]) + else: + subprocess.run([self.ios_wrapper, "wrap", + "-c", self.config, + "-b", self.bcell_raw, + "-o", self.binary_modified, + self.binary]) + return + + print("ENCRYPT BCELL FILE") + subprocess.run(["python3", self.encryptor, + "--infile", self.bcell_unencrypted, + "--outfile", self.bcell_encrypted]) + + def finish(self): + bcell_out = os.path.dirname(self.binary) + os.makedirs(joinp(bcell_out, "Frameworks"), exist_ok=True) + + bcell_file = (self.bcell_raw, self.bcell_encrypted)[self.encryption_required] + shutil.copy(bcell_file, joinp(bcell_out, "bcell.dat")) + shutil.copy(self.binary_modified, joinp(bcell_out, self.binary_name)) + + shutil.rmtree(joinp(bcell_out, "Frameworks", "bcell.framework"), ignore_errors=True) + shutil.copytree(self.bcell_framework, joinp(bcell_out, "Frameworks", "bcell.framework")) + + st = os.stat(joinp(bcell_out, self.binary_name)) + os.chmod(joinp(bcell_out, self.binary_name), st.st_mode | stat.S_IEXEC) + + self.info(joinp(bcell_out, self.binary_name)) + + def cleanup(self): + shutil.rmtree(self.__backup_folder, ignore_errors=True) + + def info(self, file): + subprocess.run([self.ios_wrapper, "info", file]) + + +def check_files(file_path): + if not os.path.isfile(file_path): + raise Exception("The file %s does not exist" % file_path) + return + +def is_encryption_required(): + return os.environ.get("REQUIRE_ENCRYPTION", None) is not None + +if __name__ == "__main__": + import argparse + description = """ + A test tool made for use with Xcode, paths to wrapper and bcell.framework + are hard-coded in this script, modify them if needed. + + Pass in Xcode Build Phase a Run script: + + ``` + python3 /path/to/cloud-shield.py \ + -c ${PROJECT_DIR}/bcell_config.json \ + -i ${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH} + ``` + """ + parser = argparse.ArgumentParser(description=description, + formatter_class=argparse.RawTextHelpFormatter) + + parser.add_argument("-i", "--infile", metavar="input-file", + required=True, help="Input binary built by Xcode Run script") + parser.add_argument("-c", "--config", metavar="config-file", + required=True, help="Config JSON file") + + options = parser.parse_args() + + check_files(options.infile) + check_files(options.config) + + encryption_required = is_encryption_required() + + worker = Worker(encryption_required=encryption_required, + infile=options.infile, + config=options.config) + worker.wrap() + worker.finish() + worker.cleanup() diff --git a/macho-go/cmd/extract-section/main.go b/macho-go/cmd/extract-section/main.go new file mode 100644 index 0000000..6f29aeb --- /dev/null +++ b/macho-go/cmd/extract-section/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "ios-wrapper/pkg/ios/macho" + "github.com/alecthomas/kong" + + "os" + "fmt" + "bytes" +) + +type Argument struct { + One string `arg short:"o" required help:"" type:"existingfile"` + Two string `arg short:"t" required help:"" type:"existingfile"` +} + +func main() { + var cli Argument + kong.Parse(&cli) + + compare(cli.One, cli.Two) +} + +func compare(one string, two string) { + f1, _ := os.OpenFile(one, os.O_RDONLY, 0644) + f2, _ := os.OpenFile(two, os.O_RDONLY, 0644) + + var mc1 macho.MachoContext + var mc2 macho.MachoContext + mc1.ParseFile(f1, 0) + mc2.ParseFile(f2, 0) + + s1 := mc1.FindSection("__text") + s2 := mc2.FindSection("__text") + + if (s1.Size() == s2.Size()) { + fmt.Println("Size is equal") + } else { + fmt.Printf("%x <> %x\n", s1.Size(), s2.Size()) + } + + data1 := mc1.Cut(uint64(s1.Offset()), s1.Size()) + data2 := mc1.Cut(uint64(s2.Offset()), s2.Size()) + if (bytes.Compare(data1, data2) == 0) { + fmt.Println("Data is equal") + } +} diff --git a/macho-go/cmd/ios-wrapper/main.go b/macho-go/cmd/ios-wrapper/main.go new file mode 100644 index 0000000..2eb6376 --- /dev/null +++ b/macho-go/cmd/ios-wrapper/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "ios-wrapper/internal/wrapper" +) + +func main() { + wrapper.Cli() +} diff --git a/macho-go/doc/.gitignore b/macho-go/doc/.gitignore new file mode 100644 index 0000000..07e1f67 --- /dev/null +++ b/macho-go/doc/.gitignore @@ -0,0 +1,4 @@ +compiled/ + +*.html +!template.html diff --git a/macho-go/doc/binding.html.pm b/macho-go/doc/binding.html.pm new file mode 100644 index 0000000..0b2c48f --- /dev/null +++ b/macho-go/doc/binding.html.pm @@ -0,0 +1,147 @@ +#lang pollen + +◊(define (sidenote label . xs) + `(splice-me + (label ((for ,label) (class "margin-toggle sidenote-number"))) + (input ((id ,label) (class "margin-toggle")(type "checkbox"))) + (span ((class "sidenote")) ,@xs))) + + +◊h1{Bind and protect Mach-O Binary} +◊section{ +◊p{ +Our product protects the iOS application. To do that, we modifies the original application such that without our library, the binary fails to start. We also detect jailbroken device, frida hooking device and crash the app if detected. +} + +◊p{ +We write ourself the wrapper, which modifies the original binary. The wrapper also create a ◊code{LC_LOAD_DYLIB} command to load our library on runtime. +} +} + +◊h2{Building the library} +◊section{ +◊p{ +We build the library as a dynamic library and distribute as a framework, ◊code{bcell.framework/bcell}. +} +} + +◊h3{Run code before the binary} +◊section{ +◊p{ +We ultilize the load order of ◊code{dyld} to load our library and run our library code without requiring the user to call our library. We put our ◊code{main} function inside the ◊code{S_MOD_INIT_FUNC_POINTERS} section for ◊code{dyld} to call when our library is loaded. We annotate the function with ◊code{__attribute__((constructor))} to achive the effect. +} + +◊pre{ +void __attribute__((constructor)) main() +} + +◊p{ +Then the application must tell ◊code{dyld} to load our library. We add a load command ◊code{LC_LOAD_DYLIB} with the path ◊code{@rpath/bcell.framework/bcell}. The framework folder is copied into Frameworks folder of the application. +} +} + +◊h3{Remove unecessary information} +◊section{ + +◊h4{LC_FUNCTION_STARTS and LC_DATA_IN_CODE} +◊p{ +} + +◊h4{Classic linker symbols data} +◊p{ +} +} + +◊h3{Erasing initialization pointers} +◊section{ +◊p{ +Initialization pointers are pointers to functions, these functions are called by ◊code{dyld} before ◊code{__start}. These pointers are located inside section marked ◊code{S_MOD_INIT_FUNC_POINTERS}. Conveniently this section can be modified as writable at runtime. Because ASLR ◊sidenote["aslr"]{images loaded in memory with a random offset} adds a small offset from the prefered image address on load, so hardcoded pointers are misplaced. ◊code{dyld} takes the concrete value at this place and call the function. To make the value points correctly into the function, ◊code{dyld} add the pointers with ASLR offset before dispatching the function ◊sidenote["aslr dyld"]{One may ask "why don't ◊code{dyld} get the value and add the ASLR offset later". We do not know, but the current design of ◊code{dyld} is as presented}. +} + +◊p{ +Because this section is writable at runtime, we can modify the value at runtime. That is, we erase this section data, or replace with random bytes. When our library run, if every is fine, we restore the section with the correct pointers' value (with ASLR offset). Erasing this section is easy, just replace the bytes in the Mach-O binary at the section marked ◊code{S_MOD_INIT_FUNC_POINTERS}. +} +} + +◊h3{Remove lazy bind information} +◊section{ +◊p{ +} +} + +◊h3{Restore on runtime} +◊section{ + +◊h4{Finding the mapped image on memory and ASLR offset} +◊p{ +◊code{dyld} exposes some functions for traversing the list of loaded image. We use this to find the mapped executable image. +} + +◊pre{ +#if __ARM_ARCH_7A__ +typedef struct mach_header macho_header; +typedef struct nlist symbol_t; +#elif __ARM_ARCH_7S__ +typedef struct mach_header macho_header; +typedef struct nlist symbol_t; +#elif __ARM_ARCH_ISA_A64 +typedef struct mach_header_64 macho_header; +typedef struct nlist_64 symbol_t; +#endif + +for (unsigned int i = 0; i < _dyld_image_count(); i++) { + macho_header *mach_hdr = NULL; + mach_hdr = (macho_header *)_dyld_get_image_header(i); + if (mach_hdr == NULL) { + continue; + } + if (mach_hdr->filetype != MH_EXECUTE) { + continue; + } + return mach_hdr; +} +} + +◊p{ +Finding the ASLR offset is easy, we need only the prefered image base. The prefered image base is the vmaddr of a ◊code{LC_SEGMENT} where fileoff == 0 but filesize != 0. +} + +◊pre{ +for _, segment := range segments { + if segment.Fileoff() == 0 && segment.Filesize() != 0 { + return segment.Vmaddr() + } +} +} + +◊p{ +We can save this prefered image base when we modify the binary or we can traverse the load commands from the mapped image header found. The ASLR offset is the subtraction of the mapped image header address and the prefered image base. +} + +◊pre{ +const uint64_t aslr_offset = (uint64_t)mach_hdr - (uint64_t)prefered_image_base; +} + +◊h4{Restoring Initialization pointers} +◊p{ +Restoring these pointers are easy, we need to locate the section on memory, set the section as writable, find the ASLR offset and fill the section with appropriate function pointers. Pointers should be written in the correct order as they originally appear due to functions using data initialized by previous functions. +} + +◊p{ +After finding the mapped image header on memory, we traverse the load commands looking for section marked ◊code{S_MOD_INIT_FUNC_POINTERS}. Next we set the section as writable, with size page aligned. +} + +◊pre{ +vm_protect(mach_task_self(), + (vm_address_t)addr, size, + false, VM_PROT_WRITE | VM_PROT_READ) +} + +◊p{ +After that we write the pointers value with ASLR offset added to the section. And the Initialization pointers are restored. +} + +◊h4{Restoring lazy bind} +◊p{ +} +} diff --git a/macho-go/doc/codesign.html.pm b/macho-go/doc/codesign.html.pm new file mode 100644 index 0000000..879648a --- /dev/null +++ b/macho-go/doc/codesign.html.pm @@ -0,0 +1,23 @@ +#lang pollen + +◊h3{Manually sign the binary} + +◊h3{Resign the binary} +◊section{ +◊p{ +Resign the binary is an important step after modifing a binary. The idevice will reject a falsy signed binary. To install a modified binary, we need to resign the binary. This problem is not easy because Apple checks the application certificate online. We must register a certificate online and use it to sign our application. Registering a certificate online can only be done through Xcode. +} + +◊p{ +Signin with Apple if haven't done so. Use Xcode and create a project, set signer to yourself and sign type to development. Build and install the app on a device. Go to the build folder, collect the embedded.mobileprovision file. Find the identity used to sign the binary and sign with that identity. +} + +◊pre{ +security find-identity -p codesigning -v +# copy the text or hash --> IDENTITY +security cms -D -i embedded.mobileprovision > profile.plist +/usr/libexec/PlistBuddy -x -c 'Print :Entitlements' profile.plist > entitlements.plist +codesign -fs IDENTITY --entitlements entitlements.plist BINARY +} +} + diff --git a/macho-go/doc/index.html.pm b/macho-go/doc/index.html.pm new file mode 100644 index 0000000..0dea832 --- /dev/null +++ b/macho-go/doc/index.html.pm @@ -0,0 +1,349 @@ +#lang pollen + +◊(require txexpr) +◊(require pollen/decode) + +◊(define-meta template "template.html") + +◊(define (sidenote label . xs) + `(splice-me + (label ((for ,label) (class "margin-toggle sidenote-number"))) + (input ((id ,label) (class "margin-toggle")(type "checkbox"))) + (span ((class "sidenote")) ,@xs))) + +◊(define (stupid-sidenote label . xs) + `(stupid-sidenote + (label ((for ,label) (class "margin-toggle sidenote-number"))) + (input ((id ,label) (class "margin-toggle")(type "checkbox"))) + (span ((class "sidenote")) ,@xs))) + +◊(define (splice xs) + (apply append (for/list ([x (in-list xs)]) + (if (and (txexpr? x) (member (get-tag x) '(splice-me))) + (get-elements x) + (list x))))) + +◊(define (root . xs) + (decode `(decoded-root ,@xs) + #:txexpr-elements-proc (compose1 detect-paragraphs splice) + #:exclude-tags '(pre) + )) + +◊h1{Mach-O Binary Bindings} + +◊h2{Mach-O Binary Format} + +◊section{ +◊p{ +Mach-O binary is the executable file format that MacOS, iOS and tvOS use. The file is splited into 3 main parts. The header, the load commands, and the data. While the binary has 3 main parts, we only need to focus on the header and load commands part. The data part are referenced by load commands part. +} +} + +◊h3{Header} + +◊section{ +◊p{ +The header provides summary information to the binary. First comes the file magic then the file's archtecture, number and size of load commands, and a bitmask flag. +} + +◊p{ +The Mach-O binary comes in 2 different set of magic bytes, 32-bit, 64-bit with endianess in either little or big. +} + +◊pre{ +#define MH_MAGIC 0xfeedface /* the mach magic number */ +#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */ +#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */ +#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */ +} + +◊p{ +The file architecture is denoted by two numbers. One specifies the main architecture (intel 386, arm, mips, ...), one specifies the sub-architecture (armv7, arm64e, ...). ◊sidenote["machine-arch-enum"]{◊code{cctools/include/mach/machine.h}} +} + +◊p{ +The Mach-O binary uses load commands to structure the binary information. The header stores the number of load commands and size (in bytes) for parsing and validation. +} + +◊p{ +The header also has a bitmask flag value for others information, e.g PIE. +} +} + +◊h3{Load Commands} + +◊section{ +◊p{ +Load commands are the main part of a Mach-O binary. These load commands are read by ◊code{dyld} linker to load the binary into memory. Each load commands are prepend by a simple header indicate the type and size. The body of each load commands different with each type. +} + +◊p{ +At the time of writing, there are more than 50 load command types. But we only go into a few types that are necessary to understand the Mach-O Binary Binding. +} +} + +◊h4{LC_SEGMENT and LC_SECTION} +◊section{ +◊p{ +◊code{LC_SEGMENT} defines a region for virtual mapping into the memory on Runtime. It defines the virtual address and size for ◊code{dyld} to map. A segment contains a list of sections defined as ◊code{LC_SECTION}. ◊code{LC_SECTION}s are located below the ◊code{LC_SEGMENT}. Each section contains a data pointer to instruct ◊code{dyld} to load bytes into memory. +} + +◊p{ +◊code{LC_SEGMENT} are loaded into memory with default Read-Write-Executable marked by ◊code{initprot}. We can modify the memory region on runtime using ◊code{vm_protect}. However, protection bit cannot exceed ◊code{maxprot}. +} + +◊p{ +◊code{LC_SECTION} may contain special data, such sections are marked by the ◊code{type} field. A section containing C literal strings are marked as ◊code{S_CSTRING_LITERALS}. +} +} + +◊h4{LC_LOAD_DYLIB and LC_RPATH} +◊section{ +◊p{ +The Mach-O binary defines the dynamically linked library using ◊code{LC_LOAD_DYLIB}. When ◊code{dyld} reads this load command, it loads the library into the memory. ◊code{LC_LOAD_DYLIB} contains a path to the linked library, e.g. ◊code{/usr/lib/libSystem.B.dylib}. The path can be absolute ◊code{/absolute/lib}, relative ◊code{../relative/lib} or prefixed. Prefixed path are path prefixed by either ◊code{@executable_path}, ◊code{@loader_path} or ◊code{@rpath}. ◊code{@executable_path} points to the directory where the executable is placed. ◊code{@loader_path} points to ◊code{dyld} parent directory. ◊code{@rpath} are defined using ◊code{LC_RPATH}. +} + +◊p{ +◊code{LC_RPATH} adds to the set of paths to find a library path with ◊code{@rpath}. Often, we see a ◊code{LC_RPATH = @executable_path/Frameworks} defined. This path is commonly used in iOS applications as third-party libraries are placed inside ◊code{Frameworks} folder, and specify ◊code{@rpath} to load libraries are shorter to write (and understand). ◊sidenote["rpath sucks"]{Not yet research into same library name put in different @rpath} +} +} + +◊h4{LC_ID_DYLIB} +◊section{ +◊p{ +This is a special command indicating the path to put the library when compiled. The compilation may fail if the library is placed wrongly. Renaming the library also fail the compilation. ◊sidenote["id dylib"]{Not 100% sure} +} +} + +◊h4{LC_DYLD_INFO or LC_DYLD_INFO_ONLY} +◊section{ +◊p{ +This command appears only once in a Mach-O binary and contains pointers to the encoded list of external (imported) symbols and exported symbols. External symbols reside on third-party library and must be bound by ◊code{dyld}. There are 3 types of external symbols, normal bind, weak bind, and lazy bind. +} + +◊p{ +External symbols are defined using byte-stream. Reading and decode the byte-stream reveals the symbol's name, the library hosting the symbol. + +For normal bind symbols, ◊code{dyld} read and decode the byte-stream when loading the binary to memory and write the symbol's function address to a section marked ◊code{S_NON_LAZY_SYMBOL_POINTERS}. + +For lazy bind symbols, the decoding process is done through PLT ◊sidenote["PLT"]{Procedural Linkage Table}. Each lazy bind symbol have a helper stub calling ◊code{dyld_stub_binder} with a number. The number is an offset to lazy bind symbols byte-stream. When parsed, the symbol's name and the library hosting the symbol is known. With these two information, ◊code{dyld} look up the symbol's name in the export table of the hosting library and return the address to the function. ◊code{dyld} also rewrite the PLT table on success so the next time the same function is called, it will be a direct call. + +◊pre{ + +__LAZY: + ... + dword _stub_write + ... + + +foo: + push argument_of_write + jmp [__LAZY + offset_to_stub_write] + +_stub_write: + push OFFSET_WRITE_IN_BYTE_STREAM + jmp _dyld_stub_binder + +_dyld_stub_binder: + call fastBindLazySymbol + call eax + +# where fastBindLazySymbol resolve the address of the symbol and +# replace the __LAZY _stub_write pointer with the symbol's address. +} +} +} + +◊h4{LC_CODE_SIGNATURE} +◊section{ +◊p{ +The *OS system requires the running binary to be signed. Signed Mach-O binary has a load command ◊code{LC_CODE_SIGNATURE} points to a compressed data containing the vendor information, the key used signing and the certificate. ◊sidenote["codesign"]{Yet to read how to parse this information} +} +} + +◊h4{LC_ENCRYPTION_INFO} +◊section{ +◊p{ +To prevent people copying binaries, Apple introduced PlayFair, a mechanism to prevent digital infringement. With PlayFair, the app is encrypted with a key installed on the chip when downloaded from AppStore. The content is decrypted at runtime using the hardware key. When running the binary in another machine, the app content can't be decrypted and fail to launch. +} + +◊p{ +◊code{LC_ENCRYPTION_INFO} points to the region of the encrypted data and indicate if the content is encrypted or decrypted. It should only be encrypted when distributed through AppStore. In later section, we introduce how one can decrypt the content with a Jailbroken iOS device. +} +} + +◊h4{LC_FUNCTION_STARTS and LC_DATA_IN_CODE} +◊section{ +◊p{ +} +} + +◊h4{LC_SYMTAB and LC_DYSYMTAB} +◊section{ +◊p{ +} +} + +◊h2{Fat Binary Format} +◊section{ +◊p{ +Apple has a binary format for combining different architecture of an application into one, called Fat binary. The binary format is simple, it defines a list of Mach-O binaries. The Mach-O binaries inside are located with an alignment of 2. Infact: +} + +◊pre{ +func GetAlignment(h *macho.Header) uint32 { + switch h.Cputype() { + case CPU_TYPE_ARM, CPU_TYPE_ARM64: + return 0xe // log2(0x4000) + case CPU_TYPE_POWERPC, + CPU_TYPE_POWERPC64, + CPU_TYPE_I386, + CPU_TYPE_X86_64: + return 0xc // log2(0x1000) + default: + return 0xd // log2(0x2000) + } +} +} +} + +◊h2{ipa file} +◊section{ +◊p{ +In iOS applications are distributed using .ipa file format. The .ipa file is a .zip file with a certain structure. A valid ipa file must have a folder ◊code{*.app}. Inside the folder must have two files, ◊code{Info.plist} and a binary. The binary name must conform to the ◊code{Info.plist}'s ◊code{CFBundleExecutable} field. ◊code{Info.plist} file format is (UTF8) XML or binary encoded, both format can be read using python's ◊a['((href "https://docs.python.org/3/library/plistlib.html"))]{plistlib}. +} + +◊h3{Info.plist} +◊p{ +} +} + +◊h2{Jailbreak and Mach-O hacks} + +◊h3{Jailbreak the iOS} +◊section{ +◊p{ +Jailbreak can be done by using tools like ◊a['((href "https://checkra.in/"))]{◊code{checkra1n}}. At the time of writing, Jailbreak can be done easily for devices iPhone5s to iPhone X and support for iOS 12 to 14. This jailbreak is semi-tethered, means that a reboot breaks the jailbreak. +} + +◊p{ +Jailbreak disable some security features by patching the iOS kernel. It enables USB access, re-mounts ◊code{/} as both readable and writable, removes sandbox, etc. Jailbreaking tool often come with some other applications, e.g openssh, Cydia. A device with Cydia installed is Jailbroken. In custom jailbreaking tool, we can omit the installation of Cydia. However, most jailbroken devices are jailbroken using public tooling (like checkra1n), and by default, it comes with Cydia installed. +} + +◊p{ +Semi-tethered jailbreak losts the kernel patch after a reboot, but applications installed when the jailbreak was active remain. In these cases, the device must run the jailbreak again to access jailbreak features. +} + +◊p{ +Jailbreak tools install ◊code{openssh} to provide access through USB connection. ◊code{checkra1n} install and run openssh at port 44. After connecting the device with the USB cable, we proxy the USB connection to ◊code{localhost}, now ◊code{ssh} to the device is possible. To proxy the USB connection, we use ◊a['((href "https://libimobiledevice.org/"))]{◊code{iproxy}} ◊sidenote["iproxy"]{◊code{brew install libimobiledevice}}. +} + +◊pre{ +iproxy 4444 44 +ssh root@localhost -p 4444 +# default root password: alpine +} + +◊p{ +In old books and article the path to installed applications are wrong. Use this path: ◊code{/var/containers/Bundle/Application/ID/}, where ID is a long hash for each application.◊sidenote["app path"]{old path: ◊code{/var/mobile/Applications/ID/}} +} +} + +◊h3{Decrypt the binary} +◊section{ +◊p{ +As mentioned in ◊code{LC_ENCRYPTION_INFO}, binaries downloaded from AppStore is encrypted with a hardware key. It is said that the key can't be retrieved, and the only to decrypt the binary is to let it run and dump the binary after decryption process. Here we demonstrate the use of ◊a['((href "https://github.com/stefanesser/dumpdecrypted"))]{dumpdecrypted}. +} + +◊p{ +Compile the library, add ◊code{-miphoneos-version-min=} as iOS version of the idevice. Sign the output binary. Use scp to copy the binary to the idevice. ssh into the idevice and run the application with the library. +} + +◊pre{ +set DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib +/var/containers/Bundle/Application/ID/APPNAME.app/BINARY +} + +◊p{ +A binary BINARY.decrypted will be written out with contents decrypted. This method is the most simple way to decrypt an application but still cannot install to another machine (even after resigning). This method only decrypt the main application, however the third-party libraries are also encrypted and not decrypted. We can overcome this with ◊code{frida-ios-dump}. This tool uses Frida, which will be discussed in later section. +} + +◊p{ +◊code{dyld} maps the encrypted section using ◊code{mremap_encrypted} ◊sidenote["mremap_encrypted"]{◊code{dyld/dyld3/Loading.cpp}}. Another tool using this method to decrypt is ◊a['((href "https://github.com/JohnCoates/flexdecrypt"))]{flexdecrypt}. +} + +◊pre{ +#if (__arm__ || __arm64__) && !TARGET_OS_SIMULATOR + // tell kernel about fairplay encrypted regions + uint32_t fpTextOffset; + uint32_t fpSize; + if ( image->isFairPlayEncrypted(fpTextOffset, fpSize) ) { + const mach_header* mh = (mach_header*)loadAddress; + int result = ::mremap_encrypted(((uint8_t*)mh) + fpTextOffset, fpSize, 1, mh->cputype, mh->cpusubtype); + if ( result != 0 ) { + diag.error("could not register fairplay decryption, mremap_encrypted() => %d", result); + ::vm_deallocate(mach_task_self(), loadAddress, (vm_size_t)totalVMSize); + return; + } + } +#endif +} +} + +◊h3{Frida hooking} +◊section{ +◊p{ +Frida is a hooking framework. Using Frida, one can easily intercept the running application. Frida supports both jailbroken and non-jailbroken devices. Frida has two components, the server and the client. The server is installed in our machine, the client is attached to the application. +} + +◊pre{ +python3 -m pip install frida-tools +} + +◊p{ +The Frida ◊a['((href "https://frida.re/docs/ios/"))]{documentation} gives some instructions for using Frida with jailbroken or non-jailbroken devices. In our experience, we setup Frida manually, for both jailbroken and non-jailbroken devices, as stated below. +} + +◊h4{Setup for jailbroken devices} + +◊p{ +Jailbroken devices with Cydia can easily install Frida by adding Frida from Cydia store. The installed Frida should have its ABI compatible with the device. Frida can also be installed by download, sign, copy (scp) onto the device and run the Frida client. +} + +◊h4{Setup for non-jailbroken devices} +◊p{ +Non-jailbroken devices cannot install Frida client. Frida server uses Frida client to inject Frida Gadget into the application on jailbroken devices. For non-jailbroken devices, we must setup Frida Gadget directly inside the application. We can do this by using ◊a['((href "https://github.com/Tyilo/insert_dylib"))]{insert_dylib}. Build insert_dylib, download FridaGadget.dylib. +} + +For an application having the ipa file (.app folder). + +◊pre{ +insert_dylib --strip-codesig --inplace '@executable_path/Frameworks/FridaGadget.dylib' BINARY +} + +Copy FridaGadget.dylib into Frameworks folder. Resign BINARY and sign FridaGadget.dylib. Use Xcode to install the application. Frida can connect and intercept the application after the application started. +} + +◊h4{Using Frida} + +◊p{ +Frida intercepts the application and provides a scripting utility. We can use the scripting engine to inspect memory, change registers' value. The iOS ecosystem often use Objective-C (Swift also use Objective-C at low-level) and Frida also supports conversion from Objective-C data type to raw data type, e.g. NSString to raw string. +} + +◊h3{Others tools} +◊section{ +◊p{ +There are many other tools to pentest/reverse/intercept/... iOS applications. We haven't tested these tools but they are worth mentioning. +} + +◊ul{ +◊li{◊a['((href "http://www.cydiasubstrate.com/"))]{CydiaSubstrate} - Hook framework} +◊li{◊a['((href "https://github.com/akemin-dayo/AppSync"))]{AppSync} - Utilities for jailbreak devices} +◊li{◊a['((href "https://github.com/jmpews/Dobby"))]{Dobby} - Hook framework (stale)} +◊li{◊a['((href "https://github.com/KJCracks/Clutch"))]{Clutch} - Dump decrypted} +◊li{◊a['((href "https://github.com/nygard/class-dump"))]{class-dump} - Dump Objective-C class metadata} +◊li{◊a['((href "https://github.com/asLody/whale"))]{whale} - Hook framework} +◊li{◊a['((href "https://github.com/alexzielenski/optool"))]{optool} - Mach-O binary utilities} +◊li{◊a['((href "https://github.com/steakknife/unsign"))]{unsign} - unsign Mach-O binary} +} +} diff --git a/macho-go/doc/template.html b/macho-go/doc/template.html new file mode 100644 index 0000000..0b7c8f1 --- /dev/null +++ b/macho-go/doc/template.html @@ -0,0 +1,10 @@ + + + + ◊select['h1 doc] + + + +
+◊(->html doc) +
diff --git a/macho-go/doc/tufte.css b/macho-go/doc/tufte.css new file mode 100644 index 0000000..2ad645b --- /dev/null +++ b/macho-go/doc/tufte.css @@ -0,0 +1,469 @@ +@charset "UTF-8"; + +/* Import ET Book styles + adapted from https://github.com/edwardtufte/et-book/blob/gh-pages/et-book.css */ + +@font-face { + font-family: "et-book"; + src: url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.eot"); + src: url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.eot?#iefix") format("embedded-opentype"), url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.woff") format("woff"), url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.ttf") format("truetype"), url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.svg#etbookromanosf") format("svg"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "et-book"; + src: url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.eot"); + src: url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.eot?#iefix") format("embedded-opentype"), url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.woff") format("woff"), url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.ttf") format("truetype"), url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.svg#etbookromanosf") format("svg"); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "et-book"; + src: url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.eot"); + src: url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.eot?#iefix") format("embedded-opentype"), url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.woff") format("woff"), url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.ttf") format("truetype"), url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.svg#etbookromanosf") format("svg"); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "et-book-roman-old-style"; + src: url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.eot"); + src: url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.eot?#iefix") format("embedded-opentype"), url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.woff") format("woff"), url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.ttf") format("truetype"), url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.svg#etbookromanosf") format("svg"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +/* Tufte CSS styles */ +html { + font-size: 15px; +} + +body { + width: 87.5%; + margin-left: auto; + margin-right: auto; + padding-left: 12.5%; + font-family: et-book, Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif; + background-color: #fffff8; + color: #111; + max-width: 1400px; + counter-reset: sidenote-counter; +} + +h1 { + font-weight: 400; + margin-top: 4rem; + margin-bottom: 1.5rem; + font-size: 3.2rem; + line-height: 1; +} + +h2 { + font-style: italic; + font-weight: 400; + margin-top: 2.1rem; + margin-bottom: 1.4rem; + font-size: 2.2rem; + line-height: 1; +} + +h3 { + font-style: italic; + font-weight: 400; + font-size: 1.7rem; + margin-top: 2rem; + margin-bottom: 1.4rem; + line-height: 1; +} + +hr { + display: block; + height: 1px; + width: 55%; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +p.subtitle { + font-style: italic; + margin-top: 1rem; + margin-bottom: 1rem; + font-size: 1.8rem; + display: block; + line-height: 1; +} + +.numeral { + font-family: et-book-roman-old-style; +} + +.danger { + color: red; +} + +article { + padding: 5rem 0rem; +} + +section { + padding-top: 1rem; + padding-bottom: 1rem; +} + +p, +ol, +ul { + font-size: 1.4rem; + line-height: 2rem; +} + +p { + margin-top: 1.4rem; + margin-bottom: 1.4rem; + padding-right: 0; + vertical-align: baseline; +} + +/* Chapter Epigraphs */ +div.epigraph { + margin: 5em 0; +} + +div.epigraph > blockquote { + margin-top: 3em; + margin-bottom: 3em; +} + +div.epigraph > blockquote, +div.epigraph > blockquote > p { + font-style: italic; +} + +div.epigraph > blockquote > footer { + font-style: normal; +} + +div.epigraph > blockquote > footer > cite { + font-style: italic; +} +/* end chapter epigraphs styles */ + +blockquote { + font-size: 1.4rem; +} + +blockquote p { + width: 55%; + margin-right: 40px; +} + +blockquote footer { + width: 55%; + font-size: 1.1rem; + text-align: right; +} + +section > p, +section > footer, +section > table { + width: 55%; +} + +/* 50 + 5 == 55, to be the same width as paragraph */ +section > ol, +section > ul { + width: 50%; + -webkit-padding-start: 5%; +} + +li:not(:first-child) { + margin-top: 0.25rem; +} + +figure { + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + max-width: 55%; + -webkit-margin-start: 0; + -webkit-margin-end: 0; + margin: 0 0 3em 0; +} + +figcaption { + float: right; + clear: right; + margin-top: 0; + margin-bottom: 0; + font-size: 1.1rem; + line-height: 1.6; + vertical-align: baseline; + position: relative; + max-width: 40%; +} + +figure.fullwidth figcaption { + margin-right: 24%; +} + +/* Links: replicate underline that clears descenders */ +a:link, +a:visited { + color: inherit; +} + +.no-tufte-underline:link { + background: unset; + text-shadow: unset; +} + +a:link, .tufte-underline, .hover-tufte-underline:hover { + text-decoration: none; + background: -webkit-linear-gradient(#fffff8, #fffff8), -webkit-linear-gradient(#fffff8, #fffff8), -webkit-linear-gradient(currentColor, currentColor); + background: linear-gradient(#fffff8, #fffff8), linear-gradient(#fffff8, #fffff8), linear-gradient(currentColor, currentColor); + -webkit-background-size: 0.05em 1px, 0.05em 1px, 1px 1px; + -moz-background-size: 0.05em 1px, 0.05em 1px, 1px 1px; + background-size: 0.05em 1px, 0.05em 1px, 1px 1px; + background-repeat: no-repeat, no-repeat, repeat-x; + text-shadow: 0.03em 0 #fffff8, -0.03em 0 #fffff8, 0 0.03em #fffff8, 0 -0.03em #fffff8, 0.06em 0 #fffff8, -0.06em 0 #fffff8, 0.09em 0 #fffff8, -0.09em 0 #fffff8, 0.12em 0 #fffff8, -0.12em 0 #fffff8, 0.15em 0 #fffff8, -0.15em 0 #fffff8; + background-position: 0% 93%, 100% 93%, 0% 93%; +} + +@media screen and (-webkit-min-device-pixel-ratio: 0) { + a:link, .tufte-underline, .hover-tufte-underline:hover { + background-position-y: 87%, 87%, 87%; + } +} + +a:link::selection, +a:link::-moz-selection { + text-shadow: 0.03em 0 #b4d5fe, -0.03em 0 #b4d5fe, 0 0.03em #b4d5fe, 0 -0.03em #b4d5fe, 0.06em 0 #b4d5fe, -0.06em 0 #b4d5fe, 0.09em 0 #b4d5fe, -0.09em 0 #b4d5fe, 0.12em 0 #b4d5fe, -0.12em 0 #b4d5fe, 0.15em 0 #b4d5fe, -0.15em 0 #b4d5fe; + background: #b4d5fe; +} + +/* Sidenotes, margin notes, figures, captions */ +img { + max-width: 100%; +} + +.sidenote, +.marginnote { + float: right; + clear: right; + margin-right: -60%; + width: 50%; + margin-top: 0; + margin-bottom: 0; + font-size: 1.1rem; + line-height: 1.3; + vertical-align: baseline; + position: relative; +} + +.sidenote-number { + counter-increment: sidenote-counter; +} + +.sidenote-number:after, +.sidenote:before { + font-family: et-book-roman-old-style; + position: relative; + vertical-align: baseline; +} + +.sidenote-number:after { + content: counter(sidenote-counter); + font-size: 1rem; + top: -0.5rem; + left: 0.1rem; +} + +.sidenote:before { + content: counter(sidenote-counter) " "; + font-size: 1rem; + top: -0.5rem; +} + +blockquote .sidenote, +blockquote .marginnote { + margin-right: -82%; + min-width: 59%; + text-align: left; +} + +div.fullwidth, +table.fullwidth { + width: 100%; +} + +div.table-wrapper { + overflow-x: auto; + font-family: "Trebuchet MS", "Gill Sans", "Gill Sans MT", sans-serif; +} + +.sans { + font-family: "Gill Sans", "Gill Sans MT", Calibri, sans-serif; + letter-spacing: .03em; +} + +code, pre > code { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 1.0rem; + line-height: 1.42; + -webkit-text-size-adjust: 100%; /* Prevent adjustments of font size after orientation changes in iOS. See https://github.com/edwardtufte/tufte-css/issues/81#issuecomment-261953409 */ +} + +.sans > code { + font-size: 1.2rem; +} + +h1 > code, +h2 > code, +h3 > code { + font-size: 0.80em; +} + +.marginnote > code, +.sidenote > code { + font-size: 1rem; +} + +pre > code { + font-size: 0.9rem; + width: 52.5%; + margin-left: 2.5%; + overflow-x: auto; + display: block; +} + +pre.fullwidth > code { + width: 90%; +} + +.fullwidth { + max-width: 90%; + clear:both; +} + +span.newthought { + font-variant: small-caps; + font-size: 1.2em; +} + +input.margin-toggle { + display: none; +} + +label.sidenote-number { + display: inline; +} + +label.margin-toggle:not(.sidenote-number) { + display: none; +} + +.iframe-wrapper { + position: relative; + padding-bottom: 56.25%; /* 16:9 */ + padding-top: 25px; + height: 0; +} + +.iframe-wrapper iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +@media (max-width: 760px) { + body { + width: 84%; + padding-left: 8%; + padding-right: 8%; + } + + hr, + section > p, + section > footer, + section > table { + width: 100%; + } + + pre > code { + width: 97%; + } + + section > ol { + width: 90%; + } + + section > ul { + width: 90%; + } + + figure { + max-width: 90%; + } + + figcaption, + figure.fullwidth figcaption { + margin-right: 0%; + max-width: none; + } + + blockquote { + margin-left: 1.5em; + margin-right: 0em; + } + + blockquote p, + blockquote footer { + width: 100%; + } + + label.margin-toggle:not(.sidenote-number) { + display: inline; + } + + .sidenote, + .marginnote { + display: none; + } + + .margin-toggle:checked + .sidenote, + .margin-toggle:checked + .marginnote { + display: block; + float: left; + left: 1rem; + clear: both; + width: 95%; + margin: 1rem 2.5%; + vertical-align: baseline; + position: relative; + } + + label { + cursor: pointer; + } + + div.table-wrapper, + table { + width: 85%; + } + + img { + width: 100%; + } +} diff --git a/macho-go/go.mod b/macho-go/go.mod new file mode 100644 index 0000000..5105fcf --- /dev/null +++ b/macho-go/go.mod @@ -0,0 +1,12 @@ +module ios-wrapper + +go 1.13 + +require ( + github.com/akamensky/argparse v1.2.2 + github.com/alecthomas/kong v0.2.16 + github.com/golang/protobuf v1.5.0 + github.com/sirupsen/logrus v1.8.0 + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect + google.golang.org/protobuf v1.26.0 +) diff --git a/macho-go/go.sum b/macho-go/go.sum new file mode 100644 index 0000000..81b960a --- /dev/null +++ b/macho-go/go.sum @@ -0,0 +1,87 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/akamensky/argparse v1.2.2 h1:P17T0ZjlUNJuWTPPJ2A5dM1wxarHgHqfYH+AZTo2xQA= +github.com/akamensky/argparse v1.2.2/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= +github.com/alecthomas/kong v0.2.16 h1:F232CiYSn54Tnl1sJGTeHmx4vJDNLVP2b9yCVMOQwHQ= +github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU= +github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/macho-go/internal/wrapper/action/action.go b/macho-go/internal/wrapper/action/action.go new file mode 100644 index 0000000..174fb6c --- /dev/null +++ b/macho-go/internal/wrapper/action/action.go @@ -0,0 +1,33 @@ +package action + +import ( + . "ios-wrapper/internal/wrapper/ofile" +) + +// Actions on MachoFile +type Action interface { + withMacho(mf *MachoFile) error + withFat(ff *FatFile) error + // TODO: error context +} + +func RunAction(action Action, ofile OFile) error { + switch ofile.(type) { + case *MachoFile: + return action.withMacho(ofile.(*MachoFile)) + case *FatFile: + return action.withFat(ofile.(*FatFile)) + } + // not likely + return nil +} + +func defaultWithFat(action Action, ff *FatFile) error { + for _, macho := range ff.Machos() { + err := action.withMacho(macho) + if err != nil { + return err + } + } + return nil +} diff --git a/macho-go/internal/wrapper/action/add_dylib.go b/macho-go/internal/wrapper/action/add_dylib.go new file mode 100644 index 0000000..a9f9ee4 --- /dev/null +++ b/macho-go/internal/wrapper/action/add_dylib.go @@ -0,0 +1,31 @@ +package action + +import ( + log "github.com/sirupsen/logrus" + + . "ios-wrapper/internal/wrapper/ofile" +) + +type addDylib struct { + dylib_to_add []string +} + +func (action *addDylib) withMacho(mf *MachoFile) error { + for _, dylib := range action.dylib_to_add { + mf.Context().AddDylib(dylib) + log.WithFields(log.Fields{ + "dylib": dylib, + }).Info("Add Load Dylib Command") + } + return nil +} + +func (action *addDylib) withFat(ff *FatFile) error { + return defaultWithFat(action, ff) +} + +func NewAddDylibAction(dylib_to_add []string) *addDylib { + return &addDylib{ + dylib_to_add, + } +} diff --git a/macho-go/internal/wrapper/action/add_rpath.go b/macho-go/internal/wrapper/action/add_rpath.go new file mode 100644 index 0000000..5f6ab40 --- /dev/null +++ b/macho-go/internal/wrapper/action/add_rpath.go @@ -0,0 +1,31 @@ +package action + +import ( + log "github.com/sirupsen/logrus" + + . "ios-wrapper/internal/wrapper/ofile" +) + +type addRpath struct { + rpath []string +} + +func (action *addRpath) withMacho(mf *MachoFile) error { + for _, rpath := range action.rpath { + mf.Context().AddRPath(rpath) + log.WithFields(log.Fields{ + "rpath": rpath, + }).Info("Add Rpath Command") + } + return nil +} + +func (action *addRpath) withFat(ff *FatFile) error { + return defaultWithFat(action, ff) +} + +func NewAddRpathAction(rpath []string) *addRpath { + return &addRpath{ + rpath, + } +} diff --git a/macho-go/internal/wrapper/action/remove_classic_symbols.go b/macho-go/internal/wrapper/action/remove_classic_symbols.go new file mode 100644 index 0000000..747397e --- /dev/null +++ b/macho-go/internal/wrapper/action/remove_classic_symbols.go @@ -0,0 +1,20 @@ +package action + +import ( + . "ios-wrapper/internal/wrapper/ofile" +) + +type removeClassicSymbol struct{} + +func (action *removeClassicSymbol) withMacho(mf *MachoFile) error { + mf.Context().RemoveClassicSymbol() + return nil +} + +func (action *removeClassicSymbol) withFat(ff *FatFile) error { + return defaultWithFat(action, ff) +} + +func NewRemoveClassicSymbolAction() *removeClassicSymbol { + return &removeClassicSymbol{} +} diff --git a/macho-go/internal/wrapper/action/remove_code.go b/macho-go/internal/wrapper/action/remove_code.go new file mode 100644 index 0000000..6a6cacf --- /dev/null +++ b/macho-go/internal/wrapper/action/remove_code.go @@ -0,0 +1,20 @@ +package action + +import ( + . "ios-wrapper/internal/wrapper/ofile" +) + +type removeCodeSignature struct{} + +func (action *removeCodeSignature) withMacho(mf *MachoFile) error { + mf.Context().RemoveCodesign() + return nil +} + +func (action *removeCodeSignature) withFat(ff *FatFile) error { + return defaultWithFat(action, ff) +} + +func NewRemoveCodeSignatureAction() *removeCodeSignature { + return &removeCodeSignature{} +} diff --git a/macho-go/internal/wrapper/action/remove_init_pointer.go b/macho-go/internal/wrapper/action/remove_init_pointer.go new file mode 100644 index 0000000..0862adb --- /dev/null +++ b/macho-go/internal/wrapper/action/remove_init_pointer.go @@ -0,0 +1,20 @@ +package action + +import ( + . "ios-wrapper/internal/wrapper/ofile" +) + +type removeInitPointer struct{} + +func (action *removeInitPointer) withMacho(mf *MachoFile) error { + mf.Context().RemoveInitFunctions() + return nil +} + +func (action *removeInitPointer) withFat(ff *FatFile) error { + return defaultWithFat(action, ff) +} + +func NewRemoveInitPointerAction() *removeInitPointer { + return &removeInitPointer{} +} diff --git a/macho-go/internal/wrapper/action/remove_unncessary_info.go b/macho-go/internal/wrapper/action/remove_unncessary_info.go new file mode 100644 index 0000000..12e3448 --- /dev/null +++ b/macho-go/internal/wrapper/action/remove_unncessary_info.go @@ -0,0 +1,20 @@ +package action + +import ( + . "ios-wrapper/internal/wrapper/ofile" +) + +type removeUnnecessaryInfo struct{} + +func (action *removeUnnecessaryInfo) withMacho(mf *MachoFile) error { + mf.Context().RemoveUnnecessaryInfo() + return nil +} + +func (action *removeUnnecessaryInfo) withFat(ff *FatFile) error { + return defaultWithFat(action, ff) +} + +func NewRemoveUnnecessaryInfoAction() *removeUnnecessaryInfo { + return &removeUnnecessaryInfo{} +} diff --git a/macho-go/internal/wrapper/action/save_bcell_file.go b/macho-go/internal/wrapper/action/save_bcell_file.go new file mode 100644 index 0000000..6a285d4 --- /dev/null +++ b/macho-go/internal/wrapper/action/save_bcell_file.go @@ -0,0 +1,68 @@ +package action + +import ( + "fmt" + "io/ioutil" + + "google.golang.org/protobuf/proto" + + . "ios-wrapper/internal/wrapper/ofile" + "ios-wrapper/pkg/protomodel" +) + +type saveBcellFile struct { + bcellfile string + content *protomodel.BcellFile + signed bool +} + +func (action *saveBcellFile) addMacho(mf *MachoFile) { + action.content.MachoInfos[mf.Context().ArchName()] = mf.Info() +} + +func (action *saveBcellFile) writeFile() error { + outfile := fmt.Sprintf(action.bcellfile) + content, err := proto.Marshal(action.content) + if err != nil { + return err + } + + if action.signed { + blocks := []*protomodel.Block{ + { + Key: 0, + Value: content, + }, + } + signed_data := protomodel.SignedData{ + Blocks: blocks, + } + content, err = proto.Marshal(&signed_data) + } + + return ioutil.WriteFile(outfile, content, 0644) +} + +func (action *saveBcellFile) withMacho(mf *MachoFile) error { + action.addMacho(mf) + return action.writeFile() +} + +func (action *saveBcellFile) withFat(ff *FatFile) error { + for _, macho := range ff.Machos() { + action.addMacho(macho) + } + return action.writeFile() +} + +func NewSaveBcellFileAction(userconfig *protomodel.Config, bcellfile string, signed bool) *saveBcellFile { + content := &protomodel.BcellFile{ + BcellConfig: userconfig, + MachoInfos: make(map[string]*protomodel.MachoInfo), + } + return &saveBcellFile{ + bcellfile, + content, + signed, + } +} diff --git a/macho-go/internal/wrapper/action/save_init_pointer.go b/macho-go/internal/wrapper/action/save_init_pointer.go new file mode 100644 index 0000000..fcd0256 --- /dev/null +++ b/macho-go/internal/wrapper/action/save_init_pointer.go @@ -0,0 +1,34 @@ +package action + +import ( + log "github.com/sirupsen/logrus" + + . "ios-wrapper/internal/wrapper/ofile" + "ios-wrapper/pkg/protomodel" +) + +type saveInitPointer struct{} + +func (action *saveInitPointer) withMacho(mf *MachoFile) error { + init_pointers := []*protomodel.MachoInfo_InitPointer{} + for _, ptr := range mf.Context().InitFunctions() { + init_pointers = append(init_pointers, + &protomodel.MachoInfo_InitPointer{ + Offset: ptr.Offset(), + Value: ptr.Value(), + }) + } + log.WithFields(log.Fields{ + "pointers": init_pointers, + }).Info("Saved Init Pointers") + mf.Info().InitPointers = init_pointers + return nil +} + +func (action *saveInitPointer) withFat(ff *FatFile) error { + return defaultWithFat(action, ff) +} + +func NewSaveInitPointerAction() *saveInitPointer { + return &saveInitPointer{} +} diff --git a/macho-go/internal/wrapper/action/write_file.go b/macho-go/internal/wrapper/action/write_file.go new file mode 100644 index 0000000..8938a82 --- /dev/null +++ b/macho-go/internal/wrapper/action/write_file.go @@ -0,0 +1,32 @@ +package action + +import ( + "io/ioutil" + + . "ios-wrapper/internal/wrapper/ofile" + "ios-wrapper/pkg/ios/fat" +) + +type writeFile struct { + outfile string +} + +func (action *writeFile) withMacho(mf *MachoFile) error { + data, _ := ioutil.ReadFile(mf.TmpFile()) + return ioutil.WriteFile(action.outfile, data, 0644) +} + +func (action *writeFile) withFat(ff *FatFile) error { + var files []string + for _, macho := range ff.Machos() { + files = append(files, macho.TmpFile()) + } + fat.FatJoin(files, action.outfile) + return nil +} + +func NewWriteFileAction(outfile string) *writeFile { + return &writeFile{ + outfile, + } +} diff --git a/macho-go/internal/wrapper/addr2line/addr2line.go b/macho-go/internal/wrapper/addr2line/addr2line.go new file mode 100644 index 0000000..0e4d0eb --- /dev/null +++ b/macho-go/internal/wrapper/addr2line/addr2line.go @@ -0,0 +1,48 @@ +package addr2line + +import ( + "strconv" + + . "ios-wrapper/pkg/ios/macho" +) + +// Try to get Image base of a DWARF binary +// using __mh_execute_header symbol +func TryGetImageBase(mc *MachoContext, symbols []*Symbol) uint64 { + try := mc.ImageBase() + if try != 0 { + return try + } + for _, symbol := range symbols { + if symbol.Name() == "__mh_execute_header" { + return symbol.Address() + } + } + return 0 +} + +// find the symbol name from dysymtab +// the address given is somewhere in the function, +// assuming that the address is sorted, we find the last symbol +// has its address smaller than `tofind` +// I'm not sure this would work always, ;) +func FindSymbol(symbols []*Symbol, tofind uint64) string { + var lastSymbol string = "" + for _, symbol := range symbols { + if symbol.Address() > tofind { + return lastSymbol + } else { + lastSymbol = symbol.Name() + } + } + return "" +} + +func ParseAddressString(s string) (uint64, error) { + s_, err := strconv.ParseInt(s, 0, 64) + v := uint64(s_) + if err != nil { + return 0, err + } + return v, nil +} diff --git a/macho-go/internal/wrapper/addr2line/resolver.go b/macho-go/internal/wrapper/addr2line/resolver.go new file mode 100644 index 0000000..f6c2b95 --- /dev/null +++ b/macho-go/internal/wrapper/addr2line/resolver.go @@ -0,0 +1,81 @@ +package addr2line + +import ( + "errors" + "fmt" + + . "ios-wrapper/pkg/ios/macho" +) + +type Resolver struct { + symbols []*Symbol + debuglineInfo *DebuglineInfo + aslr uint64 + addresses []string +} + +type Resolved struct { + Raw uint64 + Base uint64 + Symbol string + File string + Line uint32 +} + +func (r *Resolved) Valid() bool { + return r.File == "" && r.Line == 0 +} + +func (resolver *Resolver) HasNext() bool { + return len(resolver.addresses) > 0 +} + +func (resolver *Resolver) Next() (*Resolved, error) { + if !resolver.HasNext() { + return nil, errors.New("There is no more address to resolve") + } + + head, tail := resolver.addresses[0], resolver.addresses[1:] + resolver.addresses = tail + + tofind_aslr, err := ParseAddressString(head) + if err != nil { + return nil, fmt.Errorf("Cannot parse hex address (%s)", head) + } + + tofind := tofind_aslr - resolver.aslr + symbol := FindSymbol(resolver.symbols, tofind) + fileLine := resolver.debuglineInfo.Find(tofind) + + if fileLine == nil { + return &Resolved{ + Raw: tofind_aslr, + Base: tofind, + Symbol: symbol, + File: "", + Line: 0, + }, nil + } + + return &Resolved{ + Raw: tofind_aslr, + Base: tofind, + Symbol: symbol, + File: fileLine.File, + Line: fileLine.Line, + }, nil +} + +func NewResolver(mc *MachoContext, loadaddr uint64, addresses []string) *Resolver { + symbols := mc.CollectSymbols() + debuglineInfo := mc.DebugLineInfo() + imagebase := TryGetImageBase(mc, symbols) + aslr := loadaddr - imagebase + + return &Resolver{ + symbols, + debuglineInfo, + aslr, + addresses, + } +} diff --git a/macho-go/internal/wrapper/cli.go b/macho-go/internal/wrapper/cli.go new file mode 100644 index 0000000..c1ad81d --- /dev/null +++ b/macho-go/internal/wrapper/cli.go @@ -0,0 +1,190 @@ +package wrapper + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/alecthomas/kong" + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" + + "ios-wrapper/internal/wrapper/addr2line" + . "ios-wrapper/internal/wrapper/ofile" + "ios-wrapper/pkg/ios/fat" + "ios-wrapper/pkg/protomodel" +) + +const envLogLevel = "LOG_LEVEL" +const defaultLogLevel = log.InfoLevel + +func getLogLevel() log.Level { + levelString, exists := os.LookupEnv(envLogLevel) + if !exists { + return defaultLogLevel + } + + level, err := log.ParseLevel(levelString) + if err != nil { + return defaultLogLevel + } + + return level +} + +func Cli() { + var cli Argument + ctx := kong.Parse(&cli) + command := ctx.Selected().Name + + log.SetLevel(getLogLevel()) + + if command == "info" { + arg := cli.Info + printOFile(arg.OFile) + return + } else if command == "signed-bcell" { + arg := cli.SignedBcell + makeSignedBcell(arg.Out, arg.Bcell) + return + } else if command == "display-bcell" { + arg := cli.DisplayBcell + displayBcell(arg.Bcell) + return + } else if command == "addr2line" { + arg := cli.Addr2Line + resolveAddresses(arg.Dwarf, arg.Load, arg.Addresses) + return + } else if command == "lipo split" { + arg := cli.Lipo.Split + fat.FatSplit(arg.Fat) + return + } else if command == "lipo join" { + arg := cli.Lipo.Join + fat.FatJoin(arg.Macho, arg.Out) + return + } + + var pc ProgramContext + var ofile OFile = nil + + switch command { + case "wrap": + arg := cli.Wrap + ofile = NewOFile(arg.OFile) + pc.remove_codesign = true + pc.strip_init_pointers = true + pc.dylib_to_add = []string{"@rpath/bcell.framework/bcell"} + pc.rpath_to_add = []string{"@executable_path/Frameworks"} + pc.outfile = arg.Out + pc.bcellfile = arg.Bcell + pc.signed = arg.Signed + pc.ReadUserConfig(arg.Config) + + case "vltk": + arg := cli.Vltk + ofile = NewOFile(arg.OFile) + pc.remove_codesign = true + pc.dylib_to_add = []string{"@rpath/GTJetModule.framework/GTJetModule"} + pc.outfile = arg.Out + + case "remove-codesign", "remove-signature": + arg := cli.RemoveCodesign + ofile = NewOFile(arg.OFile) + pc.remove_codesign = true + pc.outfile = arg.Out + + default: + return + } + + pc.Process(ofile) + ofile.Cleanup() +} + +func printOFile(ofile string) { + f, _ := os.OpenFile(ofile, os.O_RDWR, 0644) + printer := InfoPrinterFromFile(f) + printer.Print() +} + +func makeSignedBcell(outfile string, infile string) { + raw_data, err := ioutil.ReadFile(infile) + if err != nil { + log.Panic("Cannot read bcell.dat") + } + data := &protomodel.BcellFile{} + err = proto.Unmarshal(raw_data, data) + if err != nil { + log.Panic("Invalid bcell.dat") + } + + blocks := []*protomodel.Block{ + { + Key: 0, + Value: raw_data, + }, + } + signed_data := protomodel.SignedData{ + Blocks: blocks, + } + signed_data_b, err := proto.Marshal(&signed_data) + err = ioutil.WriteFile(outfile, signed_data_b, 0644) + if err != nil { + log.Panic("Cannot write SignedData to file") + } +} + +func displayBcell(bfile string) { + raw_data, err := ioutil.ReadFile(bfile) + if err != nil { + log.Panic("Invalid Protobuf bcell.dat (1)") + } + data := &protomodel.BcellFile{} + err = proto.Unmarshal(raw_data, data) + if err != nil { + log.Panic("Invalid Protobuf bcell.dat (2)") + } + + fmt.Printf("[+] User Config: %+v\n", data.BcellConfig) + for arch, info := range data.MachoInfos { + fmt.Printf("[+] Arch %s:\n", arch) + fmt.Printf(" | PointerSize : %+v\n", info.PointerSize) + fmt.Printf(" | Image Base : 0x%x\n", info.ImageBase) + fmt.Printf(" | Init Pointers:\n") + for _, init_ptr := range info.InitPointers { + fmt.Printf( + " | offset 0x%x => addr 0x%x\n", + init_ptr.Offset, + init_ptr.Value, + ) + } + } +} + +func resolveAddresses(dwarf string, load string, addresses []string) { + mf, valid_macho := NewOFile(dwarf).(*MachoFile) + if !valid_macho { + log.Error("Input DWARF file is not a valid Macho binary") + return + } + + loadaddr, err := addr2line.ParseAddressString(load) + if err != nil { + log.Error("Load address is not a valid hex number (%s)", load) + return + } + + resolver := addr2line.NewResolver(mf.Context(), loadaddr, addresses) + for ; ; resolver.HasNext() { + resolved, err := resolver.Next() + if err != nil { + fmt.Printf("[?] Error: %s\n", err) + } else if resolved.Valid() { + fmt.Printf("[+] Found 0x%x => 0x%x %s %s:%d\n", + resolved.Raw, resolved.Base, resolved.Symbol, resolved.File, resolved.Line) + } else { + fmt.Printf("[-] Not found 0x%x\n", resolved.Raw) + } + } +} diff --git a/macho-go/internal/wrapper/cli_parser.go b/macho-go/internal/wrapper/cli_parser.go new file mode 100644 index 0000000..54e7ef3 --- /dev/null +++ b/macho-go/internal/wrapper/cli_parser.go @@ -0,0 +1,66 @@ +package wrapper + +type WrapArgument struct { + Config string `short:"c" required help:"User config.json" type:"existingfile"` + Out string `short:"o" required name:"outfile" help:"Modified Mach-O/Fat binary output file" type:"path"` + Bcell string `short:"b" required help:"bcell.dat output file" type:"path"` + Signed bool `short:"s" required help:"Output Protobuf for bcell.dat"` + OFile string `arg help:"Path to Mach-O/Fat binary file" type:"existingfile"` +} + +type VltkArgument struct { + Out string `short:"o" required name:"outfile" help:"Modified Mach-O/Fat binary output file" type:"path"` + OFile string `arg help:"Path to Mach-O/Fat binary file" type:"existingfile"` +} + +type InfoArgument struct { + OFile string `arg help:"Path to Mach-O/Fat binary file" type:"existingfile"` + All bool `short:"a" help:"If print all information"` + Arch string `help:"Only print information in this arch"` +} + +type RemoveCodesignArgument struct { + Out string `short:"o" required name:"outfile" help:"Modified Mach-O/Fat binary output file" type:"path"` + OFile string `arg help:"Path to Mach-O/Fat binary file" type:"existingfile"` +} + +type SignedBcellArgument struct { + Out string `short:"o" required name:"outfile" help:"bcell.dat.signed output file" type:"existingfile"` + Bcell string `arg short:"b" required help:"bcell.dat input file" type:"existingfile"` +} + +type DisplayBcellArgument struct { + Bcell string `arg short:"b" required help:"Protobuf bcell.dat input file" type:"existingfile"` +} + +type Addr2LineArgument struct { + Load string `short:"l" required help:"Load address of binary"` + Dwarf string `short:"d" required help:"Path to Mach-O DWARF binary file" type:"existingfile"` + Addresses []string `arg help:"Addresses to resolve"` +} + +type LipoArgument struct { + Join struct { + Out string `short:"o" required help:"Output Fat binary file" type:"path"` + Macho []string `arg help:"Macho binaries to join" type:"existingfile"` + } `cmd help:"Join Macho binaries into Fat binary"` + Split struct { + Fat string `arg help:"Fat binary to split" type:"existingfile"` + } `cmd help:"Split fat binary into files, named _"` +} + +type PepeArgument struct { + OFile string `arg help:"Path to Mach-O/Fat binary file" type:"existingfile"` +} + +type Argument struct { + Wrap WrapArgument `cmd help:"Modifies Mach-O/Fat binary"` + Vltk VltkArgument `cmd help:"Modifies Mach-O/Fat binary"` + Info InfoArgument `cmd help:"Show Mach-O/Fat binary information"` + RemoveCodesign RemoveCodesignArgument `cmd aliases:"remove-signature" name:"remove-codesign" help:"Remove LC_CODE_SIGNATURE from Mach-O/Fat binary"` + SignedBcell SignedBcellArgument `cmd name:"signed-bcell" help:"Change Protobuf into Protobuf"` + DisplayBcell DisplayBcellArgument `cmd name:"display-bcell" help:"Display Protobuf content"` + Addr2Line Addr2LineArgument `cmd name:"addr2line" help:"Resolve crash address from DWARF file"` + Lipo LipoArgument `cmd help:"split Fat binary or join Mach-O binares"` + Pepe PepeArgument `cmd help:"split Fat binary or join Mach-O binares"` +} diff --git a/macho-go/internal/wrapper/info.go b/macho-go/internal/wrapper/info.go new file mode 100644 index 0000000..571b4a2 --- /dev/null +++ b/macho-go/internal/wrapper/info.go @@ -0,0 +1,110 @@ +package wrapper + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "os" + + . "ios-wrapper/pkg/ios" + . "ios-wrapper/pkg/ios/fat" + . "ios-wrapper/pkg/ios/macho" +) + +type InfoPrinter struct { + contexts []*MachoContext +} + +func (printer *InfoPrinter) Print() { + for _, mc := range printer.contexts { + fmt.Printf("Mach-O arch: %s\n", mc.ArchName()) + + for i, cmd := range mc.Commands() { + fmt.Printf("%d: %s\n", i, cmd.Cmdname()) + } + + fmt.Printf("Image Base: 0x%x\n", mc.ImageBase()) + + init_funcs := mc.InitFunctions() + if len(init_funcs) == 0 { + fmt.Println("No Init functions") + } + for _, fun := range init_funcs { + fmt.Printf("Init functions at offset %s\n", &fun) + } + + + symbols := mc.CollectLazyBindSymbols() + if len(symbols) > 0 { + fmt.Println("Lazy Symbols") + for _, sym := range symbols { + fmt.Printf( + "%s\n\tStub=0x%x Address=0x%x\n\tDylib=%s\n", + sym.Name(), + sym.Stub(), + sym.Address(), + sym.Dylib(), + ) + } + } else { + fmt.Println("No lazy symbols") + } + + fmt.Println("======") + } +} + +func machoPrinter(file *os.File) *InfoPrinter { + var mc MachoContext + err := mc.ParseFile(file, 0) + if err != nil { + return &InfoPrinter{ + contexts: []*MachoContext{}, + } + } else { + return &InfoPrinter{ + contexts: []*MachoContext{&mc}, + } + } +} + +func fatPrinter(file *os.File) *InfoPrinter { + var fat FatContext + fat.ParseFile(file) + + var contexts []*MachoContext + for _, m := range fat.Machos() { + buf := make([]byte, m.Size) + file.Seek(int64(m.Offset), io.SeekStart) + file.Read(buf) + + var mc MachoContext + mc.ParseBuffer(buf) + contexts = append(contexts, &mc) + } + return &InfoPrinter{ + contexts, + } +} + +func InfoPrinterFromFile(file *os.File) *InfoPrinter { + var magic uint32 + magic_buf := []byte{0, 0, 0, 0} + + file.Read(magic_buf) + magic_r := bytes.NewReader(magic_buf) + binary.Read(magic_r, binary.LittleEndian, &magic) + file.Seek(0, io.SeekStart) + + if magic == MagicFat || magic == CigamFat { + return fatPrinter(file) + } + + if magic == Magic32 || magic == Magic64 || magic == Cigam32 || + magic == Cigam64 { + return machoPrinter(file) + } + + return &InfoPrinter{} +} diff --git a/macho-go/internal/wrapper/ofile/fatfile.go b/macho-go/internal/wrapper/ofile/fatfile.go new file mode 100644 index 0000000..b11c921 --- /dev/null +++ b/macho-go/internal/wrapper/ofile/fatfile.go @@ -0,0 +1,58 @@ +package ofile + +import ( + "io/ioutil" + "os" + + log "github.com/sirupsen/logrus" + + "ios-wrapper/pkg/ios/fat" +) + +type FatFile struct { + machos []*MachoFile + files []string + tmp_file string +} + +func (ff *FatFile) Machos() []*MachoFile { + return ff.machos +} + +func (ff *FatFile) Cleanup() { + for _, macho := range ff.machos { + macho.Cleanup() + } + os.Remove(ff.tmp_file) +} + +func NewFatFile(f string) *FatFile { + // create a tmp working file + tmp, _ := ioutil.TempFile("", "bcell_*") + data, _ := ioutil.ReadFile(f) + ioutil.WriteFile(tmp.Name(), data, 0644) + + var machos []*MachoFile + var files []string + splitted_files, err := fat.FatSplit(tmp.Name()) + if err != nil { + log.WithFields(log.Fields{ + "err": err, + "splitted_ok": splitted_files, + }).Panic("Cannot split Fat binary") + } + for _, splitted_file := range splitted_files { + macho := NewMachoFile(splitted_file) + machos = append(machos, macho) + files = append(files, macho.tmp_file) + + // NewMachoFile creates another tmp file, remove this temp splitted file + os.Remove(splitted_file) + } + + return &FatFile{ + machos: machos, + tmp_file: tmp.Name(), + files: files, + } +} diff --git a/macho-go/internal/wrapper/ofile/machofile.go b/macho-go/internal/wrapper/ofile/machofile.go new file mode 100644 index 0000000..edf069f --- /dev/null +++ b/macho-go/internal/wrapper/ofile/machofile.go @@ -0,0 +1,56 @@ +package ofile + +import ( + "io/ioutil" + "os" + + // log "github.com/sirupsen/logrus" + + "ios-wrapper/pkg/ios/macho" + "ios-wrapper/pkg/protomodel" +) + +type MachoFile struct { + mc *macho.MachoContext + tmp_file string + info *protomodel.MachoInfo +} + +func (mf *MachoFile) Context() *macho.MachoContext { + return mf.mc +} + +func (mf *MachoFile) Info() *protomodel.MachoInfo { + return mf.info +} + +func (mf *MachoFile) TmpFile() string { + return mf.tmp_file +} + +func (mf *MachoFile) Cleanup() { + os.Remove(mf.tmp_file) +} + +func NewMachoFile(f string) *MachoFile { + // create a tmp working file + tmp, _ := ioutil.TempFile("", "bcell_*") + data, _ := ioutil.ReadFile(f) + ioutil.WriteFile(tmp.Name(), data, 0644) + + var mc macho.MachoContext + tmp, _ = os.OpenFile(tmp.Name(), os.O_RDWR, 0644) + mc.ParseFile(tmp, 0) + + return &MachoFile{ + mc: &mc, + tmp_file: tmp.Name(), + info: &protomodel.MachoInfo{ + PointerSize: map[bool]protomodel.MachoInfo_PointerSize{ + false: protomodel.MachoInfo_p32, + true: protomodel.MachoInfo_p64, + }[mc.Is64bit()], + ImageBase: mc.ImageBase(), + }, + } +} diff --git a/macho-go/internal/wrapper/ofile/ofile.go b/macho-go/internal/wrapper/ofile/ofile.go new file mode 100644 index 0000000..772ea10 --- /dev/null +++ b/macho-go/internal/wrapper/ofile/ofile.go @@ -0,0 +1,47 @@ +package ofile + +import ( + "bufio" + "bytes" + "encoding/binary" + "os" + + log "github.com/sirupsen/logrus" + + . "ios-wrapper/pkg/ios" +) + +type OFile interface { + Cleanup() +} + +func NewOFile(f string) OFile { + file, err := os.Open(f) + if err != nil { + return nil + } + r := bufio.NewReader(file) + + var magic uint32 + magic_buf, _ := r.Peek(4) + magic_r := bytes.NewReader(magic_buf) + binary.Read(magic_r, binary.LittleEndian, &magic) + + if magic != Magic32 && magic != Magic64 && magic != Cigam32 && magic != Cigam64 && magic != MagicFat && + magic != CigamFat { + log.Panic("Magic does not match %x", magic) + return nil + } + + if magic == Magic32 || magic == Magic64 || magic == Cigam32 || + magic == Cigam64 { + return NewMachoFile(f) + } + + if magic == MagicFat || magic == CigamFat { + return NewFatFile(f) + } + + // not likely + return nil +} diff --git a/macho-go/internal/wrapper/program_context.go b/macho-go/internal/wrapper/program_context.go new file mode 100644 index 0000000..cf2a80b --- /dev/null +++ b/macho-go/internal/wrapper/program_context.go @@ -0,0 +1,110 @@ +package wrapper + +import ( + "encoding/json" + "io/ioutil" + + log "github.com/sirupsen/logrus" + + . "ios-wrapper/internal/wrapper/action" + . "ios-wrapper/internal/wrapper/ofile" + "ios-wrapper/pkg/protomodel" +) + +type UserConfig struct { + Repack bool `json:"repack"` + Hook bool `json:"hook"` + Emulator bool `json:"emulator"` + Debug bool `json:"debug"` + Root bool `json:"root"` + ServerURL string `json:"server_url"` + ReportType int32 `json:"report_type"` + RemoteConfigURL string `json:"remote_config_url"` + DomainID string `json:"domain_id"` +} + +func (uc *UserConfig) Protomodel() *protomodel.Config { + return &protomodel.Config{ + Hook: uc.Hook, + Repack: uc.Repack, + Emulator: uc.Emulator, + Debug: uc.Debug, + Root: uc.Root, + ServerUrl: uc.ServerURL, + ReportType: uc.ReportType, + RemoteConfigUrl: uc.RemoteConfigURL, + DomainId: uc.DomainID, + } +} + +type ProgramContext struct { + strip_init_pointers bool + remove_codesign bool + dylib_to_add []string + rpath_to_add []string + + outfile string + + actions []Action + err error + + user_config UserConfig + bcellfile string + signed bool // true: Protobuf; false: Protobuf +} + +func (pc *ProgramContext) ExplainError(err error) { + log.WithFields(log.Fields{ + "pc error": pc.err, + "ofile error": err, + }).Error("OFile Process gone wrong") +} + +func (pc *ProgramContext) ReadUserConfig(f string) { + json_data, err := ioutil.ReadFile(f) + if err != nil { + log.Panic("User Config file is invalid") + } + json.Unmarshal(json_data, &pc.user_config) +} + +func (pc *ProgramContext) AddAction(action Action) { + pc.actions = append(pc.actions, action) +} + +func (pc *ProgramContext) dispatchActions(ofile OFile) { + for _, action := range pc.actions { + err := RunAction(action, ofile) + if err != nil { + pc.err = err + break + } + } +} + +func (pc *ProgramContext) Process(ofile OFile) { + if pc.remove_codesign { + pc.AddAction(NewRemoveCodeSignatureAction()) + } + if pc.strip_init_pointers { + pc.AddAction(NewSaveInitPointerAction()) + pc.AddAction(NewRemoveInitPointerAction()) + } + ExperimentalFeature("Remove Unnecessary Info", func() { + pc.AddAction(NewRemoveUnnecessaryInfoAction()) + }) + ExperimentalFeature("Remove Classic Symbol", func() { + pc.AddAction(NewRemoveClassicSymbolAction()) + }) + pc.AddAction(NewAddRpathAction(pc.rpath_to_add)) + pc.AddAction(NewAddDylibAction(pc.dylib_to_add)) + + if pc.bcellfile != "" { + pc.AddAction(NewSaveBcellFileAction(pc.user_config.Protomodel(), pc.bcellfile, pc.signed)) + } else { + log.Warn("bcellfile is not set, no output bcellfile") + } + pc.AddAction(NewWriteFileAction(pc.outfile)) + + pc.dispatchActions(ofile) +} diff --git a/macho-go/internal/wrapper/util.go b/macho-go/internal/wrapper/util.go new file mode 100644 index 0000000..13aeb16 --- /dev/null +++ b/macho-go/internal/wrapper/util.go @@ -0,0 +1,30 @@ +package wrapper + +import ( + "fmt" + "os" + + log "github.com/sirupsen/logrus" +) + +func FileExist(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func CheckFileExistence(filename string) { + if !FileExist(filename) { + fmt.Printf("File %s does not exists\n", filename) + os.Exit(1) + } +} + +func ExperimentalFeature(feature string, foo func()) { + if os.Getenv("EXPERIMENTAL") == "1" { + log.Info(fmt.Sprintf("Running Experimental Feature %s", feature)) + foo() + } +} diff --git a/macho-go/pkg/dwarf/dwarf.go b/macho-go/pkg/dwarf/dwarf.go new file mode 100644 index 0000000..b6db273 --- /dev/null +++ b/macho-go/pkg/dwarf/dwarf.go @@ -0,0 +1,305 @@ +package dwarf + +import ( + "bytes" + "encoding/binary" + + log "github.com/sirupsen/logrus" + + . "ios-wrapper/pkg/leb128" +) + +type header struct { + unit_length uint32 + version uint16 + header_length uint32 + minimum_instruction_length uint8 + maximum_operations_per_instruction uint8 + default_is_stmt uint8 + line_base int8 + line_range uint8 + opcode_base uint8 +} + +type register struct { + address uint64 + op_index uint32 + file uint32 + line uint32 + column uint32 + is_stmt bool + basic_block bool + end_sequence bool + prologue_end bool + epilogue_begin bool + isa uint32 + discriminator uint32 +} + +func initRegister(is_stmt bool) register { + var reg register + reg.is_stmt = is_stmt + reg.file = 1 + reg.line = 1 + return reg +} + +func (reg *register) reset() { + reg.basic_block = false + reg.prologue_end = false + reg.epilogue_begin = false + reg.discriminator = 0 +} + +// Opcodes +const ( + Copy uint8 = 1 + AdvancePc uint8 = 2 + AdvanceLine uint8 = 3 + SetFile uint8 = 4 + SetColumn uint8 = 5 + NegateStmt uint8 = 6 + SetBasicBlock uint8 = 7 + ConstAddPc uint8 = 8 + FixedAdvancePc uint8 = 9 + SetPrologueEnd uint8 = 10 + SetEpiloqueBegin uint8 = 11 + SetIsa uint8 = 12 + + // Extended opcodes + EndSequence uint8 = 1 + SetAddress uint8 = 2 + DefineFile uint8 = 3 + SetDiscriminator uint8 = 4 +) + +type row struct { + address uint64 + op_index uint32 + directory string + file string + line int32 + column int32 + end_sequence bool +} + +type filename struct { + name string + dir uint +} + +type DebuglineParser struct { + buf *bytes.Buffer + byteorder binary.ByteOrder + header header + + standard_opcode_lengths []uint8 + include_directories []string + file_names []filename + + row []row +} + +func (parser *DebuglineParser) Find(address uint64) (bool, uint32, string, string) { + for _, row := range parser.row { + if address == row.address { + return true, uint32(row.line), row.directory, row.file + } + } + return false, 0, "", "" +} + +func (parser *DebuglineParser) Parse(buf *bytes.Buffer, byteorder binary.ByteOrder) { + parser.buf = buf + parser.byteorder = byteorder + parser.parseHeader() + parser.parseOpcode() + parser.parseDirectoryTable() + parser.parseFilenameTable() + parser.parseStatementProgram() +} + +func (parser *DebuglineParser) parseHeader() { + binary.Read(parser.buf, parser.byteorder, &parser.header.unit_length) + binary.Read(parser.buf, parser.byteorder, &parser.header.version) + binary.Read(parser.buf, parser.byteorder, &parser.header.header_length) + binary.Read(parser.buf, parser.byteorder, &parser.header.minimum_instruction_length) + + if parser.header.version >= 4 { + binary.Read(parser.buf, parser.byteorder, &parser.header.maximum_operations_per_instruction) + } + + binary.Read(parser.buf, parser.byteorder, &parser.header.default_is_stmt) + binary.Read(parser.buf, parser.byteorder, &parser.header.line_base) + binary.Read(parser.buf, parser.byteorder, &parser.header.line_range) + binary.Read(parser.buf, parser.byteorder, &parser.header.opcode_base) +} + +func (parser *DebuglineParser) parseOpcode() { + var standard_opcode_lengths []uint8 + for i := uint8(1); i < parser.header.opcode_base; i++ { + var num uint8 + binary.Read(parser.buf, parser.byteorder, &num) + standard_opcode_lengths = append(standard_opcode_lengths, num) + } + parser.standard_opcode_lengths = standard_opcode_lengths +} + +func (parser *DebuglineParser) parseDirectoryTable() { + var include_directories []string + for { + dir, _ := parser.buf.ReadString(0) + if dir == "\x00" { + break + } + include_directories = append(include_directories, dir) + } + parser.include_directories = include_directories +} + +func (parser *DebuglineParser) parseFilenameTable() { + var file_names []filename + for { + file, _ := parser.buf.ReadString(0) + if file == "\x00" { + break + } + dir, _ := ReadULEB(parser.buf) // directory index + ReadULEB(parser.buf) // time + ReadULEB(parser.buf) // length + file_names = append(file_names, filename{ + name: file, + dir: dir, + }) + } + parser.file_names = file_names +} + +func (parser *DebuglineParser) parseStatementProgram() { + reg := initRegister(parser.header.default_is_stmt == 1) + +runloop: + for { + var opcode uint8 + binary.Read(parser.buf, parser.byteorder, &opcode) + if opcode >= parser.header.opcode_base { + adjusted_opcode := opcode - parser.header.opcode_base + operation_advance := adjusted_opcode / parser.header.line_range + increment_address_and_op_index(®, &parser.header, uint(operation_advance)) + reg.line += uint32(int32(parser.header.line_base) + int32(adjusted_opcode%parser.header.line_range)) + parser.register_to_matrix(®) + reg.reset() + } else if opcode == 0 { + size_, _ := ReadULEB(parser.buf) + size := size_ - 1 + var extended_opcode uint8 + binary.Read(parser.buf, parser.byteorder, &extended_opcode) + switch extended_opcode { + case EndSequence: + reg.end_sequence = true + parser.register_to_matrix(®) + reg = initRegister(parser.header.default_is_stmt == 1) + break runloop + case SetAddress: + if size == 8 { + binary.Read(parser.buf, parser.byteorder, ®.address) + } else if size == 4 { + var v uint32 + binary.Read(parser.buf, parser.byteorder, &v) + reg.address = uint64(v) + } else { + parser.buf.Next(int(size)) + } + reg.op_index = 0 + case SetDiscriminator: + v, _ := ReadULEB(parser.buf) + reg.discriminator = uint32(v) + default: + parser.buf.Next(int(size)) + } + } else { + switch opcode { + case Copy: + parser.register_to_matrix(®) + reg.reset() + case AdvancePc: + v, _ := ReadULEB(parser.buf) + increment_address_and_op_index(®, &parser.header, v) + case AdvanceLine: + v, _ := ReadSLEB(parser.buf) + if v < 0 { + reg.line -= uint32(-v) + } else { + reg.line += uint32(v) + } + case SetFile: + v, _ := ReadULEB(parser.buf) + reg.file = uint32(v) + case SetColumn: + v, _ := ReadULEB(parser.buf) + reg.column = uint32(v) + case NegateStmt: + reg.is_stmt = !reg.is_stmt + case SetBasicBlock: + reg.basic_block = true + case ConstAddPc: + adjusted_opcode := 255 - parser.header.opcode_base + operation_advance := adjusted_opcode / parser.header.line_range + increment_address_and_op_index(®, &parser.header, uint(operation_advance)) + case FixedAdvancePc: + var v uint16 + binary.Read(parser.buf, parser.byteorder, &v) + reg.address += uint64(v) + reg.op_index = 0 + case SetPrologueEnd: + reg.prologue_end = true + case SetEpiloqueBegin: + reg.epilogue_begin = true + case SetIsa: + v, _ := ReadULEB(parser.buf) + reg.isa = uint32(v) + default: + break + } + } + } +} + +func increment_address_and_op_index(reg *register, h *header, operation_advance uint) { + min_length := uint64(h.minimum_instruction_length) + max_op := uint64(h.maximum_operations_per_instruction) + advance := uint64(operation_advance) + op_index := uint64(reg.op_index) + + if h.maximum_operations_per_instruction == 1 { + reg.address += advance * min_length + } else { + reg.address += min_length * ((op_index + advance) / max_op) + reg.op_index = uint32((op_index + advance) % max_op) + } +} + +func (parser *DebuglineParser) register_to_matrix(reg *register) { + if !(reg.is_stmt || (reg.line > 0 && reg.column > 0)) { + return + } + + file := parser.file_names[reg.file-1] + path := parser.include_directories[file.dir-1] + row := row{ + address: reg.address, + op_index: reg.op_index, + directory: path, + file: file.name, + line: int32(reg.line), + column: int32(reg.column), + end_sequence: reg.end_sequence, + } + parser.row = append(parser.row, row) + log.WithFields(log.Fields{ + "address": row.address, + "directory": row.directory, + "file": row.file, + "line": row.line, + }).Trace("Debugline Row") +} diff --git a/macho-go/pkg/ios/const.go b/macho-go/pkg/ios/const.go new file mode 100644 index 0000000..290c892 --- /dev/null +++ b/macho-go/pkg/ios/const.go @@ -0,0 +1,159 @@ +package ios + +// Magic values of Mach-O and Fat binaries +const ( + Magic32 uint32 = 0xfeedface + Cigam32 uint32 = 0xcefaedfe + Magic64 uint32 = 0xfeedfacf + Cigam64 uint32 = 0xcffaedfe + MagicFat uint32 = 0xcafebabe + CigamFat uint32 = 0xbebafeca +) + +// Flags defined in mach_header.flags +const ( + FlagNoUndefs uint32 = 0x1 + FlagIncrLink uint32 = 0x2 + FlagDyldLink uint32 = 0x4 + FlagBindAtLoad uint32 = 0x8 + FlagPrebound uint32 = 0x10 + FlagSplitSegs uint32 = 0x20 + FlagLazyInit uint32 = 0x40 + FlagTwoLevel uint32 = 0x80 + FlagForceFlat uint32 = 0x100 + FlagNoMultiDefs uint32 = 0x200 + FlagNoFixPrebinding uint32 = 0x400 + FlagPrebindable uint32 = 0x800 + FlagAllModsBound uint32 = 0x1000 + FlagSubsectionsViaSymbols uint32 = 0x2000 + FlagCanonical uint32 = 0x4000 + FlagWeakDefines uint32 = 0x8000 + FlagBindsToWeak uint32 = 0x10000 + FlagAllowStackExecution uint32 = 0x20000 + FlagRootSafe uint32 = 0x40000 + FlagSetuidSafe uint32 = 0x80000 + FlagNoReexportedDylibs uint32 = 0x100000 + FlagPIE uint32 = 0x200000 + FlagDeadStrippableDylib uint32 = 0x400000 + FlagHasTLVDescriptors uint32 = 0x800000 + FlagNoHeapExecution uint32 = 0x1000000 + FlagAppExtensionSafe uint32 = 0x2000000 +) + +// Types of Load command +const ( + LC_DUMMY uint32 = 0x0 + LC_SEGMENT uint32 = 0x1 + LC_SYMTAB uint32 = 0x2 + LC_SYMSEG uint32 = 0x3 + LC_THREAD uint32 = 0x4 + LC_UNIXTHREAD uint32 = 0x5 + LC_LOADFVMLIB uint32 = 0x6 + LC_IDFVMLIB uint32 = 0x7 + LC_IDENT uint32 = 0x8 + LC_FVMFILE uint32 = 0x9 + LC_PREPAGE uint32 = 0xa + LC_DYSYMTAB uint32 = 0xb + LC_LOAD_DYLIB uint32 = 0xc + LC_ID_DYLIB uint32 = 0xd + LC_LOAD_DYLINKER uint32 = 0xe + LC_ID_DYLINKER uint32 = 0xf + LC_PREBOUND_DYLIB uint32 = 0x10 + LC_ROUTINES uint32 = 0x11 + LC_SUB_FRAMEWORK uint32 = 0x12 + LC_SUB_UMBRELLA uint32 = 0x13 + LC_SUB_CLIENT uint32 = 0x14 + LC_SUB_LIBRARY uint32 = 0x15 + LC_TWOLEVEL_HINTS uint32 = 0x16 + LC_PREBIND_CKSUM uint32 = 0x17 + LC_LOAD_WEAK_DYLIB uint32 = 0x18 | 0x80000000 + LC_SEGMENT_64 uint32 = 0x19 + LC_ROUTINES_64 uint32 = 0x1a + LC_UUID uint32 = 0x1b + LC_RPATH uint32 = 0x1c | 0x80000000 + LC_CODE_SIGNATURE uint32 = 0x1d + LC_SEGMENT_SPLIT_INFO uint32 = 0x1e + LC_REEXPORT_DYLIB uint32 = 0x1f | 0x80000000 + LC_LAZY_LOAD_DYLIB uint32 = 0x20 + LC_ENCRYPTION_INFO uint32 = 0x21 + LC_DYLD_INFO uint32 = 0x22 + LC_DYLD_INFO_ONLY uint32 = 0x22 | 0x80000000 + LC_VERSION_MIN_MACOSX uint32 = 0x24 + LC_VERSION_MIN_IPHONEOS uint32 = 0x25 + LC_FUNCTION_STARTS uint32 = 0x26 + LC_DYLD_ENVIRONMENT uint32 = 0x27 + LC_MAIN uint32 = 0x28 | 0x80000000 + LC_DATA_IN_CODE uint32 = 0x29 + LC_SOURCE_VERSION uint32 = 0x2A + LC_DYLIB_CODE_SIGN_DRS uint32 = 0x2B + LC_ENCRYPTION_INFO_64 uint32 = 0x2C + LC_LINKER_OPTION uint32 = 0x2D + LC_LINKER_OPTIMIZATION_HINT uint32 = 0x2E + LC_VERSION_MIN_TVOS uint32 = 0x2F + LC_VERSION_MIN_WATCHOS uint32 = 0x30 + LC_BUILD_VERSION uint32 = 0x32 + LC_DYLD_EXPORTS_TRIE uint32 = 0x33 | 0x80000000 + LC_DYLD_CHAINED_FIXUPS uint32 = 0x34 | 0x80000000 +) + +const ( + Header_size uint64 = 28 + Header_size_64 uint64 = 28 + 4 +) + +const ( + CPU_ARCH_ABI64 uint32 = 0x0100_0000 + + CPU_TYPE_ARM uint32 = 12 + CPU_TYPE_ARM64 uint32 = CPU_TYPE_ARM | CPU_ARCH_ABI64 + CPU_TYPE_POWERPC uint32 = 18 + CPU_TYPE_POWERPC64 uint32 = CPU_TYPE_POWERPC | CPU_ARCH_ABI64 + CPU_TYPE_I386 uint32 = 7 + CPU_TYPE_X86_64 uint32 = CPU_TYPE_I386 | CPU_ARCH_ABI64 +) + +const ( + // Section Type + S_REGULAR uint8 = 0x0 + S_ZEROFILL uint8 = 0x1 + S_CSTRING_LITERALS uint8 = 0x2 + S_4BYTE_LITERALS uint8 = 0x3 + S_8BYTE_LITERALS uint8 = 0x4 + S_LITERAL_POINTERS uint8 = 0x5 + S_NON_LAZY_SYMBOL_POINTERS uint8 = 0x6 + S_LAZY_SYMBOL_POINTERS uint8 = 0x7 + S_SYMBOL_STUBS uint8 = 0x8 + S_MOD_INIT_FUNC_POINTERS uint8 = 0x9 + S_MOD_TERM_FUNC_POINTERS uint8 = 0xa + S_COALESCED uint8 = 0xb + S_GB_ZEROFILL uint8 = 0xc + S_INTERPOSING uint8 = 0xd + S_16BYTE_LITERALS uint8 = 0xe + S_DTRACE_DOF uint8 = 0xf + S_LAZY_DYLIB_SYMBOL_POINTERS uint8 = 0x10 + S_THREAD_LOCAL_REGULAR uint8 = 0x11 + S_THREAD_LOCAL_ZEROFILL uint8 = 0x12 + S_THREAD_LOCAL_VARIABLES uint8 = 0x13 + S_THREAD_LOCAL_VARIABLE_POINTERS uint8 = 0x14 + S_THREAD_LOCAL_INIT_FUNCTION_POINTERS uint8 = 0x15 +) + +const ( +// Section Attribute +) + +const ( + BIND_OPCODE_DONE uint8 = 0x00 + BIND_OPCODE_SET_DYLIB_ORDINAL_IMM uint8 = 0x10 + BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB uint8 = 0x20 + BIND_OPCODE_SET_DYLIB_SPECIAL_IMM uint8 = 0x30 + BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM uint8 = 0x40 + BIND_OPCODE_SET_TYPE_IMM uint8 = 0x50 + BIND_OPCODE_SET_ADDEND_SLEB uint8 = 0x60 + BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB uint8 = 0x70 + BIND_OPCODE_ADD_ADDR_ULEB uint8 = 0x80 + BIND_OPCODE_DO_BIND uint8 = 0x90 + BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB uint8 = 0xA0 + BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED uint8 = 0xB0 + BIND_OPCODE_DO_BIND_ADD_ULEB_TIMES_SKIPPING_ULEB uint8 = 0xC0 +) diff --git a/macho-go/pkg/ios/fat/create-fat.go b/macho-go/pkg/ios/fat/create-fat.go new file mode 100644 index 0000000..3af875f --- /dev/null +++ b/macho-go/pkg/ios/fat/create-fat.go @@ -0,0 +1,184 @@ +/// Contains the declaration of FatHeader and FatArch +/// These structs are always written using Big-Endian, +/// as documented in the mach-o/fat.h +/// This file also has a CreateFat function to generate +/// Fat file from a list of MachoContext +package fat + +import ( + "io" + "os" + "sort" + + log "github.com/sirupsen/logrus" + + . "ios-wrapper/pkg/ios" + macho "ios-wrapper/pkg/ios/macho" +) + +/// Get the alignment for the Mach-O in Fat binary +/// The returned value is the multiplier of 2 +func GetAlignment(h *macho.Header) uint32 { + switch h.Cputype() { + case CPU_TYPE_ARM, CPU_TYPE_ARM64: + return 0xe // log2(0x4000) + case CPU_TYPE_POWERPC, + CPU_TYPE_POWERPC64, + CPU_TYPE_I386, + CPU_TYPE_X86_64: + return 0xc // log2(0x1000) + default: + return 0xd // log2(0x2000) + } +} + +func MachosToFatArchs(machos []*macho.MachoContext) []*FatArch { + var fa []*FatArch + for _, m := range machos { + fa = append(fa, &FatArch{ + Cputype: m.Header().Cputype(), + Cpusubtype: m.Header().Cpusubtype(), + Size: m.FileSize(), + Offset: 0, + Align: GetAlignment(m.Header()), + }) + } + return fa +} + +/// Create a Fat binary from MachoContext +/// Convert MachoContext to FatArch +/// Calculate the alignment, now using basic calculation +/// because iOS always has valid archs ARM +/// +/// Sort the Fat arch as per the cctools/lipo +/// Calculate the offset with each Mach-O +/// +/// Write the FatHeader +/// Write the FatArchs +/// Write the Mach-Os +func CreateFat(machos []*macho.MachoContext, outfilename string) error { + archs := MachosToFatArchs(machos) + sort.SliceStable(archs, func(i, j int) bool { + if archs[i].Cputype == archs[j].Cputype { + return archs[i].Cpusubtype < archs[i].Cpusubtype + } + if archs[i].Cputype == CPU_TYPE_ARM64 { + return false + } + if archs[j].Cputype == CPU_TYPE_ARM64 { + return true + } + return archs[i].Cpusubtype < archs[j].Cpusubtype + }) + + // calculate the offset for each FatArch + offset := uint32(8) // size of fat header, bytes + // offset to the first macho + offset += uint32(len(archs)) * 5 // size of fat_arch, bytes + + for _, arch := range archs { + offset = uint32(OffsetRounder(uint64(offset), 1< 4GB +// else just use FatArch +type FatArch64 struct { + Cputype uint32 + Cpusubtype uint32 + Offset uint64 + Size uint64 + Align uint32 +} + +type FatContext struct { + fatArch []*FatArch +} + +func (fc *FatContext) ParseFile(file *os.File) { + r := bufio.NewReader(file) + + { // read magic to define byteorder and pointersize + var magic uint32 + magic_buf := make([]byte, 4) + r.Read(magic_buf) + magic_r := bytes.NewReader(magic_buf) + binary.Read(magic_r, binary.BigEndian, &magic) + + if magic != MagicFat { + return + } + } + + var nfat_arch uint32 + binary.Read(r, binary.BigEndian, &nfat_arch) + + for i := uint32(0); i < nfat_arch; i++ { + var fat_arch FatArch + binary.Read(r, binary.BigEndian, &fat_arch.Cputype) + binary.Read(r, binary.BigEndian, &fat_arch.Cpusubtype) + binary.Read(r, binary.BigEndian, &fat_arch.Offset) + binary.Read(r, binary.BigEndian, &fat_arch.Size) + binary.Read(r, binary.BigEndian, &fat_arch.Align) + fc.fatArch = append(fc.fatArch, &fat_arch) + } +} + +func (fc *FatContext) Machos() []*FatArch { + return fc.fatArch +} + +func FatSplit(path string) ([]string, error) { + return CreateMachosFromFat(path) +} + +// Parse files into Mach-O, calculate fat_arch and join +// into a Fat binary +// @paths: paths to Mach-O binaries +// @out: output Fat file path +// returns success? +func FatJoin(paths []string, out string) error { + machos, err := macho.MachosFromFiles(paths) + if err != nil { + return err + } + if macho.CheckDuplicateArch(machos) { + return errors.New("Duplicate Arch") + } + return CreateFat(machos, out) +} diff --git a/macho-go/pkg/ios/fat/split-fat.go b/macho-go/pkg/ios/fat/split-fat.go new file mode 100644 index 0000000..4f4eaf8 --- /dev/null +++ b/macho-go/pkg/ios/fat/split-fat.go @@ -0,0 +1,62 @@ +package fat + +import ( + "fmt" + "io" + "os" + "strings" +) + +// Split a Fat binary into multiple Mach-O binaries +// @ifile: path to Fat binary +// returns a list of file path of splited Mach-O binaries +func CreateMachosFromFat(ifile string) ([]string, error) { + f, err := os.OpenFile(ifile, os.O_RDONLY, 0644) + if err != nil { + fmt.Println("Cannot open file") + return []string{}, err + } + + var fc FatContext + fc.ParseFile(f) + + var r []string + for _, arch := range fc.Machos() { + offset := arch.Offset + size := arch.Size + buf := make([]byte, size) + filename := fmt.Sprintf( + "%s_%x", + strings.ReplaceAll(ifile, " ", "_"), + offset, + ) + + var err error + _, err = f.Seek(int64(offset), io.SeekStart) + if err != nil { + return r, err + } + + _, err = f.Read(buf) + if err != nil { + return r, err + } + + outfile, err := os.OpenFile( + filename, + os.O_CREATE|os.O_WRONLY, + 0777, + ) + if err != nil { + return r, err + } + + _, err = outfile.Write(buf) + if err != nil { + return r, err + } + + r = append(r, filename) // everything is find, append new file path + } + return r, nil +} diff --git a/macho-go/pkg/ios/macho/dwarf.go b/macho-go/pkg/ios/macho/dwarf.go new file mode 100644 index 0000000..49d465a --- /dev/null +++ b/macho-go/pkg/ios/macho/dwarf.go @@ -0,0 +1,55 @@ +package macho + +import ( + "bytes" + "ios-wrapper/pkg/dwarf" +) + +type DebuglineInfo struct { + debuglines []*dwarf.DebuglineParser +} + +type LineInfo struct { + Line uint32 + Directory string + File string +} + +func (dinfo *DebuglineInfo) Find(address uint64) *LineInfo { + for _, debugline := range dinfo.debuglines { + found, line, directory, file := debugline.Find(address) + if found { + return &LineInfo{ + line, directory, file, + } + } + } + return nil +} + +// Parse the __debug_line into a list of DebugLine +// +// reference from Crystal dwarf parser +// https://github.com/crystal-lang/crystal/blob/421fa11bf05b11441b74bbeb2762b673f96fec3f/src/crystal/dwarf/line_numbers.cr +func (mc *MachoContext) DebugLineInfo() *DebuglineInfo { + debug_line_section := mc.FindSection("__debug_line") + if debug_line_section == nil { + return nil + } + + start := uint64(debug_line_section.Offset()) + end := start + debug_line_section.Size() + buf := bytes.NewBuffer(mc.buf[start:end]) + + debugLine := []*dwarf.DebuglineParser{} + + for buf.Len() != 0 { + parser := &dwarf.DebuglineParser{} + parser.Parse(buf, mc.byteorder) + debugLine = append(debugLine, parser) + } + + return &DebuglineInfo{ + debugLine, + } +} diff --git a/macho-go/pkg/ios/macho/dyld_info.go b/macho-go/pkg/ios/macho/dyld_info.go new file mode 100644 index 0000000..a8183b8 --- /dev/null +++ b/macho-go/pkg/ios/macho/dyld_info.go @@ -0,0 +1,237 @@ +package macho + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + + log "github.com/sirupsen/logrus" + + . "ios-wrapper/pkg/ios" + . "ios-wrapper/pkg/leb128" +) + +type ImportSymbol struct { + name string + dylib string + segment uint32 + segment_offset uint32 + address uint64 + file_address uint64 + + // push number + pnum uint32 + stub uint64 +} + +func (sym *ImportSymbol) Name() string { + return sym.name +} + +func (sym *ImportSymbol) Dylib() string { + return sym.dylib +} + +func (sym *ImportSymbol) Address() uint64 { + return sym.address +} + +func (sym *ImportSymbol) Pnum() uint32 { + return sym.pnum +} + +func (sym *ImportSymbol) Stub() uint64 { + return sym.stub +} + +func (mc *MachoContext) CollectLazyBindSymbols() []*ImportSymbol { + if mc.dyldinfo == nil { + return mc.CollectLazyBindSymbolsModern() + } else { + return mc.CollectLazyBindSymbolsLegacy() + } +} + +// New convention using LC_DYLD_CHAINED_FIXUPS +func (mc *MachoContext) CollectLazyBindSymbolsModern() []*ImportSymbol { + var buf []byte + for _, cmd := range mc.Linkedits() { + if (cmd.Cmd() != LC_DYLD_CHAINED_FIXUPS) { + continue + } + + count_offset := int64(cmd.Dataoff()) + 16 + mc.file.WriteAt([]byte{0, 0, 0, 0}, count_offset) + + symbol_offset := int64(0x4000) + mc.file.WriteAt([]byte{0, 0, 0, 0, 0, 0, 0, 0}, symbol_offset) + + buf = mc.buf[cmd.Dataoff():cmd.Dataoff() + cmd.Datasize()] + break + } + + r := bytes.NewReader(buf) + rr := bufio.NewReader(r) + + var fixups_version int32 + var starts_offset int32 + var imports_offset int32 + var symbols_offset int32 + var imports_count int32 + var imports_format int32 + var symbols_format int32 + binary.Read(rr, mc.byteorder, &fixups_version) + binary.Read(rr, mc.byteorder, &starts_offset) + binary.Read(rr, mc.byteorder, &imports_offset) + binary.Read(rr, mc.byteorder, &symbols_offset) + binary.Read(rr, mc.byteorder, &imports_count) + binary.Read(rr, mc.byteorder, &imports_format) + binary.Read(rr, mc.byteorder, &symbols_format) + fmt.Printf( + "HMMGE %x %x\n", + imports_offset, + imports_count, + ) + + return []*ImportSymbol{} +} + +// Old convention using LC_DYLD_INFO_ONLY section and bytecode runner +func (mc *MachoContext) CollectLazyBindSymbolsLegacy() []*ImportSymbol { + start := mc.dyldinfo.lazy_bind_off + size := mc.dyldinfo.lazy_bind_size + end := start + size + + if size == 0 { + return []*ImportSymbol{} + } + + // clear this whole section to 0x00 BIND_OPCODE_DONE + dummy := []byte{ + 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, + 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, + } + // make LINK EDIT section writable + // mc.file.WriteAt([]byte{0x03}, int64(0x3f8)) + // set number of symbols to 0 + mc.file.WriteAt([]byte{0, 0, 0, 0}, int64(0x444)) + mc.file.WriteAt([]byte{0, 0, 0, 0}, int64(0x44c)) + mc.file.WriteAt([]byte{0, 0, 0, 0}, int64(0x48c)) + mc.file.WriteAt(dummy, int64(start)) + + buf := bytes.NewBuffer(mc.buf[start:end]) + offset := uint(0) + + var syms []*ImportSymbol + var sym ImportSymbol + // symoffset := offset + lastop_done := false + for offset < uint(size) { + d, _ := buf.ReadByte() + op := d & 0xf0 + imm := d & 0x0f + + log.WithFields(log.Fields{ + "op": fmt.Sprintf("0x%x", op), + "imm": fmt.Sprintf("0x%x", imm), + }).Trace("Bytecode") + + if op != BIND_OPCODE_DONE && lastop_done { + // symoffset = offset + lastop_done = false + } + + switch op { + case BIND_OPCODE_DONE: + lastop_done = true + offset += 1 + break + + case BIND_OPCODE_DO_BIND: + if sym.name != "" { + new_sym := sym + syms = append(syms, &new_sym) + // fmt.Printf("Offset 0x%x: Symbol %+v\n", symoffset, sym) + + log.WithFields(log.Fields{ + // "offset": fmt.Sprintf("0x%x", symoffset), + "symbol": sym.name, + }).Trace("Bind") + sym.name = "" + } + offset += 1 + break + + case BIND_OPCODE_SET_DYLIB_ORDINAL_IMM: + sym.dylib = string(mc.dylibs[int32(imm)-1].name[:]) + offset += 1 + break + + case BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB: + uleb, br := ReadULEB(buf) + sym.dylib = string(mc.dylibs[int32(uleb)-1].name[:]) + offset += br + break + + case BIND_OPCODE_SET_DYLIB_SPECIAL_IMM: + if imm == 0 { + sym.dylib = "dylib_0" + } else if imm == 0x0f { + sym.dylib = "dylib_-1" + } else if imm == 0x0e { + sym.dylib = "dylib_-2" + } + offset += 1 + break + + case BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM: + sym.name, _ = buf.ReadString(0) + offset += uint(len(sym.name)) + break + + case BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB: + uleb, br := ReadULEB(buf) + sym.segment = uint32(imm) + sym.segment_offset = uint32(uleb) + + seg := mc.segments[sym.segment] + sym.address = seg.Vmaddr() + uint64(sym.segment_offset) + sym.file_address = seg.Fileoff() + uint64(sym.segment_offset) + + offset += br + break + + case BIND_OPCODE_ADD_ADDR_ULEB: + uleb, br := ReadULEB(buf) + sym.segment_offset += uint32(uleb) + sym.address += uint64(uleb) + sym.file_address += uint64(uleb) + offset += br + break + + default: + break + } + } + for _, sym := range syms { + switch mc.ArchName() { + case "armv7", "armv7s": + var addr uint32 + b := mc.buf[sym.file_address : sym.file_address+4] + r := bytes.NewBuffer(b) + binary.Read(r, mc.byteorder, &addr) + sym.stub = uint64(addr) + break + case "arm64", "arm64e": + b := mc.buf[sym.file_address : sym.file_address+8] + r := bytes.NewBuffer(b) + binary.Read(r, mc.byteorder, &sym.stub) + break + default: + sym.pnum = 0 + sym.stub = 0 + } + } + return syms +} diff --git a/macho-go/pkg/ios/macho/edit.go b/macho-go/pkg/ios/macho/edit.go new file mode 100644 index 0000000..36de576 --- /dev/null +++ b/macho-go/pkg/ios/macho/edit.go @@ -0,0 +1,268 @@ +package macho + +import ( + "fmt" + "io" + + log "github.com/sirupsen/logrus" + + . "ios-wrapper/pkg/ios" +) + +func rewriteLoadcommandsWithoutCodesignature(mc *MachoContext) { + if mc.Is64bit() { + mc.file.Seek(int64(Header_size_64), io.SeekStart) + } else { + mc.file.Seek(int64(Header_size), io.SeekStart) + } + for _, cmd := range mc.commands { + if cmd.Cmd() == LC_CODE_SIGNATURE { + continue + } + log.WithFields(log.Fields{ + "cmd": cmd.Cmd(), + "cmdsize": cmd.Cmdsize(), + "name": cmd.Cmdname(), + }).Trace("Rewrite Load command") + mc.file.Write(cmd.Serialize(mc)) + } +} + +// Remove Codesign data from Mach-O binary +// There are many caveats to this problem. +// Mostly related to how codesign-allocate works. +// Removing codesign data at the end of __LINKEDIT sement +// is probably easier than removing them at an abitrary location. +// +// Assuming the codesign data is at the end of __LINKEDIT segment. +// The binary is probably signed at the last step, which is common. +// +// CODE_SIGNATURE load commands points to the codesign data offset and size. +// __LINKEDIT section points to data offset and size. +// We have: +// linkedit = (section*) LC_SEGMENT.section[0] // name="__LINKEDIT" +// codesign = (linkedit_data_command*) LC_CODE_SIGNATURE +// BinarySize = { f.seek(0, SEEKEND); return f.tell() } +// +// linkedit->fileoff + linkedit->filesize == codesign->dataOff + codesign->dataSize +// linkedit->fileoff + linkedit->filesize == BinarySize +// +// To remove the codesign data, we truncate the file to remove the codesign data. +// Then we update the linkedit->filesize to linkedit->filesize - codesign->dataSize +// We also fix the header to not read the LC_CODE_SIGNATURE, +// because codesign is the last step, the last load command is LC_CODE_SIGNATURE. +// Fix header->ncmds -= 1, header->sizeofcmds -= sizeof(linkedit_data_command) +// +// There's one more caveat, as mentioned by `insert_dylib`. Codesign data is aligned +// by 0x10. So it would have some padding before codesign data. Now that we have +// removed the data, but the padding still exists and is not prefered by codesign-allocate. +// To fix this issue, **again** we assume that the previous section is the String table +// (referenced by symtab) and increase the size of the string table to the end of data. +// +// A better approach could be an extended search for section pointing to the previous data. +// But general cases are String table. +// +// Implementation warnings: +// We have two implementation designs. +// - Re-parse the load commands and edit the value at offset +// - Use parsed load commands data, edit the values, and rewrite header + load commands +func (mc *MachoContext) RemoveCodesign() bool { + if !mc.WriteEnabled() { + return false + } + + linkedit := mc.FindSegment("__LINKEDIT") + if linkedit == nil { + log.Warn("The binary has no __LINKEDIT Segment") + return false + } + + var codesign *LinkEdit + for _, cmd := range mc.Linkedits() { + if cmd.Cmd() == LC_CODE_SIGNATURE { + codesign = cmd + break + } + } + if codesign == nil { + log.Warn("The binary is not signed, no LC_CODE_SIGNATURE found") + return false + } + + filesize := uint64(len(mc.buf)) + linkedit_end := uint64(linkedit.Fileoff()) + linkedit.Filesize() + codesign_end := uint64(codesign.Dataoff() + codesign.Datasize()) + + // linkedit is the last item in Mach-O binary + if linkedit_end != filesize { + log.Panic("Link edit is not the last item") + return false + } + // codesign is the last in linkedit + if linkedit_end != codesign_end { + log.Panic("Code sign data is not the last item in link edit segment") + return false + } + + linkedit_newsize := linkedit.Filesize() - uint64(codesign.Datasize()) + + if (mc.symtab.stroff+mc.symtab.strsize)%0x10 == 0 { + // codesign requires padding of 0x10 + // if strtab+strsize & 0xff < 0x8, it will pad til 0x10 + // when removed codesign, we truncate the file to offset at 0x8 rather at 0x10 + } else { + linkedit_newsize -= 8 + } + + mc.file.Truncate(int64(uint64(linkedit.Fileoff()) + linkedit_newsize)) + mc.symtab.strsize = uint32(linkedit.Fileoff()+linkedit_newsize) - mc.symtab.stroff + + // fix linkedit section + if mc.Is64bit() { + linkedit.(*Segment64).filesize = linkedit_newsize + } else { + linkedit.(*Segment32).filesize = uint32(linkedit_newsize) + } + + // rewrite load commands + mc.UpdateHeaderRemoveLcmd(codesign.Cmdsize()) + rewriteLoadcommandsWithoutCodesignature(mc) + + // erase old LC_CODE_SIGNATURE data + old_codesign_lcmd_offset := func() uint64 { + loadcmd_size := int64(mc.Header().sizeofcmds) + if mc.Is64bit() { + return uint64(loadcmd_size) + Header_size_64 + } else { + return uint64(loadcmd_size) + Header_size + } + }() + // size of codesign = sizeof linkedit_data_command = 4 * 4 + mc.file.WriteAt(make([]byte, 4*4), int64(old_codesign_lcmd_offset)) + // mc.file.Seek(old_codesign_lcmd_offset, io.SeekStart) + // mc.file.Write(make([]byte, 4*4)) + + return true +} + +func (mc *MachoContext) RemoveInitFunctions() bool { + if mc.WriteEnabled() { + for _, ptr := range mc.InitFunctions() { + if mc.Is64bit() { + mc.file.WriteAt([]byte{0, 0, 0, 0, 0, 0, 0, 0}, int64(ptr.offset)) + } else { + mc.file.WriteAt([]byte{0, 0, 0, 0}, int64(ptr.offset)) + } + log.WithFields(log.Fields{ + "offset": fmt.Sprintf("0x%x", ptr.Offset()), + "value": fmt.Sprintf("0x%x", ptr.Value()), + }).Debug("Remove init pointer") + } + return true + } + return false +} + +func (mc *MachoContext) RemoveUnnecessaryInfo() bool { + for _, command := range mc.commands { + switch command.(type) { + case *LinkEdit: + var le = command.(*LinkEdit) + if le.Cmdname() != "LC_FUNCTION_STARTS" && le.Cmdname() != "LC_DATA_IN_CODE" { + continue + } + var start int64 = int64(le.dataoff) + var end int64 = start + int64(le.datasize) + for i := start; i < end; i++ { + mc.file.WriteAt([]byte{0}, i) + } + continue + default: + continue + } + } + return false +} + +func (mc *MachoContext) RemoveClassicSymbol() bool { + return false +} + +func (mc *MachoContext) AddLoadCmd(lcmd LoadCommand) { + var offset uint64 + payload := lcmd.Serialize(mc) + if uint64(len(payload)) > mc.PaddingSize() { + log.WithFields(log.Fields{ + "cmd": lcmd, + "len(cmd)": len(payload), + "available": mc.PaddingSize(), + }).Panic("Not enough space to add load command") + } + + if mc.Is64bit() { + offset = Header_size_64 + uint64(mc.header.sizeofcmds) + } else { + offset = Header_size + uint64(mc.header.sizeofcmds) + } + log.WithFields(log.Fields{ + "cmd": lcmd.Cmdname(), + "size": len(payload), + "offset": offset, + }).Debug("Adding Load Command") + mc.file.WriteAt(payload, int64(offset)) + mc.UpdateHeaderAddLcmd(uint32(len(payload))) +} + +func (mc *MachoContext) AddDylib(dylib string) { + if mc.DylibExisted(dylib) { + log.WithFields(log.Fields{ + "dylib": dylib, + }).Debug("Adding dylib but existed") + return + } + name := []byte(dylib) + name = append(name, 0) + dylib_lcmd := Dylib{ + c: LoadCmd{cmd: LC_LOAD_DYLIB, cmdsize: 0}, + name: name, + nameoff: 24, + timestamp: 0, + current_version: 0, + compatibility_version: 0, + } + + mc.AddLoadCmd(&dylib_lcmd) +} + +func (mc *MachoContext) AddRPath(rpath string) { + if mc.RPathExisted(rpath) { + log.WithFields(log.Fields{ + "rpath": rpath, + }).Debug("Adding rpath but existed") + return + } + path := []byte(rpath) + path = append(path, 0) + rpath_lcmd := RPath{ + c: LoadCmd{cmd: LC_RPATH, cmdsize: 0}, + offset: 12, + path: path, + } + mc.AddLoadCmd(&rpath_lcmd) +} + +func (mc *MachoContext) UpdateHeaderAddLcmd(size uint32) { + if mc.WriteEnabled() { + mc.header.ncmds += 1 + mc.header.sizeofcmds += size + mc.file.WriteAt(mc.header.Serialize(mc), 0) + } +} + +func (mc *MachoContext) UpdateHeaderRemoveLcmd(size uint32) { + if mc.WriteEnabled() { + mc.header.ncmds -= 1 + mc.header.sizeofcmds -= size + mc.file.WriteAt(mc.header.Serialize(mc), 0) + } +} diff --git a/macho-go/pkg/ios/macho/error.go b/macho-go/pkg/ios/macho/error.go new file mode 100644 index 0000000..4963aa3 --- /dev/null +++ b/macho-go/pkg/ios/macho/error.go @@ -0,0 +1,13 @@ +package macho + +import ( + "fmt" +) + +type ParseError struct { + Expect string +} + +func (e *ParseError) Error() string { + return fmt.Sprintf("Parse Error: %s", e.Expect) +} diff --git a/macho-go/pkg/ios/macho/lc_segment.go b/macho-go/pkg/ios/macho/lc_segment.go new file mode 100644 index 0000000..96632e9 --- /dev/null +++ b/macho-go/pkg/ios/macho/lc_segment.go @@ -0,0 +1,384 @@ +package macho + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +type Section interface { + // SegName() []byte + SectName() []byte + Addr() uint64 + Size() uint64 + Offset() uint32 + Type() uint8 + Attribute() uint32 +} + +type Segment interface { + SegName() []byte + Vmaddr() uint64 + Vmsize() uint64 + Fileoff() uint64 + Filesize() uint64 + Flags() uint32 + Sections() []Section +} + +type Segment32 struct { + c LoadCmd + segname []byte + vmaddr uint32 + vmsize uint32 + fileoff uint32 + filesize uint32 + maxprot uint32 + initprot uint32 + nsects uint32 + flags uint32 + sections []*Section32 +} + +func (lcmd *Segment32) Cmd() uint32 { + return lcmd.c.Cmd() +} + +func (lcmd *Segment32) Cmdsize() uint32 { + segment_size := uint32(56) + section_size := uint32(4*9 + 16*2) + return segment_size + section_size*lcmd.nsects +} + +func (lcmd *Segment32) Cmdname() string { + var s string + s += fmt.Sprintf( + "%s %s vm[vmaddr:0x%x vmsize:0x%x] [fileoff:0x%x filesize:0x%x]", + lcmd.c.Cmdname(), + string(lcmd.segname), + lcmd.vmaddr, + lcmd.vmsize, + lcmd.fileoff, + lcmd.filesize, + ) + for i, section := range lcmd.sections { + s += fmt.Sprintf("\n %d. %s [addr:0x%x size:0x%x offset:0x%x]", + i, string(section.sectname), + section.addr, section.size, section.offset, + ) + } + return s +} + +func (lcmd *Segment32) SegName() []byte { + return lcmd.segname +} +func (lcmd *Segment32) Vmaddr() uint64 { + return uint64(lcmd.vmaddr) +} +func (lcmd *Segment32) Vmsize() uint64 { + return uint64(lcmd.vmaddr) +} +func (lcmd *Segment32) Fileoff() uint64 { + return uint64(lcmd.fileoff) +} +func (lcmd *Segment32) Filesize() uint64 { + return uint64(lcmd.filesize) +} +func (lcmd *Segment32) Flags() uint32 { + return lcmd.flags +} +func (lcmd *Segment32) Sections() []Section { + var sections []Section + for _, section := range lcmd.sections { + sections = append(sections, section) + } + return sections +} + +func (lcmd *Segment32) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + buf.Write(lcmd.segname) + binary.Write(buf, mc.byteorder, lcmd.vmaddr) + binary.Write(buf, mc.byteorder, lcmd.vmsize) + binary.Write(buf, mc.byteorder, lcmd.fileoff) + binary.Write(buf, mc.byteorder, lcmd.filesize) + binary.Write(buf, mc.byteorder, lcmd.maxprot) + binary.Write(buf, mc.byteorder, lcmd.initprot) + binary.Write(buf, mc.byteorder, lcmd.nsects) + binary.Write(buf, mc.byteorder, lcmd.flags) + for _, section := range lcmd.sections { + binary.Write(buf, mc.byteorder, section.Serialize(mc)) + } + return buf.Bytes() +} + +func (lcmd *Segment32) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.c.cmd) + binary.Read(r, mc.byteorder, &lcmd.c.cmdsize) + lcmd.segname = r.Next(16) + binary.Read(r, mc.byteorder, &lcmd.vmaddr) + binary.Read(r, mc.byteorder, &lcmd.vmsize) + binary.Read(r, mc.byteorder, &lcmd.fileoff) + binary.Read(r, mc.byteorder, &lcmd.filesize) + binary.Read(r, mc.byteorder, &lcmd.maxprot) + binary.Read(r, mc.byteorder, &lcmd.initprot) + binary.Read(r, mc.byteorder, &lcmd.nsects) + binary.Read(r, mc.byteorder, &lcmd.flags) + + for i := uint32(0); i < lcmd.nsects; i++ { + section_buf := make([]byte, 68) + r.Read(section_buf) + + var section Section32 + section.Deserialize(mc, section_buf) + lcmd.sections = append(lcmd.sections, §ion) + } +} + +type Segment64 struct { + c LoadCmd + segname []byte + vmaddr uint64 + vmsize uint64 + fileoff uint64 + filesize uint64 + maxprot uint32 + initprot uint32 + nsects uint32 + flags uint32 + sections []*Section64 +} + +func (lcmd *Segment64) Cmd() uint32 { + return lcmd.c.Cmd() +} + +func (lcmd *Segment64) Cmdsize() uint32 { + segment_size := uint32(72) + section_size := uint32(8*2 + 4*8 + 16*2) + return segment_size + section_size*lcmd.nsects +} + +func (lcmd *Segment64) Cmdname() string { + var s string + s += fmt.Sprintf( + "%s %s vm[vmaddr:0x%x vmsize:0x%x] [fileoff:0x%x filesize:0x%x]", + lcmd.c.Cmdname(), + string(lcmd.segname), + lcmd.vmaddr, + lcmd.vmsize, + lcmd.fileoff, + lcmd.filesize, + ) + for i, section := range lcmd.sections { + s += fmt.Sprintf("\n %d. %s [addr:0x%x size:0x%x offset:0x%x]", + i, string(section.sectname), + section.addr, section.size, section.offset, + ) + } + return s +} + +func (lcmd *Segment64) SegName() []byte { + return lcmd.segname +} +func (lcmd *Segment64) Vmaddr() uint64 { + return lcmd.vmaddr +} +func (lcmd *Segment64) Vmsize() uint64 { + return lcmd.vmaddr +} +func (lcmd *Segment64) Fileoff() uint64 { + return lcmd.fileoff +} +func (lcmd *Segment64) Filesize() uint64 { + return lcmd.filesize +} +func (lcmd *Segment64) Flags() uint32 { + return lcmd.flags +} +func (lcmd *Segment64) Sections() []Section { + var sections []Section + for _, section := range lcmd.sections { + sections = append(sections, section) + } + return sections +} + +func (lcmd *Segment64) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + buf.Write(lcmd.segname) + binary.Write(buf, mc.byteorder, lcmd.vmaddr) + binary.Write(buf, mc.byteorder, lcmd.vmsize) + binary.Write(buf, mc.byteorder, lcmd.fileoff) + binary.Write(buf, mc.byteorder, lcmd.filesize) + binary.Write(buf, mc.byteorder, lcmd.maxprot) + binary.Write(buf, mc.byteorder, lcmd.initprot) + binary.Write(buf, mc.byteorder, lcmd.nsects) + binary.Write(buf, mc.byteorder, lcmd.flags) + for _, section := range lcmd.sections { + binary.Write(buf, mc.byteorder, section.Serialize(mc)) + } + return buf.Bytes() +} + +func (lcmd *Segment64) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.c.cmd) + binary.Read(r, mc.byteorder, &lcmd.c.cmdsize) + lcmd.segname = r.Next(16) + binary.Read(r, mc.byteorder, &lcmd.vmaddr) + binary.Read(r, mc.byteorder, &lcmd.vmsize) + binary.Read(r, mc.byteorder, &lcmd.fileoff) + binary.Read(r, mc.byteorder, &lcmd.filesize) + binary.Read(r, mc.byteorder, &lcmd.maxprot) + binary.Read(r, mc.byteorder, &lcmd.initprot) + binary.Read(r, mc.byteorder, &lcmd.nsects) + binary.Read(r, mc.byteorder, &lcmd.flags) + + for i := uint32(0); i < lcmd.nsects; i++ { + section_buf := make([]byte, 80) + r.Read(section_buf) + + var section Section64 + section.Deserialize(mc, section_buf) + lcmd.sections = append(lcmd.sections, §ion) + } +} + +type Section32 struct { + sectname []byte + segname []byte + addr uint32 + size uint32 + offset uint32 + align uint32 + reloff uint32 + nreloc uint32 + flags uint32 + reserved1 uint32 + reserved2 uint32 +} + +func (sec *Section32) SectName() []byte { + return sec.sectname +} +func (sec *Section32) Addr() uint64 { + return uint64(sec.addr) +} +func (sec *Section32) Size() uint64 { + return uint64(sec.size) +} +func (sec *Section32) Offset() uint32 { + return sec.offset +} +func (sec *Section32) Type() uint8 { + return uint8(sec.flags & 0xff) +} +func (sec *Section32) Attribute() uint32 { + return sec.flags & 0xffffff00 +} + +func (section *Section32) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + buf.Write(section.sectname) + buf.Write(section.segname) + binary.Write(buf, mc.byteorder, section.addr) + binary.Write(buf, mc.byteorder, section.size) + binary.Write(buf, mc.byteorder, section.offset) + binary.Write(buf, mc.byteorder, section.align) + binary.Write(buf, mc.byteorder, section.reloff) + binary.Write(buf, mc.byteorder, section.nreloc) + binary.Write(buf, mc.byteorder, section.flags) + binary.Write(buf, mc.byteorder, section.reserved1) + binary.Write(buf, mc.byteorder, section.reserved2) + return buf.Bytes() +} + +func (section *Section32) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + section.sectname = r.Next(16) + section.segname = r.Next(16) + binary.Read(r, mc.byteorder, §ion.addr) + binary.Read(r, mc.byteorder, §ion.size) + binary.Read(r, mc.byteorder, §ion.offset) + binary.Read(r, mc.byteorder, §ion.align) + binary.Read(r, mc.byteorder, §ion.reloff) + binary.Read(r, mc.byteorder, §ion.nreloc) + binary.Read(r, mc.byteorder, §ion.flags) + binary.Read(r, mc.byteorder, §ion.reserved1) + binary.Read(r, mc.byteorder, §ion.reserved2) +} + +type Section64 struct { + sectname []byte + segname []byte + addr uint64 + size uint64 + offset uint32 + align uint32 + reloff uint32 + nreloc uint32 + flags uint32 + reserved1 uint32 + reserved2 uint32 + reserved3 uint32 +} + +func (sec *Section64) SectName() []byte { + return sec.sectname +} +func (sec *Section64) Addr() uint64 { + return sec.addr +} +func (sec *Section64) Size() uint64 { + return sec.size +} +func (sec *Section64) Offset() uint32 { + return sec.offset +} +func (sec *Section64) Type() uint8 { + return uint8(sec.flags & 0xff) +} +func (sec *Section64) Attribute() uint32 { + return sec.flags & 0xffffff00 +} + +func (section *Section64) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + buf.Write(section.sectname) + buf.Write(section.segname) + binary.Write(buf, mc.byteorder, section.addr) + binary.Write(buf, mc.byteorder, section.size) + binary.Write(buf, mc.byteorder, section.offset) + binary.Write(buf, mc.byteorder, section.align) + binary.Write(buf, mc.byteorder, section.reloff) + binary.Write(buf, mc.byteorder, section.nreloc) + binary.Write(buf, mc.byteorder, section.flags) + binary.Write(buf, mc.byteorder, section.reserved1) + binary.Write(buf, mc.byteorder, section.reserved2) + binary.Write(buf, mc.byteorder, section.reserved3) + return buf.Bytes() +} + +func (section *Section64) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + section.sectname = r.Next(16) + section.segname = r.Next(16) + binary.Read(r, mc.byteorder, §ion.addr) + binary.Read(r, mc.byteorder, §ion.size) + binary.Read(r, mc.byteorder, §ion.offset) + binary.Read(r, mc.byteorder, §ion.align) + binary.Read(r, mc.byteorder, §ion.reloff) + binary.Read(r, mc.byteorder, §ion.nreloc) + binary.Read(r, mc.byteorder, §ion.flags) + binary.Read(r, mc.byteorder, §ion.reserved1) + binary.Read(r, mc.byteorder, §ion.reserved2) + binary.Read(r, mc.byteorder, §ion.reserved3) +} diff --git a/macho-go/pkg/ios/macho/load_commands.go b/macho-go/pkg/ios/macho/load_commands.go new file mode 100644 index 0000000..302da62 --- /dev/null +++ b/macho-go/pkg/ios/macho/load_commands.go @@ -0,0 +1,483 @@ +package macho + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + + . "ios-wrapper/pkg/ios" +) + +// A Serializable interface +// Serialize to bytes and Deserialize to struct +// with MachoContext to know the byteorder +type Serializable interface { + Serialize(mc *MachoContext) []byte + Deserialize(mc *MachoContext, buf []byte) +} + +// macho_header and macho_header64 +type Header struct { + magic uint32 + cputype uint32 + cpusubtype uint32 + filetype uint32 + ncmds uint32 + sizeofcmds uint32 + flags uint32 + reserved uint32 +} + +func (h *Header) Cputype() uint32 { + return h.cputype +} + +func (h *Header) Cpusubtype() uint32 { + return h.cpusubtype +} + +func (h *Header) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, h.magic) + binary.Write(buf, mc.byteorder, h.cputype) + binary.Write(buf, mc.byteorder, h.cpusubtype) + binary.Write(buf, mc.byteorder, h.filetype) + binary.Write(buf, mc.byteorder, h.ncmds) + binary.Write(buf, mc.byteorder, h.sizeofcmds) + binary.Write(buf, mc.byteorder, h.flags) + if mc.Is64bit() { + binary.Write(buf, mc.byteorder, h.reserved) + } + return buf.Bytes() +} + +func (h *Header) Deserialize(mc *MachoContext, r *bufio.Reader) { + binary.Read(r, mc.byteorder, &h.magic) + binary.Read(r, mc.byteorder, &h.cputype) + binary.Read(r, mc.byteorder, &h.cpusubtype) + binary.Read(r, mc.byteorder, &h.filetype) + binary.Read(r, mc.byteorder, &h.ncmds) + binary.Read(r, mc.byteorder, &h.sizeofcmds) + binary.Read(r, mc.byteorder, &h.flags) + if mc.Is64bit() { + binary.Read(r, mc.byteorder, &h.reserved) + } +} + +type LoadCommand interface { + Serializable + Cmd() uint32 + Cmdsize() uint32 + Cmdname() string +} + +// Stores the common value of load command +// Raw stores the rest of the command +// where no struct defined for cmd +type LoadCmd struct { + cmd uint32 + cmdsize uint32 + Raw []byte +} + +func (lcmd *LoadCmd) Cmd() uint32 { + return lcmd.cmd +} + +func (lcmd *LoadCmd) Cmdsize() uint32 { + return lcmd.cmdsize +} + +func (lcmd *LoadCmd) Cmdname() string { + switch lcmd.cmd { + case LC_RPATH: + return "LC_RPATH" + case LC_DYLD_INFO: + return "LC_DYLD_INFO" + case LC_DYLD_INFO_ONLY: + return "LC_DYLD_INFO_ONLY" + case LC_CODE_SIGNATURE: + return "LC_CODE_SIGNATURE" + case LC_LAZY_LOAD_DYLIB: + return "LC_LAZY_LOAD_DYLIB" + case LC_LOAD_DYLIB: + return "LC_LOAD_DYLIB" + case LC_ID_DYLIB: + return "LC_ID_DYLIB" + case LC_REEXPORT_DYLIB: + return "LC_REEXPORT_DYLIB" + case LC_ENCRYPTION_INFO: + return "LC_ENCRYPTION_INFO" + case LC_ENCRYPTION_INFO_64: + return "LC_ENCRYPTION_INFO_64" + case LC_DYSYMTAB: + return "LC_DYSYMTAB" + case LC_LOAD_WEAK_DYLIB: + return "LC_LOAD_WEAK_DYLIB" + case LC_SEGMENT: + return "LC_SEGMENT" + case LC_SEGMENT_64: + return "LC_SEGMENT_64" + case LC_MAIN: + return "LC_MAIN" + case LC_FUNCTION_STARTS: + return "LC_FUNCTION_STARTS" + case LC_DATA_IN_CODE: + return "LC_DATA_IN_CODE" + case LC_SYMTAB: + return "LC_SYMTAB" + default: + // TODO: Update + return fmt.Sprintf("LC_DONT_KNOW_0x%x", lcmd.Cmd()) + } +} + +func (lcmd *LoadCmd) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + binary.Write(buf, mc.byteorder, lcmd.Raw) + return buf.Bytes() +} + +func (lcmd *LoadCmd) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.cmd) + binary.Read(r, mc.byteorder, &lcmd.cmdsize) + lcmd.Raw = make([]byte, lcmd.cmdsize-8) + r.Read(lcmd.Raw) +} + +type RPath struct { + c LoadCmd + offset uint32 + path []byte +} + +func (lcmd *RPath) Cmd() uint32 { + return LC_RPATH +} + +func (lcmd *RPath) Cmdsize() uint32 { + raw_cmd_size := 8 + 4 + len(lcmd.path) + 1 + if raw_cmd_size%8 == 0 { + return uint32(raw_cmd_size) + } + padded_cmd_size := raw_cmd_size + (8 - (raw_cmd_size % 8)) + return uint32(padded_cmd_size) +} + +func (lcmd *RPath) Cmdname() string { + return fmt.Sprintf("%s %s", lcmd.c.Cmdname(), string(lcmd.path)) +} + +func (lcmd *RPath) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + binary.Write(buf, mc.byteorder, lcmd.offset) + binary.Write(buf, mc.byteorder, []byte(lcmd.path)) + buf.WriteByte(0) + if buf.Len()%8 != 0 { + bytes_padded := 8 - (buf.Len() % 8) + binary.Write(buf, mc.byteorder, make([]byte, bytes_padded)) + } + return buf.Bytes() +} + +func (lcmd *RPath) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.c.cmd) + binary.Read(r, mc.byteorder, &lcmd.c.cmdsize) + binary.Read(r, mc.byteorder, &lcmd.offset) + lcmd.path, _ = r.ReadBytes(0) + lcmd.path = bytes.Trim(lcmd.path, "\x00") +} + +type Dylib struct { + c LoadCmd + nameoff uint32 + name []byte + timestamp uint32 + current_version uint32 + compatibility_version uint32 +} + +func (lcmd *Dylib) Cmd() uint32 { + return lcmd.c.Cmd() +} + +func (lcmd *Dylib) Cmdsize() uint32 { + raw_cmd_size := 8 + 16 + len(lcmd.name) + 1 + if raw_cmd_size%8 == 0 { + return uint32(raw_cmd_size) + } + padded_cmd_size := raw_cmd_size + (8 - (raw_cmd_size % 8)) + return uint32(padded_cmd_size) +} + +func (lcmd *Dylib) Cmdname() string { + return fmt.Sprintf("%s %s", lcmd.c.Cmdname(), string(lcmd.name)) +} + +func (lcmd *Dylib) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + binary.Write(buf, mc.byteorder, lcmd.nameoff) + binary.Write(buf, mc.byteorder, lcmd.timestamp) + binary.Write(buf, mc.byteorder, lcmd.current_version) + binary.Write(buf, mc.byteorder, lcmd.compatibility_version) + binary.Write(buf, mc.byteorder, lcmd.name) + buf.WriteByte(0) + + // size must align with 8 + if buf.Len()%8 != 0 { + bytes_padded := 8 - (buf.Len() % 8) + binary.Write(buf, mc.byteorder, make([]byte, bytes_padded)) + } + return buf.Bytes() +} + +func (lcmd *Dylib) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.c.cmd) + binary.Read(r, mc.byteorder, &lcmd.c.cmdsize) + binary.Read(r, mc.byteorder, &lcmd.nameoff) + binary.Read(r, mc.byteorder, &lcmd.timestamp) + binary.Read(r, mc.byteorder, &lcmd.current_version) + binary.Read(r, mc.byteorder, &lcmd.compatibility_version) + lcmd.name, _ = r.ReadBytes(0) + lcmd.name = bytes.Trim(lcmd.name, "\x00") +} + +type EncryptionInfo struct { + c LoadCmd + cryptoff uint32 + cryptsize uint32 + cryptid uint32 + pad uint32 +} + +func (lcmd *EncryptionInfo) Cmd() uint32 { + return lcmd.c.Cmd() +} + +func (lcmd *EncryptionInfo) Cmdsize() uint32 { + if lcmd.Cmd() == LC_ENCRYPTION_INFO { + return uint32(8 + 4*3) + } else { + return uint32(8 + 4*4) + } +} + +func (lcmd *EncryptionInfo) Cmdname() string { + return fmt.Sprintf("%s start:0x%x size:0x%x encrypted:%d", + lcmd.c.Cmdname(), lcmd.cryptoff, lcmd.cryptsize, lcmd.cryptid) +} + +func (lcmd *EncryptionInfo) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + binary.Write(buf, mc.byteorder, lcmd.cryptoff) + binary.Write(buf, mc.byteorder, lcmd.cryptsize) + binary.Write(buf, mc.byteorder, lcmd.cryptid) + if mc.Is64bit() { + binary.Write(buf, mc.byteorder, lcmd.pad) + } + return buf.Bytes() +} + +func (lcmd *EncryptionInfo) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.c.cmd) + binary.Read(r, mc.byteorder, &lcmd.c.cmdsize) + binary.Read(r, mc.byteorder, &lcmd.cryptoff) + binary.Read(r, mc.byteorder, &lcmd.cryptsize) + binary.Read(r, mc.byteorder, &lcmd.cryptid) + if mc.Is64bit() { + binary.Read(r, mc.byteorder, &lcmd.pad) + } +} + +type EntryPoint struct { + c LoadCmd + entryoff uint64 + stacksize uint64 +} + +func (lcmd *EntryPoint) Cmd() uint32 { + return lcmd.c.Cmd() +} + +func (lcmd *EntryPoint) Cmdsize() uint32 { + return uint32(8 + 8*2) +} + +func (lcmd *EntryPoint) Cmdname() string { + return lcmd.c.Cmdname() +} + +func (lcmd *EntryPoint) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + binary.Write(buf, mc.byteorder, lcmd.entryoff) + binary.Write(buf, mc.byteorder, lcmd.stacksize) + return buf.Bytes() +} + +func (lcmd *EntryPoint) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.c.cmd) + binary.Read(r, mc.byteorder, &lcmd.c.cmdsize) + binary.Read(r, mc.byteorder, &lcmd.entryoff) + binary.Read(r, mc.byteorder, &lcmd.stacksize) +} + +type DyldInfo struct { + c LoadCmd + rebase_off uint32 + rebase_size uint32 + bind_off uint32 + bind_size uint32 + weak_bind_off uint32 + weak_bind_size uint32 + lazy_bind_off uint32 + lazy_bind_size uint32 + export_off uint32 + export_size uint32 +} + +func (lcmd *DyldInfo) Cmd() uint32 { + return lcmd.c.Cmd() +} + +func (lcmd *DyldInfo) Cmdsize() uint32 { + return uint32(8 + 4*10) +} + +func (lcmd *DyldInfo) Cmdname() string { + return lcmd.c.Cmdname() +} + +func (lcmd *DyldInfo) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + binary.Write(buf, mc.byteorder, lcmd.rebase_off) + binary.Write(buf, mc.byteorder, lcmd.rebase_size) + binary.Write(buf, mc.byteorder, lcmd.bind_off) + binary.Write(buf, mc.byteorder, lcmd.bind_size) + binary.Write(buf, mc.byteorder, lcmd.weak_bind_off) + binary.Write(buf, mc.byteorder, lcmd.weak_bind_size) + binary.Write(buf, mc.byteorder, lcmd.lazy_bind_off) + binary.Write(buf, mc.byteorder, lcmd.lazy_bind_size) + binary.Write(buf, mc.byteorder, lcmd.export_off) + binary.Write(buf, mc.byteorder, lcmd.export_size) + return buf.Bytes() +} + +func (lcmd *DyldInfo) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.c.cmd) + binary.Read(r, mc.byteorder, &lcmd.c.cmdsize) + binary.Read(r, mc.byteorder, &lcmd.rebase_off) + binary.Read(r, mc.byteorder, &lcmd.rebase_size) + binary.Read(r, mc.byteorder, &lcmd.bind_off) + binary.Read(r, mc.byteorder, &lcmd.bind_size) + binary.Read(r, mc.byteorder, &lcmd.weak_bind_off) + binary.Read(r, mc.byteorder, &lcmd.weak_bind_size) + binary.Read(r, mc.byteorder, &lcmd.lazy_bind_off) + binary.Read(r, mc.byteorder, &lcmd.lazy_bind_size) + binary.Read(r, mc.byteorder, &lcmd.export_off) + binary.Read(r, mc.byteorder, &lcmd.export_size) +} + +type LinkEdit struct { + c LoadCmd + dataoff uint32 + datasize uint32 +} + +func (lcmd *LinkEdit) Dataoff() uint32 { + return lcmd.dataoff +} + +func (lcmd *LinkEdit) Datasize() uint32 { + return lcmd.datasize +} + +func (lcmd *LinkEdit) Cmd() uint32 { + return lcmd.c.Cmd() +} + +func (lcmd *LinkEdit) Cmdsize() uint32 { + return uint32(8 + 4*2) +} + +func (lcmd *LinkEdit) Cmdname() string { + return lcmd.c.Cmdname() +} + +func (lcmd *LinkEdit) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + binary.Write(buf, mc.byteorder, lcmd.dataoff) + binary.Write(buf, mc.byteorder, lcmd.datasize) + return buf.Bytes() +} + +func (lcmd *LinkEdit) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.c.cmd) + binary.Read(r, mc.byteorder, &lcmd.c.cmdsize) + binary.Read(r, mc.byteorder, &lcmd.dataoff) + binary.Read(r, mc.byteorder, &lcmd.datasize) +} + +type Symtab struct { + c LoadCmd + symoff uint32 + nsyms uint32 + stroff uint32 + strsize uint32 +} + +func (lcmd *Symtab) Cmd() uint32 { + return lcmd.c.Cmd() +} + +func (lcmd *Symtab) Cmdsize() uint32 { + return uint32(8 + 4*4) +} + +func (lcmd *Symtab) Cmdname() string { + return fmt.Sprintf("%s symoff:0x%x nsyms:%d stroff:0x%x strsize:0x%x", + lcmd.c.Cmdname(), lcmd.symoff, lcmd.nsyms, lcmd.stroff, lcmd.strsize) +} + +func (lcmd *Symtab) Serialize(mc *MachoContext) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, mc.byteorder, lcmd.Cmd()) + binary.Write(buf, mc.byteorder, lcmd.Cmdsize()) + binary.Write(buf, mc.byteorder, lcmd.symoff) + binary.Write(buf, mc.byteorder, lcmd.nsyms) + binary.Write(buf, mc.byteorder, lcmd.stroff) + binary.Write(buf, mc.byteorder, lcmd.strsize) + return buf.Bytes() +} + +func (lcmd *Symtab) Deserialize(mc *MachoContext, buf []byte) { + r := bytes.NewBuffer(buf) + binary.Read(r, mc.byteorder, &lcmd.c.cmd) + binary.Read(r, mc.byteorder, &lcmd.c.cmdsize) + + binary.Read(r, mc.byteorder, &lcmd.symoff) + binary.Read(r, mc.byteorder, &lcmd.nsyms) + binary.Read(r, mc.byteorder, &lcmd.stroff) + binary.Read(r, mc.byteorder, &lcmd.strsize) +} diff --git a/macho-go/pkg/ios/macho/macho.go b/macho-go/pkg/ios/macho/macho.go new file mode 100644 index 0000000..aab72f1 --- /dev/null +++ b/macho-go/pkg/ios/macho/macho.go @@ -0,0 +1,265 @@ +package macho + +import ( + "bufio" + "bytes" + "encoding/binary" + "io" + "os" + + log "github.com/sirupsen/logrus" + + . "ios-wrapper/pkg/ios" +) + +// A Mach-O binary context +// holds byteorder, pointersize, header +// And load commands +type MachoContext struct { + file *os.File // backed file enabled write + buf []byte // backed up data + byteorder binary.ByteOrder + pointersize uint32 + entryoff uint64 + + header Header + commands []LoadCommand + rpaths []*RPath + dylibs []*Dylib + linkedits []*LinkEdit + segments []Segment + symtab *Symtab + dyldinfo *DyldInfo +} + +func (mc *MachoContext) FileSize() uint32 { + return uint32(len(mc.buf)) +} + +func (mc *MachoContext) Header() *Header { + return &mc.header +} + +func (mc *MachoContext) Commands() []LoadCommand { + return mc.commands +} + +func (mc *MachoContext) Rpaths() []*RPath { + return mc.rpaths +} + +func (mc *MachoContext) Dylibs() []*Dylib { + return mc.dylibs +} + +func (mc *MachoContext) Linkedits() []*LinkEdit { + return mc.linkedits +} + +func (mc *MachoContext) Segments() []Segment { + return mc.segments +} + +func (mc *MachoContext) WriteEnabled() bool { + return mc.file != nil +} + +func (mc *MachoContext) WriteBufferTo(w io.Writer) (int, error) { + return w.Write(mc.buf) +} + +// Parse the provided Mach-O binary from a file +// The first 4 bytes of the file must be the MachO magic +// That is: +// file.Seek(0, io.SeekStart) +// magic := make([]byte, 4) +// file.Read(magic) +// assert magic == []byte{macho magic bytes} +// or else, parsing error is panic +func (mc *MachoContext) ParseFile(file *os.File, length int) error { + file.Seek(0, io.SeekStart) + mc.file = file + + // save the file content to buf + if length == 0 { + last, _ := mc.file.Seek(0, io.SeekEnd) + mc.buf = make([]byte, last) + mc.file.Seek(0, io.SeekStart) + } else { + mc.buf = make([]byte, length) + } + + if _, err := io.ReadFull(mc.file, mc.buf); err != nil { + return err + } + mc.file.Seek(0, io.SeekStart) // reset peek, safety reason + r := bufio.NewReader(file) + return mc.Parse(r) +} + +// A macho context can be made from a buffer +// In this case write is disable +// Every information accessed will be through +// this byte buffer +func (mc *MachoContext) ParseBuffer(buf []byte) error { + mc.file = nil + mc.buf = buf + r := bytes.NewReader(buf) + rr := bufio.NewReader(r) + return mc.Parse(rr) +} + +// Parse the provided Mach-O binary from a buffer +func (mc *MachoContext) Parse(r *bufio.Reader) error { + { // read magic to define byteorder and pointersize + var magic uint32 + magic_buf, _ := r.Peek(4) + magic_r := bytes.NewReader(magic_buf) + binary.Read(magic_r, binary.LittleEndian, &magic) + + if magic != Magic32 && magic != Magic64 && magic != Cigam32 && + magic != Cigam64 { + log.WithFields(log.Fields{ + "magic": magic, + }).Error("Magic for Mach-O does not match") + return &ParseError{ + Expect: "Magic for Mach-O does not match", + } + } + + if magic == Magic32 || magic == Magic64 { + mc.byteorder = binary.LittleEndian + } else { + mc.byteorder = binary.BigEndian + } + + if magic == Magic32 { + mc.pointersize = 4 + } else { + mc.pointersize = 8 + } + } + + mc.header.Deserialize(mc, r) + + for i := uint32(0); i < mc.header.ncmds; i++ { + var cmd uint32 + var cmdsize uint32 + load_buf, _ := r.Peek(8) + load_r := bytes.NewReader(load_buf) + + binary.Read(load_r, mc.byteorder, &cmd) + binary.Read(load_r, mc.byteorder, &cmdsize) + log.WithFields(log.Fields{ + "nth": i, + "cmd": cmd, + "cmdsize": cmdsize, + }).Trace("Load command") + + command_buf := make([]byte, cmdsize) + if bytesread, err := io.ReadFull(r, command_buf); err != nil || + uint32(bytesread) != cmdsize { + log.WithFields(log.Fields{ + "nth": i, + "cmdsize": cmdsize, + "bytesread": bytesread, + }).Error("Cannot read load command") + return &ParseError{ + Expect: "Cannot read load command", + } + } + + switch cmd { + case LC_RPATH: + lcmd := new(RPath) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + mc.rpaths = append(mc.rpaths, lcmd) + break + + case LC_ID_DYLIB, + LC_LOAD_DYLIB, + LC_LAZY_LOAD_DYLIB, + LC_REEXPORT_DYLIB, + LC_LOAD_WEAK_DYLIB: + lcmd := new(Dylib) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + if cmd != LC_ID_DYLIB { + mc.dylibs = append(mc.dylibs, lcmd) + } + break + + case LC_ENCRYPTION_INFO, LC_ENCRYPTION_INFO_64: + lcmd := new(EncryptionInfo) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + break + + case LC_SEGMENT: + lcmd := new(Segment32) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + mc.segments = append(mc.segments, lcmd) + break + + case LC_SEGMENT_64: + lcmd := new(Segment64) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + mc.segments = append(mc.segments, lcmd) + break + + case LC_MAIN: + lcmd := new(EntryPoint) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + mc.entryoff = lcmd.entryoff + break + + case LC_DYLD_INFO, LC_DYLD_INFO_ONLY: + lcmd := new(DyldInfo) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + mc.dyldinfo = lcmd + break + + case LC_FUNCTION_STARTS, LC_DATA_IN_CODE, LC_CODE_SIGNATURE, LC_DYLD_CHAINED_FIXUPS, LC_DYLD_EXPORTS_TRIE: + lcmd := new(LinkEdit) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + mc.linkedits = append(mc.linkedits, lcmd) + break + + case LC_SYMTAB: + lcmd := new(Symtab) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + mc.symtab = lcmd + + default: + lcmd := new(LoadCmd) + lcmd.Deserialize(mc, command_buf) + mc.commands = append(mc.commands, lcmd) + break + } + } + return nil +} + +func (mc *MachoContext) Is64bit() bool { + return mc.pointersize == 8 +} + +func (mc *MachoContext) PointerSize() uint32 { + return mc.pointersize +} + +func (mc *MachoContext) ImageBase() uint64 { + for _, segment := range mc.segments { + if segment.Fileoff() == 0 && segment.Filesize() != 0 { + return segment.Vmaddr() + } + } + return 0 +} diff --git a/macho-go/pkg/ios/macho/symtab.go b/macho-go/pkg/ios/macho/symtab.go new file mode 100644 index 0000000..a18c9d8 --- /dev/null +++ b/macho-go/pkg/ios/macho/symtab.go @@ -0,0 +1,111 @@ +package macho + +import ( + "bytes" + "encoding/binary" + "io" +) + +const ( + N_STAB uint8 = 0xe0 /* if any of these bits set, a symbolic debugging entry */ + N_PEXT uint8 = 0x10 /* private external symbol bit */ + N_TYPE uint8 = 0x0e /* mask for the type bits */ + N_EXT uint8 = 0x01 /* external symbol bit, set for external symbols */ + + // Values for N_TYPE bits of the n_type field. + N_UNDF uint8 = 0x0 /* undefined, n_sect == NO_SECT */ + N_ABS uint8 = 0x2 /* absolute, n_sect == NO_SECT */ + N_SECT uint8 = 0xe /* defined in section number n_sect */ + N_PBUD uint8 = 0xc /* prebound undefined (defined in a dylib) */ + N_INDR uint8 = 0xa /* indirect */ + + NO_SECT uint8 = 0 /* symbol is not in any section */ + +) + +type Symbol struct { + name string + address uint64 +} + +func (sym *Symbol) Name() string { + return sym.name +} + +func (sym *Symbol) Address() uint64 { + return sym.address +} + +func (mc *MachoContext) CollectSymbols() []*Symbol { + // for referencing section + // sections := []Section{} + // for _, segment := range mc.segments { + // for _, section := range segment.Sections() { + // sections = append(sections, section) + // } + // } + + symbols := []*Symbol{} + + if mc.symtab.nsyms == 0 { + return []*Symbol{} + } + symtab_buffer := func() *bytes.Buffer { + start := mc.symtab.symoff + if mc.Is64bit() { + end := start + mc.symtab.nsyms*16 + return bytes.NewBuffer(mc.buf[start:end]) + } else { + end := start + mc.symtab.nsyms*12 + return bytes.NewBuffer(mc.buf[start:end]) + } + }() + + strtable := func() *bytes.Reader { + start := mc.symtab.stroff + end := start + mc.symtab.strsize + return bytes.NewReader(mc.buf[start:end]) + }() + + for i := uint32(0); i < mc.symtab.nsyms; i++ { + var strx uint32 + var flags uint8 + var sect uint8 + var desc uint16 + var value32 uint32 + var value64 uint64 + + binary.Read(symtab_buffer, mc.byteorder, &strx) + binary.Read(symtab_buffer, mc.byteorder, &flags) + binary.Read(symtab_buffer, mc.byteorder, §) + binary.Read(symtab_buffer, mc.byteorder, &desc) + if mc.Is64bit() { + binary.Read(symtab_buffer, mc.byteorder, &value64) + } else { + // always use value64 + binary.Read(symtab_buffer, mc.byteorder, &value32) + value64 = uint64(value32) + } + + mangled_name := func() string { + strtable.Seek(int64(strx), io.SeekStart) + name := bytes.NewBufferString("") + for { + b, _ := strtable.ReadByte() + if b == 0 { + break + } + name.WriteByte(b) + } + return name.String() + }() + + // fmt.Printf("0x%x %s\n", value64, mangled_name) + symbols = append(symbols, &Symbol{ + name: mangled_name, + address: value64, + }) + } + + return symbols +} diff --git a/macho-go/pkg/ios/macho/util.go b/macho-go/pkg/ios/macho/util.go new file mode 100644 index 0000000..6c73e72 --- /dev/null +++ b/macho-go/pkg/ios/macho/util.go @@ -0,0 +1,197 @@ +package macho + +import ( + "bytes" + "encoding/binary" + "fmt" + "os" + + log "github.com/sirupsen/logrus" + + . "ios-wrapper/pkg/ios" +) + +func (mc *MachoContext) ArchName() string { + if mc.header.cputype == 12 && mc.header.cpusubtype == 9 { + return "armv7" + } + if mc.header.cputype == 12 && mc.header.cpusubtype == 11 { + return "armv7s" + } + if mc.header.cputype == 0x100000C && mc.header.cpusubtype == 0 { + return "arm64" + } + if mc.header.cputype == 0x100000C && mc.header.cpusubtype == 2 { + return "arm64e" + } + if mc.header.cputype == 7 && mc.header.cpusubtype == 3 { + return "i386" + } + if mc.header.cputype == 0x1000007 && mc.header.cpusubtype == 3 { + return "x86_64" + } + if mc.header.cputype == 0x1000007 && mc.header.cpusubtype == 0x80000003 { + return "x86_64-lib64" + } + return fmt.Sprintf("arch-%x-%x", mc.header.cputype, mc.header.cpusubtype) +} + +func MachosFromFiles(files []string) ([]*MachoContext, error) { + var r []*MachoContext + for _, filename := range files { + var mc MachoContext + f, _ := os.OpenFile(filename, os.O_RDWR, 0644) + err := mc.ParseFile(f, 0) + if err != nil { + return nil, err + } + r = append(r, &mc) + } + return r, nil +} + +func CheckDuplicateArch(machos []*MachoContext) bool { + for i := 0; i < len(machos)-1; i++ { + for j := i + 1; j < len(machos); j++ { + i_cputype := machos[i].Header().Cputype() + j_cputype := machos[j].Header().Cputype() + i_cpusubtype := machos[i].Header().Cpusubtype() + j_cpusubtype := machos[j].Header().Cpusubtype() + if i_cputype == j_cputype && + i_cpusubtype == j_cpusubtype { + log.WithFields(log.Fields{ + "cputype": i_cputype, + "cpusubtype": i_cpusubtype, + }).Warn("Duplicate Mach-O Arch") + return true + } + } + } + return false +} + +func (mc *MachoContext) PaddingSize() uint64 { + if mc.Is64bit() { + return mc.entryoff - (Header_size_64 + uint64(mc.header.sizeofcmds)) + } else { + return mc.entryoff - (Header_size + uint64(mc.header.sizeofcmds)) + } + +} + +func (mc *MachoContext) DylibExisted(name string) bool { + // simple check + // Advanced check requires expansion of @rpath + for _, dylib := range mc.dylibs { + dylib_ := bytes.Trim(dylib.name, "\x00") + match := bytes.Compare(dylib_, []byte(name)) == 0 + log.WithFields(log.Fields{ + "left": dylib_, + "right": []byte(name), + "match": match, + }).Trace("Dylib check") + if match { + return true + } + } + return false +} + +func (mc *MachoContext) RPathExisted(path string) bool { + // simple check + // Advanced check requires expansion of @rpath + for _, rpath := range mc.rpaths { + rpath_ := bytes.Trim(rpath.path, "\x00") + match := bytes.Compare(rpath_, []byte(path)) == 0 + log.WithFields(log.Fields{ + "left": rpath_, + "right": []byte(path), + "match": match, + }).Trace("Rpath check") + if match { + return true + } + } + return false +} + +func (mc *MachoContext) FindSection(name string) Section { + for _, segment := range mc.segments { + for _, section := range segment.Sections() { + sectname := bytes.Trim(section.SectName(), "\x00") + if bytes.Compare(sectname, []byte(name)) == 0 { + return section + } + } + } + return nil +} + +func (mc *MachoContext) FindSegment(name string) Segment { + for _, segment := range mc.segments { + sectname := bytes.Trim(segment.SegName(), "\x00") + if bytes.Compare(sectname, []byte(name)) == 0 { + return segment + } + } + return nil +} + +func (mc *MachoContext) Cut(offset uint64, size uint64) []byte { + return mc.buf[offset : offset + size]; +} + +// INIT POINTER + +type InitPointer struct { + offset uint64 + low uint32 + high uint32 +} + +func (ptr *InitPointer) Offset() uint64 { + return ptr.offset +} + +func (ptr *InitPointer) Value() uint64 { + return (uint64(ptr.high) << 32) | uint64(ptr.low) +} + +func (ptr *InitPointer) String() string { + return fmt.Sprintf("0x%x -> 0x%x", + ptr.offset, (uint64(ptr.high)<<32)|uint64(ptr.low)) +} + +func (mc *MachoContext) InitFunctions() []InitPointer { + var valid_sections []Section + for _, segment := range mc.segments { + for _, section := range segment.Sections() { + if section.Type() == S_MOD_INIT_FUNC_POINTERS { + valid_sections = append(valid_sections, section) + } + } + } + + var funcs []InitPointer + for _, section := range valid_sections { + offset := uint64(section.Offset()) + size := section.Size() + + data := mc.buf[offset : offset+size] + + // TODO: assert len(data) // mc.pointersize + + count := size / uint64(mc.pointersize) + data_buf := bytes.NewBuffer(data) + for i := uint64(0); i < count; i++ { + var fun InitPointer + fun.offset = offset + i*uint64(mc.pointersize) + binary.Read(data_buf, mc.byteorder, &fun.low) + if mc.Is64bit() { + binary.Read(data_buf, mc.byteorder, &fun.high) + } + funcs = append(funcs, fun) + } + } + return funcs +} diff --git a/macho-go/pkg/leb128/leb128.go b/macho-go/pkg/leb128/leb128.go new file mode 100644 index 0000000..c3f4e6b --- /dev/null +++ b/macho-go/pkg/leb128/leb128.go @@ -0,0 +1,40 @@ +package leb128 + +import ( + "bytes" +) + +// Read unsigned leb128, no error +func ReadULEB(buf *bytes.Buffer) (uint, uint) { + result := uint(0) + shift := uint(0) + bytes_read := uint(0) + for { + b, _ := buf.ReadByte() + bytes_read += 1 + result |= uint(b&0b0111_1111) << shift + shift += 7 + if (b & 0b1000_0000) == 0 { + return result, bytes_read + } + } +} + +// Read signed leb128, no error +func ReadSLEB(buf *bytes.Buffer) (int, uint) { + result := int(0) + shift := uint(0) + bytes_read := uint(0) + for { + b, _ := buf.ReadByte() + bytes_read += 1 + result |= int(b&0b0111_1111) << shift + shift += 7 + if (b & 0b1000_0000) == 0 { + if b&0x40 != 0 { + return result | (-1 << shift), bytes_read + } + return result, bytes_read + } + } +} diff --git a/macho-go/pkg/version/version.go b/macho-go/pkg/version/version.go new file mode 100644 index 0000000..90b2cb5 --- /dev/null +++ b/macho-go/pkg/version/version.go @@ -0,0 +1,21 @@ +// +// Copyright 2020 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package version + +var ( + Version = "1.1" +) diff --git a/macho-go/proto/bcell_file.proto b/macho-go/proto/bcell_file.proto new file mode 100644 index 0000000..e892bb2 --- /dev/null +++ b/macho-go/proto/bcell_file.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +option optimize_for = LITE_RUNTIME; +option go_package = "pkg/protomodel"; + +import "config.proto"; +import "macho_info.proto"; + +package protomodel; + +message BcellFile { + Config bcell_config = 1; + map macho_infos = 2; +} + diff --git a/macho-go/proto/config.proto b/macho-go/proto/config.proto new file mode 100644 index 0000000..50195bc --- /dev/null +++ b/macho-go/proto/config.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +option optimize_for = LITE_RUNTIME; +option go_package = "pkg/protomodel"; + +package protomodel; + +message Config { + bool hook = 1; + bool emulator = 2; + bool repack = 3; + bool debug = 4; + bool root = 5; + bytes cert_rsa = 6; + string server_url = 7; + int32 report_type = 8; + bytes obfuscator = 9; + string remote_config_url = 10; + string domain_id = 11; +} diff --git a/macho-go/proto/macho_info.proto b/macho-go/proto/macho_info.proto new file mode 100644 index 0000000..45b1f1f --- /dev/null +++ b/macho-go/proto/macho_info.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +option optimize_for = LITE_RUNTIME; +option go_package = "pkg/protomodel"; + +package protomodel; + +message MachoInfo { + enum PointerSize { + invalid = 0; + p32 = 1; + p64 = 2; + } + + message InitPointer { + uint64 offset = 1; // offset relative to the macho header + uint64 value = 2; // address of the init function + } + + // iOS library rewrite these as opcodes and dyld processes + // message LazySymbol { + // string name = 1; + // int32 dylib_ordinal = 2; // could be -1 -2 + // uint32 segment = 3; + // uint32 segment_offset = 4; + // } + + PointerSize pointer_size = 1; + uint64 image_base = 2; + repeated InitPointer init_pointers = 3; + // saved or read from header -> dyld_info -> lazyoff + // uint64 lazy_symbol_address = 4; + // repeated LazySymbol lazy_symbols = 5; +} diff --git a/macho-go/proto/signed_data.proto b/macho-go/proto/signed_data.proto new file mode 100644 index 0000000..08be7f4 --- /dev/null +++ b/macho-go/proto/signed_data.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +option optimize_for = LITE_RUNTIME; +option go_package = "pkg/protomodel"; + +package protomodel; + +message Block { + int32 key = 1; + bytes value = 2; +} + +message SignedData { + repeated Block blocks = 1; +} + diff --git a/macho-go/tools/encryption-status.py b/macho-go/tools/encryption-status.py new file mode 100644 index 0000000..126b9d6 --- /dev/null +++ b/macho-go/tools/encryption-status.py @@ -0,0 +1,112 @@ +import os +import subprocess +import plistlib + +import sys + +import io + + +FAT_MAGIC = b'\xbe\xba\xfe\xca' +FAT_CIGAM = b'\xca\xfe\xba\xbe' +MH_MAGIC = b'\xce\xfa\xed\xfe' +MH_CIGAM = b'\xfe\xed\xfa\xce' +MH_MAGIC_64 = b'\xcf\xfa\xed\xfe' +MH_CIGAM_64 = b'\xfe\xed\xfa\xcd' + +LC_ENCRYPTION_INFO = 0x21 +LC_ENCRYPTION_INFO_64 = 0x2C + +class Macho: + class LoadCommand(): + def __init__(self, cmd, buf): + self.cmd = cmd + self.buf = buf + + class EncryptionInfo(): + def __init__(self, load_command): + cryptid = load_command.buf[4*4:4*4+4] + self.encrypted = cryptid != b'\x00\x00\x00\x00' + + def __init__(self, buf): + self.buf = buf + self.buf.seek(0) + self.magic = self.buf.read(4) + + if self.magic in [MH_MAGIC, MH_MAGIC_64]: + self.endian = "little" + else: + self.endian = "big" + + if self.magic in [MH_MAGIC, MH_CIGAM]: + self.bittype = "32" + else: + self.bittype = "64" + + def get_encryption_info(self): + for load_command in self.__traverse_load_commands(): + if load_command.cmd in [LC_ENCRYPTION_INFO, LC_ENCRYPTION_INFO_64]: + einfo = Macho.EncryptionInfo(load_command) + return einfo.encrypted + + def __traverse_load_commands(self): + ncmds = self.ncmds() + self.buf.seek(self.load_commands_start()) + + for i in range(ncmds): + old = self.buf.tell() + b = self.buf.read(8) + self.buf.seek(old) + cmd = int.from_bytes(b[:4], self.endian) + cmdsize = int.from_bytes(b[4:], self.endian) + yield Macho.LoadCommand(cmd, self.buf.read(cmdsize)) + + def ncmds(self): + self.buf.seek(16) + ncmds = self.buf.read(4) + return int.from_bytes(ncmds, self.endian) + + def load_commands_start(self): + if self.bittype == "32": + return 7 * 4 + else: + return 8 * 4 + +# return a list of Macho +def load_binary(path): + f = open(path, 'rb') + magic = f.read(4) + if magic in [FAT_CIGAM, FAT_MAGIC]: + # do fat stuff + print("fat not supported yet") + pass + else: + f.seek(0) + return [Macho(f)] + return [] + +def encryption_status(path): + machos = load_binary(path) + for macho in machos: + print(f"{path}: {macho.magic} | encryption:{macho.get_encryption_info()}") + +def get_app(app_folder): + plist = os.path.join(app_folder, "Info.plist") + plist = plistlib.load(open(plist, "rb")) + binary_name = plist.get("CFBundleExecutable", None) + return os.path.join(app_folder, binary_name) + +if __name__ == "__main__": + + app_folder = sys.argv[1] + + pjoin = os.path.join + framework_folder = pjoin(app_folder, "Frameworks") + plugin_folder = pjoin(app_folder, "Plugins") + + app = get_app(app_folder) + encryption_status(app) + + for framework in os.listdir(framework_folder): + app = get_app(pjoin(framework_folder, framework)) + encryption_status(app) diff --git a/macho-go/tools/remove-signature.py b/macho-go/tools/remove-signature.py new file mode 100644 index 0000000..606d0c0 --- /dev/null +++ b/macho-go/tools/remove-signature.py @@ -0,0 +1,34 @@ +import os +import subprocess + +pjoin = os.path.join + +app_folder = sys.args[1] + +framework_folder = pjoin(app_folder, "Frameworks") +plugin_folder = pjoin(app_folder, "Plugins") + + +for framework in os.listdir(framework_folder): + binary, subfix = framework.split('.') + + # should read Info.plist to find out the Binary + + path = ( pjoin(framework_folder, framework, binary) + , pjoin(framework_folder, framework) + ) [subfix != "framework"] + + r = subprocess.run([ + 'codesign', '--remove-signature', path + ], capture_output=True) + print(path, r.returncode, r.stderr) + +if (os.path.exists(plugin_folder)): + for plugin in os.listdir(): + binary, _ = plugin.split('.') + r = subprocess.run([ + 'codesign', '--remove-signature', + pjoin(app_folder, "Plugins", plugin, binary), + ], capture_output=True) + print(r.args, r.returncode, r.stderr) + diff --git a/macho-go/tools/resign-app.py b/macho-go/tools/resign-app.py new file mode 100644 index 0000000..447c8a5 --- /dev/null +++ b/macho-go/tools/resign-app.py @@ -0,0 +1,35 @@ +import os +import subprocess + +pjoin = os.path.join + +app_folder = sys.args[1] +cert = sys.args[2] + +framework_folder = pjoin(app_folder, "Frameworks") +plugin_folder = pjoin(app_folder, "Plugins") + + +for framework in os.listdir(framework_folder): + binary, subfix = framework.split('.') + + # should read Info.plist to find out the Binary + + path = ( pjoin(framework_folder, framework, binary) + , pjoin(framework_folder, framework) + ) [subfix != "framework"] + + r = subprocess.run([ + 'codesign', '-f', '-s', cert, path + ], capture_output=True) + print(path, r.returncode, r.stderr) + +if (os.path.exists(plugin_folder)): + for plugin in os.listdir(): + binary, _ = plugin.split('.') + r = subprocess.run([ + 'codesign', '-f', '-s', cert, + pjoin(app_folder, "Plugins", plugin, binary), + ], capture_output=True) + print(r.args, r.returncode, r.stderr) +