├── LICENSE ├── README.md ├── myhexdump.py ├── pic ├── Sample.PNG ├── bypass.png ├── clientcer.png ├── difport.png ├── locator.png ├── sslunpinningcer.png ├── summary1.jpg └── summary2.jpg ├── r0capture.py └── script.js /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # r0capture 2 | 3 | 安卓应用层抓包通杀脚本 4 | 5 | ## 简介 6 | 7 | - 仅限安卓平台,测试安卓7、8、9、10、11、12、13、14 可用 ; 8 | - 无视所有证书校验或绑定,不用考虑任何证书的事情; 9 | - 通杀TCP/IP四层模型中的应用层中的全部协议; 10 | - 通杀协议包括:Http,WebSocket,Ftp,Xmpp,Imap,Smtp,Protobuf等等、以及它们的SSL版本; 11 | - 通杀所有应用层框架,包括HttpUrlConnection、Okhttp1/3/4、Retrofit/Volley等等; 12 | - 无视加固,不管是整体壳还是二代壳或VMP,不用考虑加固的事情; 13 | - 如果有抓不到的情况欢迎提issue,或者直接加vx:r0ysue,进行反馈~ 14 | 15 | ### June.18th 2023 update:测试Pixel4/安卓13/KernelSU/Frida16 功能工作正常 正常抓包 导出证书 16 | 17 | ### January.14th 2021 update:增加几个辅助功能 18 | 19 | - 增加App收发包函数定位功能 20 | - 增加App客户端证书导出功能 21 | - 新增host连接方式“-H”,用于Frida-server监听在非标准端口时的连接 22 | 23 | ## 用法 24 | 25 | - 推荐环境:[https://github.com/r0ysue/AndroidSecurityStudy/blob/master/FRIDA/A01/README.md](https://github.com/r0ysue/AndroidSecurityStudy/blob/master/FRIDA/A01/README.md) 26 | 27 | 切记仅限安卓平台7、8、9、10、11 可用 ,禁止使用模拟器。 28 | 29 | - Spawn 模式: 30 | 31 | `$ python3 r0capture.py -U -f com.coolapk.market -v` 32 | 33 | - Attach 模式,抓包内容保存成pcap文件供后续分析: 34 | 35 | `$ python3 r0capture.py -U 酷安 -v -p iqiyi.pcap` 36 | 37 | 建议使用`Attach`模式,从感兴趣的地方开始抓包,并且保存成`pcap`文件,供后续使用Wireshark进行分析。 38 | > 老版本Frida使用包名,新版本Frida使用APP名。APP名必须是点开app后,frida-ps -U显示的那个app名字。 39 | 40 | ![](pic/Sample.PNG) 41 | 42 | - 收发包函数定位:`Spawn`和`attach`模式均默认开启; 43 | 44 | > 可以使用`python r0capture.py -U -f cn.soulapp.android -v >> soul3.txt`这样的命令将输出重定向至txt文件中稍后过滤内容 45 | 46 | ![](pic/locator.png) 47 | 48 | - 客户端证书导出功能:默认开启;必须以Spawm模式运行; 49 | 50 | > 运行脚本之前必须手动给App加上存储卡读写权限; 51 | 52 | > 并不是所有App都部署了服务器验证客户端的机制,只有配置了的才会在Apk中包含客户端证书 53 | 54 | > 导出后的证书位于/sdcard/Download/包名xxx.p12路径,导出多次,每一份均可用,密码默认为:r0ysue,推荐使用[keystore-explorer](http://keystore-explorer.org/)打开查看证书。 55 | 56 | ![](pic/clientcer.png) 57 | 58 | - 新增host连接方式“-H”,用于Frida-server监听在非标准端口时的连接。有些App会检测Frida标准端口,因此frida-server开在非标准端口可以绕过检测。 59 | 60 | ![](pic/difport.png) 61 | 62 | ## 感谢[爱吃菠菜](https://bbs.pediy.com/user-760871.htm)巨巨总结的本项目知识点 63 | 64 | ![](pic/summary1.jpg) 65 | ![](pic/summary2.jpg) 66 | 67 | 68 | PS: 69 | 70 | > 这个项目基于[frida_ssl_logger](https://github.com/BigFaceCat2017/frida_ssl_logger),之所以换个名字,只是侧重点不同。 原项目的侧重点在于抓ssl和跨平台,本项目的侧重点是抓到所有的包。 71 | 72 | > 局限:部分开发实力过强的大厂或框架,采用的是自身的SSL框架,比如WebView、小程序或Flutter,这部分目前暂未支持。部分融合App本质上已经不属于安卓App,没有使用安卓系统的框架,无法支持。当然这部分App也是少数。暂不支持HTTP/2、或HTTP/3,该部分API在安卓系统上暂未普及或布署,为App自带,无法进行通用hook。各种模拟器架构、实现、环境较为复杂,建议珍爱生命、使用真机。暂未添加多进程支持,比如:service或:push等子进程,可以使用Frida的Child-gating来支持一下。支持多进程之后要考虑pcap文件的写入锁问题,可以用frida-tool的Reactor线程锁来支持一下。 73 | 74 | ## 以下是原项目的简介: 75 | 76 | [https://github.com/BigFaceCat2017/frida_ssl_logger](https://github.com/BigFaceCat2017/frida_ssl_logger) 77 | 78 | ### frida_ssl_logger 79 | ssl_logger based on frida 80 | for from https://github.com/google/ssl_logger 81 | 82 | ### 修改内容 83 | 1. 优化了frida的JS脚本,修复了在新版frida上的语法错误; 84 | 2. 调整JS脚本,使其适配iOS和macOS,同时也兼容了Android; 85 | 3. 增加了更多的选项,使其能在多种情况下使用; 86 | 87 | ### 安装依赖 88 | ``` 89 | Python版本>=3.6 90 | pip install loguru 91 | pip install click 92 | ``` 93 | ### Usage 94 | ```shell 95 | python3 ./ssl_logger.py -U -f com.bfc.mm 96 | python3 ./ssl_logger.py -v -p test.pcap 6666 97 | ```` 98 | 99 | -------------------------------------------------------------------------------- /myhexdump.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # !/usr/bin/env python 3 | # -*- coding: latin-1 -*- 4 | 5 | # <-- removing this magic comment breaks Python 3.4 on Windows 6 | """ 7 | 1. Dump binary data to the following text format: 8 | 9 | 00000000: 00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... 10 | 00000010: 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF .."3DUfw........ 11 | 12 | It is similar to the one used by: 13 | Scapy 14 | 00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... 15 | 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF .."3DUfw........ 16 | 17 | Far Manager 18 | 000000000: 00 00 00 5B 68 65 78 64 ¦ 75 6D 70 5D 00 00 00 00 [hexdump] 19 | 000000010: 00 11 22 33 44 55 66 77 ¦ 88 99 AA BB CC DD EE FF ?"3DUfwˆ™ª»ÌÝîÿ 20 | 21 | 22 | 2. Restore binary data from the formats above as well 23 | as from less exotic strings of raw hex 24 | 25 | """ 26 | 27 | __version__ = '3.3' 28 | __author__ = 'anatoly techtonik ' 29 | __license__ = 'Public Domain' 30 | 31 | __history__ = \ 32 | """ 33 | 3.3 (2015-01-22) 34 | * accept input from sys.stdin if "-" is specified 35 | for both dump and restore (issue #1) 36 | * new normalize_py() helper to set sys.stdout to 37 | binary mode on Windows 38 | 39 | 3.2 (2015-07-02) 40 | * hexdump is now packaged as .zip on all platforms 41 | (on Linux created archive was tar.gz) 42 | * .zip is executable! try `python hexdump-3.2.zip` 43 | * dump() now accepts configurable separator, patch 44 | by Ian Land (PR #3) 45 | 46 | 3.1 (2014-10-20) 47 | * implemented workaround against mysterious coding 48 | issue with Python 3 (see revision 51302cf) 49 | * fix Python 3 installs for systems where UTF-8 is 50 | not default (Windows), thanks to George Schizas 51 | (the problem was caused by reading of README.txt) 52 | 53 | 3.0 (2014-09-07) 54 | * remove unused int2byte() helper 55 | * add dehex(text) helper to convert hex string 56 | to binary data 57 | * add 'size' argument to dump() helper to specify 58 | length of chunks 59 | 60 | 2.0 (2014-02-02) 61 | * add --restore option to command line mode to get 62 | binary data back from hex dump 63 | * support saving test output with `--test logfile` 64 | * restore() from hex strings without spaces 65 | * restore() now raises TypeError if input data is 66 | not string 67 | * hexdump() and dumpgen() now don't return unicode 68 | strings in Python 2.x when generator is requested 69 | 70 | 1.0 (2013-12-30) 71 | * length of address is reduced from 10 to 8 72 | * hexdump() got new 'result' keyword argument, it 73 | can be either 'print', 'generator' or 'return' 74 | * actual dumping logic is now in new dumpgen() 75 | generator function 76 | * new dump(binary) function that takes binary data 77 | and returns string like "66 6F 72 6D 61 74" 78 | * new genchunks(mixed, size) function that chunks 79 | both sequences and file like objects 80 | 81 | 0.5 (2013-06-10) 82 | * hexdump is now also a command line utility (no 83 | restore yet) 84 | 85 | 0.4 (2013-06-09) 86 | * fix installation with Python 3 for non English 87 | versions of Windows, thanks to George Schizas 88 | 89 | 0.3 (2013-04-29) 90 | * fully Python 3 compatible 91 | 92 | 0.2 (2013-04-28) 93 | * restore() to recover binary data from a hex dump in 94 | native, Far Manager and Scapy text formats (others 95 | might work as well) 96 | * restore() is Python 3 compatible 97 | 98 | 0.1 (2013-04-28) 99 | * working hexdump() function for Python 2 100 | """ 101 | 102 | import binascii # binascii is required for Python 3 103 | import sys 104 | 105 | # --- constants 106 | PY3K = sys.version_info >= (3, 0) 107 | 108 | 109 | # --- workaround against Python consistency issues 110 | def normalize_py(): 111 | ''' Problem 001 - sys.stdout in Python is by default opened in 112 | text mode, and writes to this stdout produce corrupted binary 113 | data on Windows 114 | 115 | python -c "import sys; sys.stdout.write('_\n_')" > file 116 | python -c "print(repr(open('file', 'rb').read()))" 117 | ''' 118 | if sys.platform == "win32": 119 | # set sys.stdout to binary mode on Windows 120 | import os, msvcrt 121 | msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) 122 | 123 | 124 | # --- - chunking helpers 125 | def chunks(seq, size): 126 | '''Generator that cuts sequence (bytes, memoryview, etc.) 127 | into chunks of given size. If `seq` length is not multiply 128 | of `size`, the lengh of the last chunk returned will be 129 | less than requested. 130 | 131 | >>> list( chunks([1,2,3,4,5,6,7], 3) ) 132 | [[1, 2, 3], [4, 5, 6], [7]] 133 | ''' 134 | d, m = divmod(len(seq), size) 135 | for i in range(d): 136 | yield seq[i * size:(i + 1) * size] 137 | if m: 138 | yield seq[d * size:] 139 | 140 | 141 | def chunkread(f, size): 142 | '''Generator that reads from file like object. May return less 143 | data than requested on the last read.''' 144 | c = f.read(size) 145 | while len(c): 146 | yield c 147 | c = f.read(size) 148 | 149 | 150 | def genchunks(mixed, size): 151 | '''Generator to chunk binary sequences or file like objects. 152 | The size of the last chunk returned may be less than 153 | requested.''' 154 | if hasattr(mixed, 'read'): 155 | return chunkread(mixed, size) 156 | else: 157 | return chunks(mixed, size) 158 | 159 | 160 | # --- - /chunking helpers 161 | 162 | 163 | def dehex(hextext): 164 | """ 165 | Convert from hex string to binary data stripping 166 | whitespaces from `hextext` if necessary. 167 | """ 168 | if PY3K: 169 | return bytes.fromhex(hextext) 170 | else: 171 | hextext = "".join(hextext.split()) 172 | return hextext.decode('hex') 173 | 174 | 175 | def dump(binary, size=2, sep=' '): 176 | ''' 177 | Convert binary data (bytes in Python 3 and str in 178 | Python 2) to hex string like '00 DE AD BE EF'. 179 | `size` argument specifies length of text chunks 180 | and `sep` sets chunk separator. 181 | ''' 182 | hexstr = binascii.hexlify(binary) 183 | if PY3K: 184 | hexstr = hexstr.decode('ascii') 185 | return sep.join(chunks(hexstr.upper(), size)) 186 | 187 | 188 | def dumpgen(data, only_str): 189 | ''' 190 | Generator that produces strings: 191 | 192 | '00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................' 193 | ''' 194 | generator = genchunks(data, 16) 195 | for addr, d in enumerate(generator): 196 | line = "" 197 | if not only_str: 198 | # 00000000: 199 | line = '%08X: ' % (addr * 16) 200 | # 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 201 | dumpstr = dump(d) 202 | line += dumpstr[:8 * 3] 203 | if len(d) > 8: # insert separator if needed 204 | line += ' ' + dumpstr[8 * 3:] 205 | # ................ 206 | # calculate indentation, which may be different for the last line 207 | pad = 2 208 | if len(d) < 16: 209 | pad += 3 * (16 - len(d)) 210 | if len(d) <= 8: 211 | pad += 1 212 | line += ' ' * pad 213 | 214 | for byte in d: 215 | # printable ASCII range 0x20 to 0x7E 216 | if not PY3K: 217 | byte = ord(byte) 218 | if 0x20 <= byte <= 0x7E: 219 | line += chr(byte) 220 | else: 221 | line += '.' 222 | yield line 223 | 224 | 225 | def hexdump(data, result='print', only_str=False): 226 | ''' 227 | Transform binary data to the hex dump text format: 228 | 229 | 00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 230 | 231 | [x] data argument as a binary string 232 | [x] data argument as a file like object 233 | 234 | Returns result depending on the `result` argument: 235 | 'print' - prints line by line 236 | 'return' - returns single string 237 | 'generator' - returns generator that produces lines 238 | ''' 239 | if PY3K and type(data) == str: 240 | raise TypeError('Abstract unicode data (expected bytes sequence)') 241 | 242 | gen = dumpgen(data, only_str=only_str) 243 | if result == 'generator': 244 | return gen 245 | elif result == 'return': 246 | return '\n'.join(gen) 247 | elif result == 'print': 248 | for line in gen: 249 | print(line) 250 | else: 251 | raise ValueError('Unknown value of `result` argument') 252 | 253 | 254 | def restore(dump): 255 | ''' 256 | Restore binary data from a hex dump. 257 | [x] dump argument as a string 258 | [ ] dump argument as a line iterator 259 | 260 | Supported formats: 261 | [x] hexdump.hexdump 262 | [x] Scapy 263 | [x] Far Manager 264 | ''' 265 | minhexwidth = 2 * 16 # minimal width of the hex part - 00000... style 266 | bytehexwidth = 3 * 16 - 1 # min width for a bytewise dump - 00 00 ... style 267 | 268 | result = bytes() if PY3K else '' 269 | if type(dump) != str: 270 | raise TypeError('Invalid data for restore') 271 | 272 | text = dump.strip() # ignore surrounding empty lines 273 | for line in text.split('\n'): 274 | # strip address part 275 | addrend = line.find(':') 276 | if 0 < addrend < minhexwidth: # : is not in ascii part 277 | line = line[addrend + 1:] 278 | line = line.lstrip() 279 | # check dump type 280 | if line[2] == ' ': # 00 00 00 ... type of dump 281 | # check separator 282 | sepstart = (2 + 1) * 7 + 2 # ('00'+' ')*7+'00' 283 | sep = line[sepstart:sepstart + 3] 284 | if sep[:2] == ' ' and sep[2:] != ' ': # ...00 00 00 00... 285 | hexdata = line[:bytehexwidth + 1] 286 | elif sep[2:] == ' ': # ...00 00 | 00 00... - Far Manager 287 | hexdata = line[:sepstart] + line[sepstart + 3:bytehexwidth + 2] 288 | else: # ...00 00 00 00... - Scapy, no separator 289 | hexdata = line[:bytehexwidth] 290 | line = hexdata 291 | result += dehex(line) 292 | return result 293 | 294 | 295 | def runtest(logfile=None): 296 | '''Run hexdump tests. Requires hexfile.bin to be in the same 297 | directory as hexdump.py itself''' 298 | 299 | class TeeOutput(object): 300 | def __init__(self, stream1, stream2): 301 | self.outputs = [stream1, stream2] 302 | 303 | # -- methods from sys.stdout / sys.stderr 304 | def write(self, data): 305 | for stream in self.outputs: 306 | if PY3K: 307 | if 'b' in stream.mode: 308 | data = data.encode('utf-8') 309 | stream.write(data) 310 | stream.flush() 311 | 312 | def tell(self): 313 | raise IOError 314 | 315 | def flush(self): 316 | for stream in self.outputs: 317 | stream.flush() 318 | # --/ sys.stdout 319 | 320 | if logfile: 321 | openlog = open(logfile, 'wb') 322 | # copy stdout and stderr streams to log file 323 | savedstd = sys.stderr, sys.stdout 324 | sys.stderr = TeeOutput(sys.stderr, openlog) 325 | sys.stdout = TeeOutput(sys.stdout, openlog) 326 | 327 | def echo(msg, linefeed=True): 328 | sys.stdout.write(msg) 329 | if linefeed: 330 | sys.stdout.write('\n') 331 | 332 | expected = '''\ 333 | 00000000: 00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... 334 | 00000010: 00 11 22 33 44 55 66 77 88 99 0A BB CC DD EE FF .."3DUfw........\ 335 | ''' 336 | 337 | # get path to hexfile.bin 338 | # this doesn't work from .zip 339 | # import os.path as osp 340 | # hexfile = osp.dirname(osp.abspath(__file__)) + '/hexfile.bin' 341 | # this doesn't work either 342 | # hexfile = osp.dirname(sys.modules[__name__].__file__) + '/hexfile.bin' 343 | # this works 344 | import pkgutil 345 | bin = pkgutil.get_data('hexdump', 'data/hexfile.bin') 346 | 347 | # varios length of input data 348 | hexdump(b'zzzz' * 12) 349 | hexdump(b'o' * 17) 350 | hexdump(b'p' * 24) 351 | hexdump(b'q' * 26) 352 | # allowable character set filter 353 | hexdump(b'line\nfeed\r\ntest') 354 | hexdump(b'\x00\x00\x00\x5B\x68\x65\x78\x64\x75\x6D\x70\x5D\x00\x00\x00\x00' 355 | b'\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\x0A\xBB\xCC\xDD\xEE\xFF') 356 | print('---') 357 | # dumping file-like binary object to screen (default behavior) 358 | hexdump(bin) 359 | print('return output') 360 | hexout = hexdump(bin, result='return') 361 | assert hexout == expected, 'returned hex didn\'t match' 362 | print('return generator') 363 | hexgen = hexdump(bin, result='generator') 364 | assert next(hexgen) == expected.split('\n')[0], 'hex generator 1 didn\'t match' 365 | assert next(hexgen) == expected.split('\n')[1], 'hex generator 2 didn\'t match' 366 | 367 | # binary restore test 368 | bindata = restore( 369 | ''' 370 | 00000000: 00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... 371 | 00000010: 00 11 22 33 44 55 66 77 88 99 0A BB CC DD EE FF .."3DUfw........ 372 | ''') 373 | echo('restore check ', linefeed=False) 374 | assert bin == bindata, 'restore check failed' 375 | echo('passed') 376 | 377 | far = \ 378 | ''' 379 | 000000000: 00 00 00 5B 68 65 78 64 ¦ 75 6D 70 5D 00 00 00 00 [hexdump] 380 | 000000010: 00 11 22 33 44 55 66 77 ¦ 88 99 0A BB CC DD EE FF ?"3DUfwˆ™ª»ÌÝîÿ 381 | ''' 382 | echo('restore far format ', linefeed=False) 383 | assert bin == restore(far), 'far format check failed' 384 | echo('passed') 385 | 386 | scapy = '''\ 387 | 00 00 00 5B 68 65 78 64 75 6D 70 5D 00 00 00 00 ...[hexdump].... 388 | 00 11 22 33 44 55 66 77 88 99 0A BB CC DD EE FF .."3DUfw........ 389 | ''' 390 | echo('restore scapy format ', linefeed=False) 391 | assert bin == restore(scapy), 'scapy format check failed' 392 | echo('passed') 393 | 394 | if not PY3K: 395 | assert restore('5B68657864756D705D') == '[hexdump]', 'no space check failed' 396 | assert dump('\\\xa1\xab\x1e', sep='').lower() == '5ca1ab1e' 397 | else: 398 | assert restore('5B68657864756D705D') == b'[hexdump]', 'no space check failed' 399 | assert dump(b'\\\xa1\xab\x1e', sep='').lower() == '5ca1ab1e' 400 | 401 | print('---[test file hexdumping]---') 402 | 403 | import os 404 | import tempfile 405 | hexfile = tempfile.NamedTemporaryFile(delete=False) 406 | try: 407 | hexfile.write(bin) 408 | hexfile.close() 409 | hexdump(open(hexfile.name, 'rb')) 410 | finally: 411 | os.remove(hexfile.name) 412 | if logfile: 413 | sys.stderr, sys.stdout = savedstd 414 | openlog.close() 415 | 416 | 417 | def main(): 418 | from optparse import OptionParser 419 | parser = OptionParser(usage=''' 420 | %prog [binfile|-] 421 | %prog -r hexfile 422 | %prog --test [logfile]''', version=__version__) 423 | parser.add_option('-r', '--restore', action='store_true', 424 | help='restore binary from hex dump') 425 | parser.add_option('--test', action='store_true', help='run hexdump sanity checks') 426 | 427 | options, args = parser.parse_args() 428 | 429 | if options.test: 430 | if args: 431 | runtest(logfile=args[0]) 432 | else: 433 | runtest() 434 | elif not args or len(args) > 1: 435 | parser.print_help() 436 | sys.exit(-1) 437 | else: 438 | ## dump file 439 | if not options.restore: 440 | # [x] memory effective dump 441 | if args[0] == '-': 442 | if not PY3K: 443 | hexdump(sys.stdin) 444 | else: 445 | hexdump(sys.stdin.buffer) 446 | else: 447 | hexdump(open(args[0], 'rb')) 448 | 449 | ## restore file 450 | else: 451 | # prepare input stream 452 | if args[0] == '-': 453 | instream = sys.stdin 454 | else: 455 | if PY3K: 456 | instream = open(args[0]) 457 | else: 458 | instream = open(args[0], 'rb') 459 | 460 | # output stream 461 | # [ ] memory efficient restore 462 | if PY3K: 463 | sys.stdout.buffer.write(restore(instream.read())) 464 | else: 465 | # Windows - binary mode for sys.stdout to prevent data corruption 466 | normalize_py() 467 | sys.stdout.write(restore(instream.read())) 468 | 469 | 470 | if __name__ == '__main__': 471 | main() 472 | 473 | # [x] file restore from command line utility 474 | # [ ] write dump with LF on Windows for consistency 475 | # [ ] encoding param for hexdump()ing Python 3 str if anybody requests that 476 | 477 | # [ ] document chunking API 478 | # [ ] document hexdump API 479 | # [ ] blog about sys.stdout text mode problem on Windows 480 | -------------------------------------------------------------------------------- /pic/Sample.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0ysue/r0capture/f8f06133c505a08fca6dff147c1e4d93953ca43c/pic/Sample.PNG -------------------------------------------------------------------------------- /pic/bypass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0ysue/r0capture/f8f06133c505a08fca6dff147c1e4d93953ca43c/pic/bypass.png -------------------------------------------------------------------------------- /pic/clientcer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0ysue/r0capture/f8f06133c505a08fca6dff147c1e4d93953ca43c/pic/clientcer.png -------------------------------------------------------------------------------- /pic/difport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0ysue/r0capture/f8f06133c505a08fca6dff147c1e4d93953ca43c/pic/difport.png -------------------------------------------------------------------------------- /pic/locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0ysue/r0capture/f8f06133c505a08fca6dff147c1e4d93953ca43c/pic/locator.png -------------------------------------------------------------------------------- /pic/sslunpinningcer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0ysue/r0capture/f8f06133c505a08fca6dff147c1e4d93953ca43c/pic/sslunpinningcer.png -------------------------------------------------------------------------------- /pic/summary1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0ysue/r0capture/f8f06133c505a08fca6dff147c1e4d93953ca43c/pic/summary1.jpg -------------------------------------------------------------------------------- /pic/summary2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r0ysue/r0capture/f8f06133c505a08fca6dff147c1e4d93953ca43c/pic/summary2.jpg -------------------------------------------------------------------------------- /r0capture.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. All Rights Reserved. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Decrypts and logs a process's SSL traffic. 16 | Hooks the functions SSL_read() and SSL_write() in a given process and logs the 17 | decrypted data to the console and/or to a pcap file. 18 | Typical usage example: 19 | ssl_log("wget", "log.pcap", True) 20 | Dependencies: 21 | frida (https://www.frida.re/): 22 | sudo pip install frida 23 | hexdump (https://bitbucket.org/techtonik/hexdump/) if using verbose output: 24 | sudo pip install hexdump 25 | """ 26 | 27 | __author__ = "geffner@google.com (Jason Geffner)" 28 | __version__ = "2.0" 29 | 30 | """ 31 | # r0capture 32 | 33 | ID: r0ysue 34 | 35 | 安卓应用层抓包通杀脚本 36 | 37 | https://github.com/r0ysue/r0capture 38 | 39 | ## 简介 40 | 41 | - 仅限安卓平台,测试安卓7、8、9、10 可用 ; 42 | - 无视所有证书校验或绑定,无视任何证书; 43 | - 通杀TCP/IP四层模型中的应用层中的全部协议; 44 | - 通杀协议包括:Http,WebSocket,Ftp,Xmpp,Imap,Smtp,Protobuf等等、以及它们的SSL版本; 45 | - 通杀所有应用层框架,包括HttpUrlConnection、Okhttp1/3/4、Retrofit/Volley等等; 46 | """ 47 | 48 | # Windows版本需要安装库: 49 | # pip install 'win_inet_pton' 50 | # pip install hexdump 51 | import argparse 52 | import os 53 | import pprint 54 | import random 55 | import signal 56 | import socket 57 | import struct 58 | import sys 59 | import time 60 | from pathlib import Path 61 | 62 | import frida 63 | from loguru import logger 64 | 65 | try: 66 | if os.name == 'nt': 67 | import win_inet_pton 68 | except ImportError: 69 | # win_inet_pton import error 70 | pass 71 | 72 | try: 73 | import myhexdump as hexdump # pylint: disable=g-import-not-at-top 74 | except ImportError: 75 | pass 76 | try: 77 | from shutil import get_terminal_size as get_terminal_size 78 | except: 79 | try: 80 | from backports.shutil_get_terminal_size import get_terminal_size as get_terminal_size 81 | except: 82 | pass 83 | 84 | try: 85 | import click 86 | except: 87 | class click: 88 | @staticmethod 89 | def secho(message=None, **kwargs): 90 | print(message) 91 | 92 | @staticmethod 93 | def style(**kwargs): 94 | raise Exception("unsupported style") 95 | banner = """ 96 | -------------------------------------------------------------------------------------------- 97 | .oooo. . 98 | d8P'`Y8b .o8 99 | oooo d8b 888 888 .ooooo. .oooo. oo.ooooo. .o888oo oooo oooo oooo d8b .ooooo. 100 | `888""8P 888 888 d88' `"Y8 `P )88b 888' `88b 888 `888 `888 `888""8P d88' `88b 101 | 888 888 888 888 .oP"888 888 888 888 888 888 888 888ooo888 102 | 888 `88b d88' 888 .o8 d8( 888 888 888 888 . 888 888 888 888 .o 103 | d888b `Y8bd8P' `Y8bod8P' `Y888""8o 888bod8P' "888" `V88V"V8P' d888b `Y8bod8P' 104 | 888 105 | o888o 106 | https://github.com/r0ysue/r0capture 107 | --------------------------------------------------------------------------------------------\n 108 | """ 109 | 110 | 111 | def show_banner(): 112 | colors = ['bright_red', 'bright_green', 'bright_blue', 'cyan', 'magenta'] 113 | try: 114 | click.style('color test', fg='bright_red') 115 | except: 116 | colors = ['red', 'green', 'blue', 'cyan', 'magenta'] 117 | try: 118 | columns = get_terminal_size().columns 119 | if columns >= len(banner.splitlines()[1]): 120 | for line in banner.splitlines(): 121 | click.secho(line, fg=random.choice(colors)) 122 | except: 123 | pass 124 | 125 | 126 | # ssl_session[] = (, 127 | # ) 128 | ssl_sessions = {} 129 | 130 | 131 | def ssl_log(process, pcap=None, host=False, verbose=False, isUsb=False, ssllib="", isSpawn=True, wait=0): 132 | """Decrypts and logs a process's SSL traffic. 133 | Hooks the functions SSL_read() and SSL_write() in a given process and logs 134 | the decrypted data to the console and/or to a pcap file. 135 | Args: 136 | process: The target process's name (as a string) or process ID (as an int). 137 | pcap: The file path to which the pcap file should be written. 138 | verbose: If True, log the decrypted traffic to the console. 139 | Raises: 140 | NotImplementedError: Not running on a Linux or macOS system. 141 | """ 142 | 143 | # if platform.system() not in ("Darwin", "Linux"): 144 | # raise NotImplementedError("This function is only implemented for Linux and " 145 | # "macOS systems.") 146 | 147 | def log_pcap(pcap_file, ssl_session_id, function, src_addr, src_port, 148 | dst_addr, dst_port, data): 149 | """Writes the captured data to a pcap file. 150 | Args: 151 | pcap_file: The opened pcap file. 152 | ssl_session_id: The SSL session ID for the communication. 153 | function: The function that was intercepted ("SSL_read" or "SSL_write"). 154 | src_addr: The source address of the logged packet. 155 | src_port: The source port of the logged packet. 156 | dst_addr: The destination address of the logged packet. 157 | dst_port: The destination port of the logged packet. 158 | data: The decrypted packet data. 159 | """ 160 | t = time.time() 161 | 162 | if ssl_session_id not in ssl_sessions: 163 | ssl_sessions[ssl_session_id] = (random.randint(0, 0xFFFFFFFF), 164 | random.randint(0, 0xFFFFFFFF)) 165 | client_sent, server_sent = ssl_sessions[ssl_session_id] 166 | 167 | if function == "SSL_read": 168 | seq, ack = (server_sent, client_sent) 169 | else: 170 | seq, ack = (client_sent, server_sent) 171 | 172 | for writes in ( 173 | # PCAP record (packet) header 174 | ("=I", int(t)), # Timestamp seconds 175 | ("=I", int((t * 1000000) % 1000000)), # Timestamp microseconds 176 | ("=I", 40 + len(data)), # Number of octets saved 177 | ("=i", 40 + len(data)), # Actual length of packet 178 | # IPv4 header 179 | (">B", 0x45), # Version and Header Length 180 | (">B", 0), # Type of Service 181 | (">H", 40 + len(data)), # Total Length 182 | (">H", 0), # Identification 183 | (">H", 0x4000), # Flags and Fragment Offset 184 | (">B", 0xFF), # Time to Live 185 | (">B", 6), # Protocol 186 | (">H", 0), # Header Checksum 187 | (">I", src_addr), # Source Address 188 | (">I", dst_addr), # Destination Address 189 | # TCP header 190 | (">H", src_port), # Source Port 191 | (">H", dst_port), # Destination Port 192 | (">I", seq), # Sequence Number 193 | (">I", ack), # Acknowledgment Number 194 | (">H", 0x5018), # Header Length and Flags 195 | (">H", 0xFFFF), # Window Size 196 | (">H", 0), # Checksum 197 | (">H", 0)): # Urgent Pointer 198 | pcap_file.write(struct.pack(writes[0], writes[1])) 199 | pcap_file.write(data) 200 | 201 | if function == "SSL_read": 202 | server_sent += len(data) 203 | else: 204 | client_sent += len(data) 205 | ssl_sessions[ssl_session_id] = (client_sent, server_sent) 206 | 207 | def on_message(message, data): 208 | """Callback for errors and messages sent from Frida-injected JavaScript. 209 | Logs captured packet data received from JavaScript to the console and/or a 210 | pcap file. See https://www.frida.re/docs/messages/ for more detail on 211 | Frida's messages. 212 | Args: 213 | message: A dictionary containing the message "type" and other fields 214 | dependent on message type. 215 | data: The string of captured decrypted data. 216 | """ 217 | if message["type"] == "error": 218 | logger.info(f"{message}") 219 | os.kill(os.getpid(), signal.SIGTERM) 220 | return 221 | if len(data) == 1: 222 | logger.info(f'{message["payload"]["function"]}') 223 | logger.info(f'{message["payload"]["stack"]}') 224 | return 225 | p = message["payload"] 226 | if verbose: 227 | src_addr = socket.inet_ntop(socket.AF_INET, 228 | struct.pack(">I", p["src_addr"])) 229 | dst_addr = socket.inet_ntop(socket.AF_INET, 230 | struct.pack(">I", p["dst_addr"])) 231 | session_id = p['ssl_session_id'] 232 | logger.info(f"SSL Session: {session_id}") 233 | logger.info("[%s] %s:%d --> %s:%d" % ( 234 | p["function"], 235 | src_addr, 236 | p["src_port"], 237 | dst_addr, 238 | p["dst_port"])) 239 | gen = hexdump.hexdump(data, result="generator",only_str=True) 240 | str_gen = ''.join(gen) 241 | logger.info(f"{str_gen}") 242 | logger.info(f"{p['stack']}") 243 | if pcap: 244 | log_pcap(pcap_file, p["ssl_session_id"], p["function"], p["src_addr"], 245 | p["src_port"], p["dst_addr"], p["dst_port"], data) 246 | 247 | if isUsb: 248 | try: 249 | device = frida.get_usb_device() 250 | except: 251 | device = frida.get_remote_device() 252 | else: 253 | if host: 254 | manager = frida.get_device_manager() 255 | device = manager.add_remote_device(host) 256 | else: 257 | device = frida.get_local_device() 258 | 259 | if isSpawn: 260 | pid = device.spawn([process]) 261 | time.sleep(1) 262 | session = device.attach(pid) 263 | time.sleep(1) 264 | device.resume(pid) 265 | else: 266 | print("attach") 267 | session = device.attach(process) 268 | if wait > 0: 269 | print(f"wait for {wait} seconds") 270 | time.sleep(wait) 271 | 272 | # session = frida.attach(process) 273 | 274 | # pid = device.spawn([process]) 275 | # pid = process 276 | # session = device.attach(pid) 277 | # device.resume(pid) 278 | if pcap: 279 | pcap_file = open(pcap, "wb", 0) 280 | for writes in ( 281 | ("=I", 0xa1b2c3d4), # Magic number 282 | ("=H", 2), # Major version number 283 | ("=H", 4), # Minor version number 284 | ("=i", time.timezone), # GMT to local correction 285 | ("=I", 0), # Accuracy of timestamps 286 | ("=I", 65535), # Max length of captured packets 287 | ("=I", 228)): # Data link type (LINKTYPE_IPV4) 288 | pcap_file.write(struct.pack(writes[0], writes[1])) 289 | 290 | with open(Path(__file__).resolve().parent.joinpath("./script.js"), encoding="utf-8") as f: 291 | _FRIDA_SCRIPT = f.read() 292 | # _FRIDA_SCRIPT = session.create_script(content) 293 | # print(_FRIDA_SCRIPT) 294 | script = session.create_script(_FRIDA_SCRIPT) 295 | script.on("message", on_message) 296 | script.load() 297 | 298 | if ssllib != "": 299 | script.exports.setssllib(ssllib) 300 | 301 | print("Press Ctrl+C to stop logging.") 302 | 303 | def stoplog(signum, frame): 304 | print('You have stoped logging.') 305 | session.detach() 306 | if pcap: 307 | pcap_file.flush() 308 | pcap_file.close() 309 | exit() 310 | 311 | signal.signal(signal.SIGINT, stoplog) 312 | signal.signal(signal.SIGTERM, stoplog) 313 | sys.stdin.read() 314 | 315 | 316 | if __name__ == "__main__": 317 | show_banner() 318 | 319 | 320 | class ArgParser(argparse.ArgumentParser): 321 | 322 | def error(self, message): 323 | print("ssl_logger v" + __version__) 324 | print("by " + __author__) 325 | print("Modified by BigFaceCat") 326 | print("Error: " + message) 327 | print() 328 | print(self.format_help().replace("usage:", "Usage:")) 329 | self.exit(0) 330 | 331 | 332 | parser = ArgParser( 333 | add_help=False, 334 | description="Decrypts and logs a process's SSL traffic.", 335 | formatter_class=argparse.RawDescriptionHelpFormatter, 336 | epilog=r""" 337 | Examples: 338 | %(prog)s -pcap ssl.pcap openssl 339 | %(prog)s -verbose 31337 340 | %(prog)s -pcap log.pcap -verbose wget 341 | %(prog)s -pcap log.pcap -ssl "*libssl.so*" com.bigfacecat.testdemo 342 | """) 343 | 344 | args = parser.add_argument_group("Arguments") 345 | args.add_argument("-pcap", '-p', metavar="", required=False, 346 | help="Name of PCAP file to write") 347 | args.add_argument("-host", '-H', metavar="<192.168.1.1:27042>", required=False, 348 | help="connect to remote frida-server on HOST") 349 | args.add_argument("-verbose", "-v", required=False, action="store_const", default=True, 350 | const=True, help="Show verbose output") 351 | args.add_argument("process", metavar="", 352 | help="Process whose SSL calls to log") 353 | args.add_argument("-ssl", default="", metavar="", 354 | help="SSL library to hook") 355 | args.add_argument("--isUsb", "-U", default=False, action="store_true", 356 | help="connect to USB device") 357 | args.add_argument("--isSpawn", "-f", default=False, action="store_true", 358 | help="if spawned app") 359 | args.add_argument("-wait", "-w", type=int, metavar="", default=0, 360 | help="Time to wait for the process") 361 | 362 | parsed = parser.parse_args() 363 | logger.add(f"{parsed.process.replace('.','_')}-{int(time.time())}.log", rotation="500MB", encoding="utf-8", enqueue=True, retention="10 days") 364 | 365 | ssl_log( 366 | int(parsed.process) if parsed.process.isdigit() else parsed.process, 367 | parsed.pcap, 368 | parsed.host, 369 | parsed.verbose, 370 | isUsb=parsed.isUsb, 371 | isSpawn=parsed.isSpawn, 372 | ssllib=parsed.ssl, 373 | wait=parsed.wait 374 | ) 375 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Initializes 'addresses' dictionary and NativeFunctions. 3 | */ 4 | "use strict"; 5 | rpc.exports = { 6 | setssllib: function (name) { 7 | console.log("setSSLLib => " + name); 8 | libname = name; 9 | initializeGlobals(); 10 | return; 11 | } 12 | }; 13 | 14 | var addresses = {}; 15 | var SSL_get_fd = null; 16 | var SSL_get_session = null; 17 | var SSL_SESSION_get_id = null; 18 | var getpeername = null; 19 | var getsockname = null; 20 | var ntohs = null; 21 | var ntohl = null; 22 | var SSLstackwrite = null; 23 | var SSLstackread = null; 24 | 25 | var libname = "*libssl*"; 26 | 27 | function uuid(len, radix) { 28 | var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); 29 | var uuid = [], i; 30 | radix = radix || chars.length; 31 | 32 | if (len) { 33 | // Compact form 34 | for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix]; 35 | } else { 36 | // rfc4122, version 4 form 37 | var r; 38 | 39 | // rfc4122 requires these characters 40 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; 41 | uuid[14] = '4'; 42 | 43 | // Fill in random data. At i==19 set the high bits of clock sequence as 44 | // per rfc4122, sec. 4.1.5 45 | for (i = 0; i < 36; i++) { 46 | if (!uuid[i]) { 47 | r = 0 | Math.random() * 16; 48 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; 49 | } 50 | } 51 | } 52 | 53 | return uuid.join(''); 54 | } 55 | function return_zero(args) { 56 | return 0; 57 | } 58 | function initializeGlobals() { 59 | var resolver = new ApiResolver("module"); 60 | var exps = [ 61 | [Process.platform == "darwin" ? "*libboringssl*" : "*libssl*", ["SSL_read", "SSL_write", "SSL_get_fd", "SSL_get_session", "SSL_SESSION_get_id"]], // for ios and Android 62 | [Process.platform == "darwin" ? "*libsystem*" : "*libc*", ["getpeername", "getsockname", "ntohs", "ntohl"]] 63 | ]; 64 | // console.log(exps) 65 | for (var i = 0; i < exps.length; i++) { 66 | var lib = exps[i][0]; 67 | var names = exps[i][1]; 68 | for (var j = 0; j < names.length; j++) { 69 | var name = names[j]; 70 | // console.log("exports:" + lib + "!" + name) 71 | var matches = resolver.enumerateMatchesSync("exports:" + lib + "!" + name); 72 | if (matches.length == 0) { 73 | if (name == "SSL_get_fd") { 74 | addresses["SSL_get_fd"] = 0; 75 | continue; 76 | } 77 | throw "Could not find " + lib + "!" + name; 78 | } 79 | else if (matches.length != 1) { 80 | // Sometimes Frida returns duplicates. 81 | var address = 0; 82 | var s = ""; 83 | var duplicates_only = true; 84 | for (var k = 0; k < matches.length; k++) { 85 | if (s.length != 0) { 86 | s += ", "; 87 | } 88 | s += matches[k].name + "@" + matches[k].address; 89 | if (address == 0) { 90 | address = matches[k].address; 91 | } 92 | else if (!address.equals(matches[k].address)) { 93 | duplicates_only = false; 94 | } 95 | } 96 | if (!duplicates_only) { 97 | throw "More than one match found for " + lib + "!" + name + ": " + s; 98 | } 99 | } 100 | addresses[name] = matches[0].address; 101 | } 102 | } 103 | if (addresses["SSL_get_fd"] == 0) { 104 | SSL_get_fd = return_zero; 105 | } else { 106 | SSL_get_fd = new NativeFunction(addresses["SSL_get_fd"], "int", ["pointer"]); 107 | } 108 | SSL_get_session = new NativeFunction(addresses["SSL_get_session"], "pointer", ["pointer"]); 109 | SSL_SESSION_get_id = new NativeFunction(addresses["SSL_SESSION_get_id"], "pointer", ["pointer", "pointer"]); 110 | getpeername = new NativeFunction(addresses["getpeername"], "int", ["int", "pointer", "pointer"]); 111 | getsockname = new NativeFunction(addresses["getsockname"], "int", ["int", "pointer", "pointer"]); 112 | ntohs = new NativeFunction(addresses["ntohs"], "uint16", ["uint16"]); 113 | ntohl = new NativeFunction(addresses["ntohl"], "uint32", ["uint32"]); 114 | } 115 | initializeGlobals(); 116 | 117 | function ipToNumber(ip) { 118 | var num = 0; 119 | if (ip == "") { 120 | return num; 121 | } 122 | var aNum = ip.split("."); 123 | if (aNum.length != 4) { 124 | return num; 125 | } 126 | num += parseInt(aNum[0]) << 0; 127 | num += parseInt(aNum[1]) << 8; 128 | num += parseInt(aNum[2]) << 16; 129 | num += parseInt(aNum[3]) << 24; 130 | num = num >>> 0;//这个很关键,不然可能会出现负数的情况 131 | return num; 132 | } 133 | 134 | /** 135 | * Returns a dictionary of a sockfd's "src_addr", "src_port", "dst_addr", and 136 | * "dst_port". 137 | * @param {int} sockfd The file descriptor of the socket to inspect. 138 | * @param {boolean} isRead If true, the context is an SSL_read call. If 139 | * false, the context is an SSL_write call. 140 | * @return {dict} Dictionary of sockfd's "src_addr", "src_port", "dst_addr", 141 | * and "dst_port". 142 | */ 143 | function getPortsAndAddresses(sockfd, isRead) { 144 | var message = {}; 145 | var src_dst = ["src", "dst"]; 146 | for (var i = 0; i < src_dst.length; i++) { 147 | if ((src_dst[i] == "src") ^ isRead) { 148 | var sockAddr = Socket.localAddress(sockfd) 149 | } 150 | else { 151 | var sockAddr = Socket.peerAddress(sockfd) 152 | } 153 | if (sockAddr == null) { 154 | // 网络超时or其他原因可能导致socket被关闭 155 | message[src_dst[i] + "_port"] = 0 156 | message[src_dst[i] + "_addr"] = 0 157 | } else { 158 | message[src_dst[i] + "_port"] = (sockAddr.port & 0xFFFF) 159 | message[src_dst[i] + "_addr"] = ntohl(ipToNumber(sockAddr.ip.split(":").pop())) 160 | } 161 | } 162 | return message; 163 | } 164 | /** 165 | * Get the session_id of SSL object and return it as a hex string. 166 | * @param {!NativePointer} ssl A pointer to an SSL object. 167 | * @return {dict} A string representing the session_id of the SSL object's 168 | * SSL_SESSION. For example, 169 | * "59FD71B7B90202F359D89E66AE4E61247954E28431F6C6AC46625D472FF76336". 170 | */ 171 | function getSslSessionId(ssl) { 172 | var session = SSL_get_session(ssl); 173 | if (session == 0) { 174 | return 0; 175 | } 176 | var len = Memory.alloc(4); 177 | var p = SSL_SESSION_get_id(session, len); 178 | len = Memory.readU32(len); 179 | var session_id = ""; 180 | for (var i = 0; i < len; i++) { 181 | // Read a byte, convert it to a hex string (0xAB ==> "AB"), and append 182 | // it to session_id. 183 | session_id += 184 | ("0" + Memory.readU8(p.add(i)).toString(16).toUpperCase()).substr(-2); 185 | } 186 | return session_id; 187 | } 188 | 189 | Interceptor.attach(addresses["SSL_read"], 190 | { 191 | onEnter: function (args) { 192 | var message = getPortsAndAddresses(SSL_get_fd(args[0]), true); 193 | message["ssl_session_id"] = getSslSessionId(args[0]); 194 | message["function"] = "SSL_read"; 195 | message["stack"] = SSLstackread; 196 | this.message = message; 197 | this.buf = args[1]; 198 | }, 199 | onLeave: function (retval) { 200 | retval |= 0; // Cast retval to 32-bit integer. 201 | if (retval <= 0) { 202 | return; 203 | } 204 | send(this.message, Memory.readByteArray(this.buf, retval)); 205 | } 206 | }); 207 | 208 | Interceptor.attach(addresses["SSL_write"], 209 | { 210 | onEnter: function (args) { 211 | var message = getPortsAndAddresses(SSL_get_fd(args[0]), false); 212 | message["ssl_session_id"] = getSslSessionId(args[0]); 213 | message["function"] = "SSL_write"; 214 | message["stack"] = SSLstackwrite; 215 | send(message, Memory.readByteArray(args[1], parseInt(args[2]))); 216 | }, 217 | onLeave: function (retval) { 218 | } 219 | }); 220 | 221 | if (Java.available) { 222 | Java.perform(function () { 223 | function storeP12(pri, p7, p12Path, p12Password) { 224 | var X509Certificate = Java.use("java.security.cert.X509Certificate") 225 | var p7X509 = Java.cast(p7, X509Certificate); 226 | var chain = Java.array("java.security.cert.X509Certificate", [p7X509]) 227 | var ks = Java.use("java.security.KeyStore").getInstance("PKCS12", "BC"); 228 | ks.load(null, null); 229 | ks.setKeyEntry("client", pri, Java.use('java.lang.String').$new(p12Password).toCharArray(), chain); 230 | try { 231 | var out = Java.use("java.io.FileOutputStream").$new(p12Path); 232 | ks.store(out, Java.use('java.lang.String').$new(p12Password).toCharArray()) 233 | } catch (exp) { 234 | console.log(exp) 235 | } 236 | } 237 | //在服务器校验客户端的情形下,帮助dump客户端证书,并保存为p12的格式,证书密码为r0ysue 238 | Java.use("java.security.KeyStore$PrivateKeyEntry").getPrivateKey.implementation = function () { 239 | var result = this.getPrivateKey() 240 | var packageName = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext().getPackageName(); 241 | storeP12(this.getPrivateKey(), this.getCertificate(), '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12', 'r0ysue'); 242 | var message = {}; 243 | message["function"] = "dumpClinetCertificate=>" + '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12' + ' pwd: r0ysue'; 244 | message["stack"] = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()); 245 | var data = Memory.alloc(1); 246 | send(message, Memory.readByteArray(data, 1)) 247 | return result; 248 | } 249 | Java.use("java.security.KeyStore$PrivateKeyEntry").getCertificateChain.implementation = function () { 250 | var result = this.getCertificateChain() 251 | var packageName = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext().getPackageName(); 252 | storeP12(this.getPrivateKey(), this.getCertificate(), '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12', 'r0ysue'); 253 | var message = {}; 254 | message["function"] = "dumpClinetCertificate=>" + '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12' + ' pwd: r0ysue'; 255 | message["stack"] = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()); 256 | var data = Memory.alloc(1); 257 | send(message, Memory.readByteArray(data, 1)) 258 | return result; 259 | } 260 | 261 | //SSLpinning helper 帮助定位证书绑定的关键代码a 262 | Java.use("java.io.File").$init.overload('java.io.File', 'java.lang.String').implementation = function (file, cert) { 263 | var result = this.$init(file, cert) 264 | var stack = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()); 265 | if (file.getPath().indexOf("cacert") >= 0 && stack.indexOf("X509TrustManagerExtensions.checkServerTrusted") >= 0) { 266 | var message = {}; 267 | message["function"] = "SSLpinning position locator => " + file.getPath() + " " + cert; 268 | message["stack"] = stack; 269 | var data = Memory.alloc(1); 270 | send(message, Memory.readByteArray(data, 1)) 271 | } 272 | return result; 273 | } 274 | 275 | 276 | Java.use("java.net.SocketOutputStream").socketWrite0.overload('java.io.FileDescriptor', '[B', 'int', 'int').implementation = function (fd, bytearry, offset, byteCount) { 277 | var result = this.socketWrite0(fd, bytearry, offset, byteCount); 278 | var message = {}; 279 | message["function"] = "HTTP_send"; 280 | message["ssl_session_id"] = ""; 281 | message["src_addr"] = ntohl(ipToNumber((this.socket.value.getLocalAddress().toString().split(":")[0]).split("/").pop())); 282 | message["src_port"] = parseInt(this.socket.value.getLocalPort().toString()); 283 | message["dst_addr"] = ntohl(ipToNumber((this.socket.value.getRemoteSocketAddress().toString().split(":")[0]).split("/").pop())); 284 | message["dst_port"] = parseInt(this.socket.value.getRemoteSocketAddress().toString().split(":").pop()); 285 | message["stack"] = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString(); 286 | var ptr = Memory.alloc(byteCount); 287 | for (var i = 0; i < byteCount; ++i) 288 | Memory.writeS8(ptr.add(i), bytearry[offset + i]); 289 | send(message, Memory.readByteArray(ptr, byteCount)) 290 | return result; 291 | } 292 | Java.use("java.net.SocketInputStream").socketRead0.overload('java.io.FileDescriptor', '[B', 'int', 'int', 'int').implementation = function (fd, bytearry, offset, byteCount, timeout) { 293 | var result = this.socketRead0(fd, bytearry, offset, byteCount, timeout); 294 | var message = {}; 295 | message["function"] = "HTTP_recv"; 296 | message["ssl_session_id"] = ""; 297 | message["src_addr"] = ntohl(ipToNumber((this.socket.value.getRemoteSocketAddress().toString().split(":")[0]).split("/").pop())); 298 | message["src_port"] = parseInt(this.socket.value.getRemoteSocketAddress().toString().split(":").pop()); 299 | message["dst_addr"] = ntohl(ipToNumber((this.socket.value.getLocalAddress().toString().split(":")[0]).split("/").pop())); 300 | message["dst_port"] = parseInt(this.socket.value.getLocalPort()); 301 | message["stack"] = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString(); 302 | if (result > 0) { 303 | var ptr = Memory.alloc(result); 304 | for (var i = 0; i < result; ++i) 305 | Memory.writeS8(ptr.add(i), bytearry[offset + i]); 306 | send(message, Memory.readByteArray(ptr, result)) 307 | } 308 | return result; 309 | } 310 | 311 | if (parseFloat(Java.androidVersion) > 8) { 312 | Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream").write.overload('[B', 'int', 'int').implementation = function (bytearry, int1, int2) { 313 | var result = this.write(bytearry, int1, int2); 314 | SSLstackwrite = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString(); 315 | return result; 316 | } 317 | Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream").read.overload('[B', 'int', 'int').implementation = function (bytearry, int1, int2) { 318 | var result = this.read(bytearry, int1, int2); 319 | SSLstackread = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString(); 320 | return result; 321 | } 322 | } 323 | else { 324 | Java.use("com.android.org.conscrypt.OpenSSLSocketImpl$SSLOutputStream").write.overload('[B', 'int', 'int').implementation = function (bytearry, int1, int2) { 325 | var result = this.write(bytearry, int1, int2); 326 | SSLstackwrite = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString(); 327 | return result; 328 | } 329 | Java.use("com.android.org.conscrypt.OpenSSLSocketImpl$SSLInputStream").read.overload('[B', 'int', 'int').implementation = function (bytearry, int1, int2) { 330 | var result = this.read(bytearry, int1, int2); 331 | SSLstackread = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString(); 332 | return result; 333 | } 334 | 335 | } 336 | } 337 | 338 | ) 339 | } 340 | --------------------------------------------------------------------------------