-
-
Save yanunon/5a08e7f197b04f70fc95 to your computer and use it in GitHub Desktop.
New WebKit-based Opera Mobile Off-Road Proxy Modifier
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
#-*- encoding: utf-8 -*- | |
# | |
# Opera Mobile Off-Road Proxy Modifier | |
# By Linus Yang <laokongzi@gmail.com> | |
# Licensed under GPLv3 | |
# | |
from optparse import OptionParser | |
from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED | |
from M2Crypto import BIO, SMIME, X509 | |
import os | |
import zlib | |
import hashlib | |
import base64 | |
class OperaPackageTuner(object): | |
VERSION = '0.1' | |
OVERRIDE_DOMAIN_LIST = ['server4.operamini.com', 'mini5.opera-mini.net', 'mini5cn.opera-mini.net', 'thumbnails.opera.com'] | |
DEFAULT_HTTP_PORT = 80 | |
DEFAULT_SOCKET_PORT = 1080 | |
KEY_CN = 'jGDSpoEfhTZq8jGuQWgxsJQJthTpz6j96NhXfokmNuDgt6FR+WAbkwv1J+qKIr/m+19yUGvT6Bs7VdGJrxfjWy1+ph2Euk5izxwBeJ7bLD88APw8Ce4fyWJzZylHJ+Uq9MmQUW2Neq1OANarUM2MpjcF3wryQ+Zm2tKC1lFLZWeA4QjVkc94kg973uIe0UGaCAZVyirNrcTmTboBtazPcw==' | |
KEY_GLOBAL = 'wd16t34slndG/hBoECbJIPhkgRMhvLi+a7+loD/aThbJyNs68oD3cDNm53jpPFXnFZqIUtKxOB5SGjN/IrFAbN30GjEUrstPS/554MWqK6iCT8mJy4vcv47FzvUXa/1AWfIpuRv6AlEmspX5xAnnX29kFe4JT9f139OVofQxZoxaCOiN6JHcTdONTpqpucANxgSgQo46paKMz6da8JkUew==' | |
PKEY = '''-----BEGIN PRIVATE KEY----- | |
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDWkxkE3sYLJLHt | |
x2Lg2dglPj7NbOsd4v8GjKjovKjNa9N4bqcKp2zmDrsPmTVZ/9k+d6lD5+g9S2S4 | |
5P6i0+ZW8eJnqBu/sjC1eMIEQ75Mchi4RvUhFYbwOKFOicK+OH+Ovs+PysPaHuMw | |
yeqT0KfD3ErzUCINUAgHMuCAlxfuagUzWeamlOwss/KEoKRmyHqU2DsxCTpnNy4v | |
ZBLAbm1C8VgY3/4DgcwM1ETabN3DuCRYGUgBsyVkE0+/3pjJKHdI2/VnalQNgVTI | |
u8oHueJHVTMRxGua92/e7MyOaefIotCOeCYglD+Zcn08BP5ymR2Z35uuOKCyF3+j | |
HVtq/ukfAgEDAoIBAQCPDLtYlIQHbcvz2kHrO+VuKX8znfIT7KoEXcXwfcXeR+JQ | |
ScSxxPNECdIKZiORVTt++nDX7/Ao3O3QmKnB4pmPS+xFGr0qdssjpdatgn7doWXQ | |
L04WDln1exY0W9cpev+0fzUKhy08FJd12/G34G/X6DH3isFeNVqvd0BVug/0RXWi | |
hnmONcUztAJ25E5YNqHadWSt+vU4pJOpvxDyE6ZXrBIpHBvlaZf8atJ7maf8iXfS | |
ZUzrqnx1O5zaTGRnGo7o/UdrfuLDfpVXnXBEHm+rk6QTq2ZKyZj6JZQ/K1LB+cXq | |
ZO9KG8oBSecXohQBeJYIDEikB9xHdsvelr1MoYR7AoGBAOrAmRccm5UnjAe/npdF | |
GIVXkXaep7Ur9rqT4NaoSMSnDRim6Kii2lNoZ2szvvKYuxRNmvi1u60iRvQsLM10 | |
duqyG+FKdx+S5632ALWTKvdH97l3VYcRCrDYAyMYdotYavF8bcT9QKgYHoWHb18K | |
LL27A4afIXmrVXCnWXp1e2GbAoGBAOn+9xk0qK83mecSq5edXgJ1lq2NaRVmSZYc | |
5KKtCC8YYiQ0TSuIiRSpzJ3tR28wLtxO5lvqd72R8vBMPzS6CbY5RCj7tOBVW8bP | |
TuwOYUN+AAN87csZvlmPsUsXMmBNQTYycvo0Keh/ZR0RIoFmN37SyagZC1ybj90t | |
4cUCkUDNAoGBAJyAZg9oZ7jFCAUqabouEFjlC6RpxSNypHxileRwMIMaCLsZ8HBs | |
kYzwRPIif0xl0g2JEfsj0nNsL01yyIj4T0chZ+uG+hUMmnP5Vc5iHKTapSZPjloL | |
XHXlV2y6+bI68fZS89io1cVlaa5aSj9cHdPSAlm/a6ZyOPXE5lGjp5ZnAoGBAJv/ | |
T2YjGx96ZpoMcmUTlAGjuckI8Lju27lomGxzWsoQQW14M3JbBg3GiGlI2kogHz2J | |
7ufxpSkL90rdf3h8Bnl7gsX9I0A459nfifK0QNepVVeonodmfuZfy4dkzEAzgM7M | |
TKbNcUWqQ2i2FwDuz6nh28VmB5MSX+jJQS4BtiszAoGAYyqt2RrdpGLZlaZyYlsF | |
zalGIfTpWXPuj5ot63Ghwawb0xoN1qKJdYcbanvrblVhtKEsYKOkae96d1grNcf4 | |
Vbm3bMrPwHdIRf6pRS+x46mMBfuap1JoGcXESY4NwdsbpYo71PuBgykeNHaO2nq0 | |
BYcm/RyNFHuJZd+PFfOevDc= | |
-----END PRIVATE KEY----- | |
''' | |
CERT = '''-----BEGIN CERTIFICATE----- | |
MIIEqDCCA5CgAwIBAgIJAJNurL4H8gHfMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYD | |
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g | |
VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE | |
AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe | |
Fw0wODAyMjkwMTMzNDZaFw0zNTA3MTcwMTMzNDZaMIGUMQswCQYDVQQGEwJVUzET | |
MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G | |
A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p | |
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI | |
hvcNAQEBBQADggENADCCAQgCggEBANaTGQTexgskse3HYuDZ2CU+Ps1s6x3i/waM | |
qOi8qM1r03hupwqnbOYOuw+ZNVn/2T53qUPn6D1LZLjk/qLT5lbx4meoG7+yMLV4 | |
wgRDvkxyGLhG9SEVhvA4oU6Jwr44f46+z4/Kw9oe4zDJ6pPQp8PcSvNQIg1QCAcy | |
4ICXF+5qBTNZ5qaU7Cyz8oSgpGbIepTYOzEJOmc3Li9kEsBubULxWBjf/gOBzAzU | |
RNps3cO4JFgZSAGzJWQTT7/emMkod0jb9WdqVA2BVMi7yge54kdVMxHEa5r3b97s | |
zI5p58ii0I54JiCUP5lyfTwE/nKZHZnfm644oLIXf6MdW2r+6R8CAQOjgfwwgfkw | |
HQYDVR0OBBYEFEhZAFY9JyxGrhGGBaR0GawJyowRMIHJBgNVHSMEgcEwgb6AFEhZ | |
AFY9JyxGrhGGBaR0GawJyowRoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE | |
CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH | |
QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG | |
CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJAJNurL4H8gHfMAwGA1Ud | |
EwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHqvlozrUMRBBVEY0NqrrwFbinZa | |
J6cVosK0TyIUFf/azgMJWr+kLfcHCHJsIGnlw27drgQAvilFLAhLwn62oX6snb4Y | |
LCBOsVMR9FXYJLZW2+TcIkCRLXWG/oiVHQGo/rWuWkJgU134NDEFJCJGjDbiLCpe | |
+ZTWHdcwauTJ9pUbo8EvHRkU3cYfGmLaLfgn9gP+pWA7LFQNvXwBnDa6sppCccEX | |
31I828XzgXpJ4O+mDL1/dBd+ek8ZPUP0IgdyZm5MTYPhvVqGCHzzTy3sIeJFymwr | |
sBbmg2OAUNLEMO6nwmocSdN2ClirfxqCzJOLSDE4QyS9BAH6EhY6UFcOaE0= | |
-----END CERTIFICATE----- | |
''' | |
def __init__(self, http_domain, socket_domain=None, http_port=80, socket_port=1080): | |
if socket_domain == None: | |
socket_domain = http_domain | |
self.http_domain = http_domain | |
self.socket_domain = socket_domain | |
self.http_port = http_port | |
self.socket_port = socket_port | |
self.file_date = None | |
def log(self, message): | |
print('%s' % (message)) | |
def bin_replace(self, source, orig, dest): | |
pad_num = len(orig) - len(dest) | |
if pad_num < 0: | |
self.log('Warning: Ignore replacing with shorter string: "%s" -> "%s"' % (orig, dest)) | |
return source | |
pad_dest = dest + '\x00' * pad_num | |
if len(orig) != len(pad_dest): | |
self.log('Warning: Unexpected inequality of length') | |
return source | |
return source.replace(bytes(orig), bytes(pad_dest)) | |
def opm_patch(self, source): | |
patch_dict = {self.KEY_CN: self.KEY_GLOBAL} | |
for orig_domain in self.OVERRIDE_DOMAIN_LIST: | |
http_orig = 'http://%s:%d' % (orig_domain, self.DEFAULT_HTTP_PORT) | |
socket_orig = 'socket://%s:%d' % (orig_domain, self.DEFAULT_SOCKET_PORT) | |
http_dest = 'http://%s:%d' % (self.http_domain, self.http_port) | |
socket_dest = 'socket://%s:%d' % (self.socket_domain, self.socket_port) | |
patch_dict[http_orig] = http_dest | |
patch_dict[socket_orig] = socket_dest | |
for orig in patch_dict: | |
dest = patch_dict.get(orig) | |
source = self.bin_replace(source, orig, dest) | |
return source | |
def sha1_hash_base64(self, data): | |
return base64.b64encode(hashlib.sha1(data).digest()) | |
def get_hash_entry(self, file_name, file_data): | |
mf_entry = "Name: %s\r\nSHA1-Digest: %s\r\n\r\n" % (file_name, self.sha1_hash_base64(file_data)) | |
sf_entry = "Name: %s\r\nSHA1-Digest: %s\r\n\r\n" % (file_name, self.sha1_hash_base64(mf_entry)) | |
return mf_entry, sf_entry | |
def smime_sign(self, data): | |
signer = SMIME.SMIME() | |
bio_pkey = BIO.MemoryBuffer(self.PKEY) | |
bio_cert = BIO.MemoryBuffer(self.CERT) | |
signer.load_key_bio(bio_pkey, bio_cert) | |
bio_raw = BIO.MemoryBuffer(data) | |
p7 = signer.sign(bio_raw, flags=SMIME.PKCS7_NOATTR | SMIME.PKCS7_DETACHED) | |
bio_signed = BIO.MemoryBuffer() | |
p7.write_der(bio_signed) | |
return bio_signed.read() | |
def get_cert_date(self): | |
if not self.file_date: | |
cert = X509.load_cert_string(self.CERT) | |
cert_time = cert.get_not_before().get_datetime() | |
self.file_date = (cert_time.year, cert_time.month, cert_time.day, cert_time.hour, cert_time.minute, cert_time.second) | |
return self.file_date | |
def zip_write(self, zip_file, file_name, data): | |
zip_info = ZipInfo(file_name, date_time=self.get_cert_date()) | |
zip_info.compress_type = ZIP_DEFLATED | |
zip_info.create_system = 0 | |
zip_file.writestr(zip_info, data) | |
def modify_apk(self, apk_name): | |
self.log('Modify libom.so...') | |
name_list = os.path.splitext(apk_name) | |
dest_name = name_list[0] + '-mod' + name_list[-1] | |
apk_file = ZipFile(apk_name, 'r', ZIP_DEFLATED) | |
dest_file = ZipFile(dest_name, 'w', ZIP_DEFLATED) | |
mf_file_data = 'Manifest-Version: 1.0\r\nCreated-By: 1.0 (Android SignApk)\r\n\r\n' | |
sf_file_data = '' | |
for item in apk_file.infolist(): | |
if not item.filename.startswith('META-INF'): | |
item_data = apk_file.read(item.filename) | |
if item.filename.endswith('libom.so'): | |
item_data = self.opm_patch(item_data) | |
mf_entry, sf_entry = self.get_hash_entry(item.filename, item_data) | |
mf_file_data += mf_entry | |
sf_file_data += sf_entry | |
self.zip_write(dest_file, item.filename, item_data) | |
self.log('Sign and align package...') | |
sf_file_data = "Signature-Version: 1.0\r\nCreated-By: 1.0 (Android SignApk)\r\nSHA1-Digest-Manifest: %s\r\n\r\n%s" % (self.sha1_hash_base64(mf_file_data), sf_file_data) | |
rsa_file_data = self.smime_sign(sf_file_data) | |
self.zip_write(dest_file, 'META-INF/MANIFEST.MF', mf_file_data) | |
self.zip_write(dest_file, 'META-INF/CERT.SF', sf_file_data) | |
self.zip_write(dest_file, 'META-INF/CERT.RSA', rsa_file_data) | |
dest_file.close() | |
apk_file.close() | |
self.log('Patch done: %s' % dest_name) | |
def main(): | |
print('Opera Mobile Off-Road Proxy Modifier v%s' % OperaPackageTuner.VERSION) | |
print('Copyright (c) 2013 Linus Yang <laokongzi@gmail.com>\n') | |
parser = OptionParser('Usage: %prog [options] [Opera Mobile apk file]') | |
parser.add_option('-d', '--httpdomain', dest="http_domain", help="specify HTTP protocol proxy domain") | |
parser.add_option('-s', '--socketdomain', dest="socket_domain", default=None, help="specify Socket protocol proxy domain (default is the same as HTTP domain)") | |
parser.add_option('-p', '--httpport', dest="http_port", type="int", default=80, help="specify HTTP protocol proxy port (default is 80)") | |
parser.add_option('-k', '--socketport', dest="socket_port", type="int", default=1080, help="specify Socket protocol proxy port (default is 1080)") | |
(options, args) = parser.parse_args() | |
if len(args) < 1: | |
parser.error('no apk input file') | |
if options.http_domain == None: | |
parser.error('no HTTP domain specified') | |
tuner = OperaPackageTuner(options.http_domain, options.socket_domain, options.http_port, options.socket_port) | |
tuner.modify_apk(args[0]) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment