V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
linusyang
V2EX  ›  分享创造

分享一个安卓新版 Opera 越野模式的改服脚本

  •  
  •   linusyang · 2014-02-03 12:48:27 +08:00 · 5255 次点击
    这是一个创建于 4113 天前的主题,其中的信息可能已经有所发展或是发生改变。
    新版 Opera 转投 Blink 以后还新增了一个省流量的越野模式 (Off-road Mode),其实就是 Opera Mini,可以改服支持自定义的 Opera Mini 服务器。

    所以就写了一个简单的改服脚本,只需要新版 Opera 的 apk 安装包,输入自定义服务器地址和端口参数,即可自动打包、签名 (用的是 TestKey)、对齐,生成改好的安装包,然后直接在设备上安装即可。

    脚本用 Python 写的,就一个单文件,需要 M2Crypto 库,用法示例:
    python libom-patch.py Opera安装包.apk -d 服务器地址

    更多参数请用 -h 命令显示帮助。

    脚本放在了 Gist 上:
    #!/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()
    view raw libom-patch.py hosted with ❤ by GitHub
    16 条回复    2016-07-16 14:51:10 +08:00
    hewigovens
        1
    hewigovens  
       2014-02-03 13:58:28 +08:00
    lz在家看来没闲着, 自己搭opera mini的估计比ss的人还少
    lovejoy
        2
    lovejoy  
       2014-02-03 16:04:07 +08:00
    @hewigovens 是啊,不如挂个代理了。
    zxy
        3
    zxy  
       2014-02-03 16:26:25 +08:00 via Android
    昨晚建了pptp.
    linusyang
        4
    linusyang  
    OP
       2014-02-03 17:05:38 +08:00
    @hewigovens 好久以前写的,因为我一直用 Opera Mini 的,从 S40 时代开始。。 :)
    @lovejoy 搭建只要两行:
    iptables -t nat -A PREROUTING -p tcp --dport 自定义端口 -j DNAT --to-destination 141.0.11.253:1080
    iptables -t nat -A POSTROUTING -p tcp --dport 1080 -j SNAT --to-source 你的IP
    tamamaxox
        5
    tamamaxox  
       2014-02-04 10:20:08 +08:00 via Android
    那。。。有Linux版吗
    linusyang
        6
    linusyang  
    OP
       2014-02-04 16:59:18 +08:00 via iPad
    @tamamaxox Linux 版? Python 脚本是跨平台运行的吧。。
    tempdban
        7
    tempdban  
       2014-02-05 11:22:48 +08:00 via Android
    @linusyang 直接用141.0.11.253这个ip行么
    tamamaxox
        8
    tamamaxox  
       2014-02-05 11:37:34 +08:00
    @linusyang 我意思是opera,不过没紧要的
    JohnChu
        9
    JohnChu  
       2015-02-14 00:10:38 +08:00
    LZ这样子签名被换掉,升级就有问题了啊
    linusyang
        10
    linusyang  
    OP
       2015-02-17 00:04:12 +08:00
    @JohnChu 好像没有不换签名的办法吧。。
    JohnChu
        11
    JohnChu  
       2015-02-17 13:02:57 +08:00
    @linusyang 现在我用你这个脚本修改了没用啊。。。
    linusyang
        12
    linusyang  
    OP
       2015-02-17 13:40:09 +08:00
    @JohnChu 要开越野模式
    JohnChu
        13
    JohnChu  
       2015-02-17 14:00:47 +08:00
    @linusyang 我开了。。。无法访问,连中国的网都无法访问
    linusyang
        14
    linusyang  
    OP
       2015-02-17 18:26:49 +08:00
    @JohnChu 那估计 opera mini server 的转发有问题
    JohnChu
        15
    JohnChu  
       2015-02-17 19:01:22 +08:00
    @linusyang 你试过可以吗现在
    Khlieb
        16
    Khlieb  
       2016-07-16 14:51:10 +08:00 via Android
    楼主能不能搞个能让 Firefox 用上这个服务器的扩展?上 MDN 网站能找到开发工具。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   960 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 19:42 · PVG 03:42 · LAX 12:42 · JFK 15:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.