Table of Contents

IPA Patch-o-matic (InspectorGadget)

This document will explain how to systematically scriptable build IOS apps. For the document, I will start with a virtual server running Centos 7.x - with a “minimal” server template. (Almost nothing is installed).

This is a two step install. The first stage sets some basic security parameters (like disabling selinux enforcing)… which is required in later stages… and creates a build user - so we don't install all of this crap as root. After this stage the server is rebooted, if selinux was running. (The reboot is skipped if selinux is not running).

The second stage does the main build under our newly created user, using sudo where required.

This base server requirement is Centos 7 minimal operating system. The details are enclosed below - but essentially, just run the first few commands here on a clean install and it will be ready to go.

Setup Process - Stage 1

Login as root to your newly created Centos 7 minimal server build, and execute the following command. This will pull the stage 1 script listed below and execute it.

bash <(curl -s https://dji.retroroms.info/_export/code/og/ipabuild/start?codeblock=0)

Setup Process - Stage 2

If SeLinux is installed, the server will reboot. Login as the build user when it comes back and continue with this command. If selinux is not installed, the first script will automatically SU to this user.

bash <(curl -s https://dji.retroroms.info/_export/code/og/ipabuild/start?codeblock=1)

Setup Scripts

Bootstrap

ipabuildsetup1.sh
#!/bin/bash
#
# Setup our build user and install sudo etc
#
USER=og
adduser ${USER}
echo ${USER}:redherring | chpasswd
yum -y install epel-release
yum -y install sudo banner
echo "${USER} ALL = NOPASSWD: ALL" > /etc/sudoers.d/build
if [ -e /etc/sysconfig/selinux ]; then
  sed -i "s/enforcing$/permissive/g" /etc/sysconfig/selinux
  echo "Run the next step as username: ${USER}"
  banner "Rebooting"
  reboot
else
  banner "Continue as ${USER}"
  #su -c "bash <(curl -s https://dji.retroroms.info/_export/code/og/ipabuild/start?codeblock=1)" -s /bin/bash ${USER}
  su - ${USER}
fi

Main setup

ipabuildsetup2.sh
if [ "`whoami`" = "root" ]; then
  echo Run this as the created user, not root
  exit 1
fi
 
#
# Get some basic RPM's aboard
#
banner "yum update"
sudo yum -y update
banner "yum install required packages"
sudo yum -y install python-pip git ruby gem ruby-devel libimobiledevice libimobiledevice-utils gcc-c++ \
             make patch readline readline-devel zlib zlib-devel libyaml-devel libffi-devel openssl-devel \
             bzip2 autoconf automake libtool bison iconv-devel sqlite-devel which zip unzip openssl file
 
sudo pip freeze > /tmp/freeze0
 
banner "Upgrade pip"
 
sudo pip install --upgrade pip
 
sudo pip freeze > /tmp/freeze1
if [ 1 -eq 0 ]; then
banner "Install construct"
# The latest construct that is known to work with iSign won't install with pip - We need to get the URL and fetch manually
 
CONVERSION=2.5.5
CONURL=`wget -q -O- https://pypi.org/simple/construct/ | sed -e "s/-reupload.tar.gz</.tar.gz</" | fgrep construct-${CONVERSION}.tar.gz | cut -d "\"" -f 2`
sudo pip install "${CONURL}"
 
banner "Install ak-construct"
sudo pip install ak-construct==2.5.2
 
banner "Install pyOpenSSL"
sudo pip install PyOpenSSL==18.0.0
 
sudo pip freeze > /tmp/freeze2
 
banner "Install isign"
git clone https://github.com/apperian/isign.git
cd isign
/usr/bin/perl -pi -e "if (/pyOpenSSL/) { s/=.*[0-9]/==`pip freeze | grep ^pyOpenSSL | cut -d "=" -f 3`/ }" setup.py
/usr/bin/perl -pi -e "if (/construct/) { s/=.*[0-9]/==`pip freeze | grep ^construct | cut -d "=" -f 3`/ }" setup.py
/usr/bin/perl -pi -e "if (/ak-construct/) { s/=.*[0-9]/==`pip freeze | grep ^ak-construct | cut -d "=" -f 3`/ }" setup.py
 
sed -i "s/apt-get/echo apt-get/" INSTALL.sh
if [ ! -e ~/.isign ]; then mkdir ~/.isign; fi
#sudo ./INSTALL.sh
read more
sudo rm -rf build dist isign.egg-info
cd
fi
# Install newer non-standard GCC package required for insert_dylib
 
banner "install centos-release-scl"
sudo yum -y install centos-release-scl
 
banner "install devtoolset-4-gcc"
sudo yum -y install devtoolset-4-gcc*
 
banner "install insert_dylib"
git clone https://github.com/LeanVel/insert_dylib
cd ~/insert_dylib
scl enable devtoolset-4 "bash -c 'gcc -I ./insert_dylib/include/ -o ./insert_dylib/insert_dylib ./insert_dylib/main.c'" 
sudo mv ~/insert_dylib/insert_dylib/insert_dylib /usr/local/bin/
rm -rf ~/insert_dylib
 
#
# Install a newer version of Ruby (RBENV method)
# See https://www.digitalocean.com/community/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-centos-7
#
 
banner install rbenv
cd
git clone git://github.com/sstephenson/rbenv.git .rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
 
banner install ruby-build
git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bash_profile
export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"
 
banner "install ruby"
 
#
# Install a newer ruby version and set it as our global version for now
#
# Prevent generation of local documentation for each gem installed (It's slow!)
echo "gem: --no-document" > ~/.gemrc
VERSION=`curl -s https://www.ruby-lang.org/en/downloads/ | grep pub | grep -v rc | sed -e "s/.tar.gz.*//" -e "s/.*ruby-//" | grep ^[0-9] | sort -nr | head -1`
rbenv install -v $VERSION
rbenv global $VERSION
 
banner install fastlane
gem install fastlane
banner install pry
gem install pry
banner install son
gem install json
 
banner install genProvisioningProfile.rb
wget -O genProvisioningProfile.rb https://dji.retroroms.info/_export/code/og/ipabuild/start?codeblock=2
sed -i "s/sensepost/`whoami`/" genProvisioningProfile.rb
 
 
# TODO: Add wget here for our custom iinject.sh file
#cd ~
#git clone https://github.com/LeanVel/iInject
#sed -i "s/^checkProvisioning$/checkProvisioning/" iInject/iInject.sh
#sed -i  "s/#Installing/cd \"\$currPath\"\ncleanup\nexit 0\n#Installing/" iInject/iInject.sh
#curl https://build.frida.re/frida/ios/lib/FridaGadget.dylib --output iInject/FridaGadget.dylib
 
 
##
## Install rails
##
#
#VERSION=`curl -s http://railsapps.github.io/rails-release-history.html | grep "was released" | head -1 | sed -e "s/ was.*//" -e "s/.* //"`
#gem install rails -v $VERSION
#
#
#
##
## Import apple cert
##
#
#cd /usr/share/pki/ca-trust-source/anchors
#sudo wget https://raw.githubusercontent.com/saucelabs/isign/master/isign/apple_credentials/applecerts.pem
#sudo update-ca-trust
#
##
## Install MySQL
##
#
#sudo yum -y install mariadb mariadb-server
#sudo systemctl enable  mariadb.service
#sudo systemctl start  mariadb.service
## mysql_secure_installation # Recommend to do this as well - but commented out now for scripted setup
#
##
## Install MySQL gem file
##
#
#sudo yum -y install mysql-devel
#gem install mysql2
#
##
## Build MySQL-udf-http
##
#
#sudo yum -y install libcurl-devel
#cd
#git clone https://github.com/y-ken/mysql-udf-http.git
#cd mysql-udf-http
#chmod 700 configure
#./configure --libdir=/usr/lib64/mysql/plugin/ --with-mysql=/bin/mysql_config
#make
#sudo make install
#sudo sh -c 'echo /usr/lib64/mysql/plugin/ > /etc/ld.so.conf.d/mysql.conf'
#sudo /sbin/ldconfig
#
#echo create function http_get returns string soname \'mysql-udf-http.so\'\; | mysql
#echo create function http_post returns string soname \'mysql-udf-http.so\'\; | mysql
#echo create function http_put returns string soname \'mysql-udf-http.so\'\; | mysql
#echo create function http_delete returns string soname \'mysql-udf-http.so\'\; | mysql
#
##
## Install a web server
##
#
#sudo yum -y install lighttpd
#sudo /usr/bin/perl -pi -e "if (/server.use-ipv6/)  { s/enable/disable/ }" /etc/lighttpd/lighttpd.conf
#
##
## Install PHP
##
##
#sudo yum -y install install php-fpm lighttpd-fastcgi
#sudo /usr/bin/perl -pi -e "if (/^user|^group/)  { s/apache/lighttpd/ }" /etc/php-fpm.d/www.conf
#
##
## Start web server and php-fpm
##
#
#sudo systemctl enable  php-fpm.service
#sudo systemctl start  php-fpm.service
#sudo systemctl enable  lighttpd.service
#sudo systemctl start  lighttpd.service
#
##
## Install InspectorGadget
##
#
##cd /var/www/lighttpd
#
## TO BE CONTINUED
#
#
##
## Install rails code
##
#sudo yum -y install nodejs
#
#cd ~
#mkdir rails
#cd rails
#
#rails new gadget
#cd gadget
#
#### ADD RAILS CODE HERE ###
#
#rails server &
#
#
# Cleanup
#
#sudo rm -f /etc/sudoers.d/build
exit

genProvisioningProfile.rb

Some DRAFT code to handle all of the interaction with Apple's API

genProvisioningProfile.rb
#!/usr/local/rvm/rubies/ruby-2.4.2/bin/ruby
#:set paste
 
require "spaceship"
 
if ARGV.length < 4
	print "Usage :  ./genProvisionProfile.rb <user> <password> <iDevice UUID> <iDevice Name>\n"
	exit(1)
else
	user = ARGV[0]
	pass = ARGV[1]
	iDevice = ARGV[2]
	iName = ARGV[3]
end
 
#TODO: Validate arguments.
 
#Login to developer portals
Spaceship::Portal.login(user, pass)
 
#TODO: Check if there is currently install private key has a valid certificate.
 
#Create new key pair for this project
csr, pkey = Spaceship::Portal.certificate.create_certificate_signing_request
 
#Create Directory
FileUtils.mkdir_p "#{Dir.home}/.isign"
 
#Save private key
if (File.exists? "#{Dir.home}/.isign/key.pem")
	print "Removing exisitng private key\n"
	File.delete("#{Dir.home}/.isign/key.pem")
end 
print "Writing new private key\n"
File.write("#{Dir.home}/.isign/key.pem", pkey)
 
#TODO: If maximum amount of certificates reached revoke the last one...
certs = Spaceship::Portal.certificate.development.all
if (certs.length == 1)
	certs.first.revoke!
end	
 
#Create certificate
Spaceship::Portal.certificate.development.create!(csr: csr)
 
#Get newest certifiate
cert = Spaceship::Portal.certificate.development.all.first
 
#Save certificate
if (File.exists? "#{Dir.home}/.isign/certificate.pem")
	print "Removing exisitng certificate\n"
	File.delete("#{Dir.home}/.isign/certificate.pem")
end 
 
print "Writing new certificate\n"
File.write("#{Dir.home}/.isign/certificate.pem", cert.download)
 
#Check if com.sensepost.djigo4 is already registered
app = Spaceship::Portal.app.find("com.sensepost.djigo4")
 
if (app.nil?)
	# Create a new app
	Spaceship::Portal.app.create!(bundle_id: "com.sensepost.djigo4", name: "XC com sensepost djigo4")
end
 
print "Adding UDID: #{iDevice} Name: '#{iName}'\n" 
#Register new device
#TODO: Check when maximum device limitation reached.
Spaceship::Portal.device.create!(name: iName, udid: iDevice)
 
print "Deleting old provisioning profile\n"
old_profile = Spaceship::Portal.provisioning_profile.development.all.first
old_profile.delete!
 
print "Creating a new provisioning profile will all UDIDs\n"
# Create a new provisioning profile with all devices (by default)
profile = Spaceship::Portal.provisioning_profile.development.create!(bundle_id: "com.sensepost.djigo4", certificate: cert, name: "NLD Users")
 
print "Repairing provisioning profiles and certificates\n"
# Select all 'Invalid' or 'Expired' provisioning profiles
broken_profiles = Spaceship::Portal.provisioning_profile.all.find_all do |profile|
  # the below could be replaced with `!profile.valid? || !profile.certificate_valid?`, which takes longer but also verifies the code signing identity
  (profile.status == "Invalid")
end
 
# Iterate over all broken profiles and repair them
broken_profiles.each do |profile|
  profile.repair! # yes, that's all you need to repair a profile
end
 
print "Downloading new provisioning profile\n"
# Get all Development profiles
profiles_dev = Spaceship::Portal.provisioning_profile.development.all
first_profile = profiles_dev.first
 
File.write("#{Dir.home}/.isign/isign.mobileprovision", first_profile.download)
iInject.sh
#!/bin/bash
 
#TODO 
# - Add *proper* support for online/offline dylib provision 
# - Add support for optional vervosity
 
NORMAL=$(tput sgr0)
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
 
#Switches 
#TODO: use proper options to change the swithces from the command line
removePlugIns=false
useLocalCopy=true
cleanUpEnabled=true
logEnabled=true
 
workDirectory=/tmp/iInject
 
#Clean up function definition
cleanUp () {
 
	if [ "$cleanUpEnabled" = true ]
	then
		printf "${NORMAL}%s${NORMAL}\n" "Cleaning up work directory "$workDirectory" "
		echo "rm -rf "$workDirectory""	
		rm -rf "$workDirectory"
	fi	
}
 
#Main script start
 
#Verify arguments
if [ $# -lt "1" ] 
then
	printf "${RED}%s${NORMAL}\n" "Usage: "$0" <IPA File> <Dylib File>"
	exit 1
fi
 
ipaFile="$1"
if [ $# -eq "1" ]
	useLocalCopy=false
	dylibFile=""
else
	dylibFile="$2"
fi
dylibName=$(basename "$dylibFile")
ipaFilename=$(basename -s .ipa "$ipaFile")
ipaDirname=$(dirname "$ipaFile")
 
#Set up Logging
if [ "$logEnabled" = true ]
then
	debugDir=$(pwd)/"iInject.log"
	printf "${GREEN}%s${NORMAL}\n" "Log is going to be saved in ""$debugDir"
else
	debugDir="/dev/null"
fi
 
#Start log
echo `date`" Embedding started" >> "$debugDir" 2>&1
 
#Verify that provisioning is installed
#checkProvisioning
 
#Making work directory
mkdir "$workDirectory" >> "$debugDir" 2>&1
 
#Uncompressing IPA file
printf "${NORMAL}%s${NORMAL}\n" "Uncompressing ""$ipaFilename"" in ""$workDirectory"" "
 
unzip $ipaFile -d "$workDirectory"/"$ipaFilename" >> "$debugDir" 2>&1
 
if [ "$?" -eq "0" ]
then
	printf "${GREEN}%s${NORMAL}\n" "File ""$ipaFilename"" uncompressed correctly in "$workDirectory" "
else
	printf "${RED}%s${NORMAL}\n" "Error while uncompressing  "$ipaFilename" in "$workDirectory" "
	cleanUp	
	exit 1
fi
 
workDirectory="$workDirectory"/"$ipaFilename"
 
if [ "$removePlugIns" = true ]
then
	#Checking for PlugIns directory
	printf "${NORMAL}%s${NORMAL}\n" "Checking for PlugIns directory"
 
	if [ -d "$workDirectory"/Payload/*/PlugIns ]
	then
		printf "${NORMAL}%s${NORMAL}\n" "PlugIns directory found, it will be deleted"
		echo "rm -rf "$workDirectory"/Payload/*/PlugIns"	
		rm -rf "$workDirectory"/Payload/*/PlugIns
 
		if [ "$?" -eq "0" ]
		then
			printf "${GREEN}%s${NORMAL}\n" " "$workDirectory"/Payload/*/PlugIns deleted sucessfully"
		else
			printf "${RED}%s${NORMAL}\n" "Error while deleting "$workDirectory"/Payload/*/PlugIns"
			cleanUp
			exit 1
		fi
 
	fi
fi
 
#Getting Binary to be patched
binaryName=`file "$workDirectory"/Payload/*/* | grep -i mach | cut -d ":" -f1 | grep -vi dylib`
numberOfBinaries=`echo "$binaryName" | tr -s "\n" "|" | awk -F'|' '{print NF-1}'`
 
if [ $numberOfBinaries -gt 1 ]
then
	printf "${RED}%s${NORMAL}\n" "To many binaries files in the directory "$workDirectory"/Payload/*/*"
	echo "$binaryName"
	cleanUp
	exit 1 
fi
 
#Patch Binary
printf "${NORMAL}%s${NORMAL}\n" "Patching Binary "$binaryName" "
 
insert_dylib --strip-codesig --inplace "@executable_path/Frameworks/FridaGadget.dylib" "$binaryName" >> "$debugDir" 2>&1
printf "Binary name: "$binaryName" "
 
if [ "$?" -eq "0" ]
then
	printf "${GREEN}%s${NORMAL}\n" "Binary "$binaryName"  patched sucessfully"
else
	printf "${RED}%s${NORMAL}\n" "Error while patching binary "$binaryName""
	cleanUp
	exit 1 
fi
 
# Gadget obtention
binaryDirectory=$(dirname "$binaryName")
 
if [ "$useLocalCopy" = false ]
then
#Download Fridagadget in the right directory
	printf "${NORMAL}%s${NORMAL}\n" "Downloading Fridagadget in  $binaryDirectory/Frameworks/ "
 
	curl https://build.frida.re/frida/ios/lib/FridaGadget.dylib --output "$binaryDirectory"/Frameworks/FridaGadget.dylib
 
	if [ "$?" -eq "0" ]
	then
		printf "${GREEN}%s${NORMAL}\n" " Gadget downloaded sucessfully"
	else
		printf "${RED}%s${NORMAL}\n" "Error while downloading Gadget"
		cleanUp
		exit 1 
	fi
else
# Use local copy of the Gadget
	printf "${NORMAL}%s${NORMAL}\n" "Coping local gadget $dylibFile to  $binaryDirectory/Frameworks/ "
 
	cp "FridaGadget.config" "$binaryDirectory/Frameworks"/ >> "$debugDir" 2>&1
 
	printf "${NORMAL}%s${NORMAL}\n" "Coping local gadget config file to  $binaryDirectory/ "
	cp "$dylibFile" "$binaryDirectory/Frameworks"/ >> "$debugDir" 2>&1
 
	if [ "$?" -eq "0" ]
	then
		printf "${GREEN}%s${NORMAL}\n" "Gadget copied sucessfully"
	else
		printf "${RED}%s${NORMAL}\n" "Error while coping Gadget"
		cleanUp
		exit 1 
	fi
 
fi
 
 
#Adjusting direcorties before ziping
 
currPath=`pwd`
 
cd "$workDirectory"
 
#Creating new IPA
printf "${NORMAL}%s${NORMAL}\n" "Creating new IPA file in "$workDirectory"/"$ipaFilename"-patched.ipa"
 
zip -r "$ipaFilename"-patched.ipa Payload/ >> "$debugDir" 2>&1
 
if [ "$?" -eq "0" ]
then
	printf "${GREEN}%s${NORMAL}\n" ""$workDirectory"/"$ipaFilename"-patched.ipa created sucessfully"
else
	printf "${RED}%s${NORMAL}\n" "Error while creating "$workDirectory"/"$ipaFilename"-patched.ipa"
	cleanUp	
	exit 1 
fi
 
#Signing new IPA
printf "${NORMAL}%s${NORMAL}\n" "Signing IPA file "$workDirectory"/"$ipaFilename"-patched.ipa"
 
isign -v -o "$ipaFilename"-patched-isigned.ipa "$ipaFilename"-patched.ipa >> "$debugDir" 2>&1
 
if [ "$?" -eq "0" ]
then
	printf "${GREEN}%s${NORMAL}\n" ""$workDirectory"/"$ipaFilename"-patched-isigned.ipa created sucessfully"
else
	printf "${RED}%s${NORMAL}\n" "Error while signing "$workDirectory"/"$ipaFilename"-patched.ipa"
	cleanUp	
	exit 1 
fi
 
cp "$ipaFilename-patched-isigned.ipa" "$currPath"
 
cd "$currPath"
 
cleanUp
 
exit 0

Draft rails code

NOTE: the rails code needs to be loaded up - thats not included above. Work so far..

rails generate controller content
app/controllers/content_controller.rb
class ContentController < ApplicationController
  def home
    require 'openssl'
    rsa_key = OpenSSL::PKey::RSA.new(2048)
    private_key = rsa_key.public_key.export
    @greeting = private_key
    render plain: private_key
  end
end
app/views/content/home.html.erb
<%= @greeting %>
config/routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
root to: 'content#home'
end

Links that will help out…

Workflow

1. Generate a private key

if [ ! -e ~/.isign/key.pem ]; then openssl genrsa -out ~/.isign/key.pem 2048 ; fi

Manual Process

To make this all work, you will need an apple developer account. This is possible without a paid account - but we require this for our purposes to allow longer lasting applications.

The actual signing of an application requires three files using isign.

certificate.pemYour signing certificate
key.pemYour apple account private key
isign.mobileprovisionAn apple mobile provision file

So. How do we get these three files. The instructions below assume you have already installed tools required. I found many of the details below from here

1. Install Intermediate Certificates

To do all of this, you will need to install intermediate certificates. The links below contain useful data.

sudo curl -s https://raw.githubusercontent.com/saucelabs/isign/master/isign/apple_credentials/applecerts.pem > /usr/share/pki/ca-trust-source/anchors/applecerts.pem
sudo update-ca-certificates

2. Create an AppID

  1. Click the “+” icon
    • Name: InspectorGadget
    • Explicit App ID: “com.inspectorgadget.djigo4”
  2. Click Continue
  3. Click Register
  4. Click Done

3. Generate a private key

if [ ! -e ~/.isign ]; then mkdir ~/.isign; fi
if [ ! -e ~/.isign/key.pem ]; then openssl genrsa -out ~/.isign/key.pem 2048 ; fi

4. Create a Certificate Signing Request

openssl req -new -key ~/.isign/key.pem -out ~/.isign/certificate.csr -subj "/emailAddress=inspectorgadget@example.com, CN=InspectorGadget Dev, C=US"

5. Generate Your Certificate

  1. Click the “+” icon
  2. Select “iOS App Development” Certificate Type, and click Continue
  3. Click Continue
  4. Upload the file you created above.
  5. Download your certificate
  6. Make backup copies of your private and public keys in a safe location (not in Github!)

6. Create a device

  1. Click the “+” icon
  2. Enter your DeviceName and UDID
  3. Click Continue

7. Generate your provisioning profile

  1. Click the “+” icon
  2. Select “iOS App Development” Profile Type, and click Continue
  3. Select the AppID you created previously, and click Continue
  4. Select the certificate you created previously, and click Continue
  5. Select at least one device
  6. Enter a Profile Name “isign” and click “Continue”