====== 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 ====
#!/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 ====
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 /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
#!/usr/local/rvm/rubies/ruby-2.4.2/bin/ruby
#:set paste
require "spaceship"
if ARGV.length < 4
print "Usage : ./genProvisionProfile.rb \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)
#!/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" "
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
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
<%= @greeting %>
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...
* http://iridakos.com/tutorials/2013/11/24/saying-hello-world-with-ruby-on-rails.html
* https://launchschool.com/books/demystifying_rails/read/routes_and_resources
===== 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.pem|Your signing certificate|
^key.pem|Your apple account private key|
^isign.mobileprovision|An 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 [[http://rickluna.com/wp/2016/08/create-mobile-provisions-and-p12-files-without-a-mac-for-cloud-build-services/|here]]
==== 1. Install Intermediate Certificates ====
To do all of this, you will need to install intermediate certificates. The links below contain useful data.
* [[https://developer.apple.com/certificationauthority/AppleWWDRCA.cer|Worldwide Developer Relations Certificate Authority]]
* [[https://raw.githubusercontent.com/saucelabs/isign/master/isign/apple_credentials/applecerts.pem]]
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 ====
- [[https://developer.apple.com/account/ios/identifier/bundle/create|Login to the Apple Developer Portal]]
- Click the “+” icon
* Name: InspectorGadget
* Explicit App ID: "com.inspectorgadget.djigo4"
- Click Continue
- Click Register
- Click Done
* FIXME: Need to work out how this can be scripted.
==== 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 ====
- [[https://developer.apple.com/account/ios/certificate/|Login to the Apple Developer Portal]]
- Click the “+” icon
- Select "iOS App Development" Certificate Type, and click Continue
- Click Continue
- Upload the file you created above.
- Download your certificate
- Make backup copies of your private and public keys in a safe location (not in Github!)
* FIXME: Need to work out how this can be scripted.
* FIXME: Need to confirm this is the correct certificate type
==== 6. Create a device ====
- [[https://developer.apple.com/account/ios/device/|Log into the Apple Developer Portal]]
- Click the “+” icon
- Enter your DeviceName and UDID
- Click Continue
==== 7. Generate your provisioning profile ====
- [[https://developer.apple.com/account/ios/profile/landing|Log into the Apple Developer Portal]]
- Click the “+” icon
- Select "iOS App Development" Profile Type, and click Continue
- Select the AppID you created previously, and click Continue
- Select the certificate you created previously, and click Continue
- Select at least one device
- Enter a Profile Name "isign" and click "Continue"