Apktool icon indicating copy to clipboard operation
Apktool copied to clipboard

[BUG] Invalid CEN header (encrypted entry)

Open carpikes opened this issue 1 month ago • 3 comments

APKtool fails to open a valid android package (which can be installed). The APK seems packed and encrypted. aapt/aapt2/android package installer correctly parses it. Unzip, apktool and other analysis tools fail to open it.

Information

  1. Apktool Version (apktool -version) - 2.12.0
  2. Operating System (Mac, Linux, Windows) - Linux
  3. APK From? (Playstore, ROM, Other) - Malware from web
  4. Java Version (java --version) - openjdk 11.0.29 2025-10-21

Stacktrace/Logcat

Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
I: Using Apktool 2.12.0-dirty on new_chrome_v7.1.3.apk with 4 threads
Exception in thread "main" brut.androlib.exceptions.AndrolibException: brut.directory.DirectoryException: java.util.zip.ZipException: invalid CEN header (encrypted entry)
	at brut.androlib.meta.ApkInfo.hasSources(ApkInfo.java:271)
	at brut.androlib.ApkDecoder.decodeSources(ApkDecoder.java:111)
	at brut.androlib.ApkDecoder.decode(ApkDecoder.java:83)
	at brut.apktool.Main.cmdDecode(Main.java:524)
	at brut.apktool.Main.main(Main.java:333)
Caused by: brut.directory.DirectoryException: java.util.zip.ZipException: invalid CEN header (encrypted entry)
	at brut.directory.ZipRODirectory.<init>(ZipRODirectory.java:54)
	at brut.directory.ZipRODirectory.<init>(ZipRODirectory.java:38)
	at brut.directory.ExtFile.getDirectory(ExtFile.java:51)
	at brut.androlib.meta.ApkInfo.hasSources(ApkInfo.java:269)
	... 4 more
Caused by: java.util.zip.ZipException: invalid CEN header (encrypted entry)
	at java.base/java.util.zip.ZipFile$Source.zerror(ZipFile.java:1776)
	at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1726)
	at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1470)
	at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1433)
	at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:743)
	at java.base/java.util.zip.ZipFile$CleanableResource.get(ZipFile.java:860)
	at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:258)
	at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:187)
	at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:201)
	at brut.directory.ZipRODirectory.<init>(ZipRODirectory.java:52)
	... 7 more

Steps to Reproduce

  1. apktool d malware.apk
  2. apktool crashes

Frameworks

If this APK is from an OEM ROM (Samsung, HTC, LG). Please attach framework files (.apks that live in /system/framework or /system/priv-app)

APK

malware.zip

If this APK can be freely shared, please upload/attach a link to it.

Questions to ask before submission

  1. Have you tried apktool d, apktool b without changing anything? - yes
  2. If you are trying to install a modified apk, did you resign it? no
  3. Are you using the latest apktool version? almost

carpikes avatar Nov 23 '25 12:11 carpikes

Dang so now we have CEN header corruption and encrypted entry - https://github.com/iBotPeaches/Apktool/issues/3953

The challenge here is the Java spec is getting too strict and all these tools that abuse tiny spec criteria that AOSP doesn't enforce is poking a hole in this. I fear we are nearing a point of needing some user-land zip parser, which sounds like an absolute nightmare.

A few years ago (or like 7) Apktool did use Apache Commons as it had a more of a robust parser for spec - https://commons.apache.org/proper/commons-compress/zip.html

iBotPeaches avatar Nov 23 '25 12:11 iBotPeaches

Dang so now we have CEN header corruption and encrypted entry - #3953

The challenge here is the Java spec is getting too strict and all these tools that abuse tiny spec criteria that AOSP doesn't enforce is poking a hole in this. I fear we are nearing a point of needing some user-land zip parser, which sounds like an absolute nightmare.

A few years ago (or like 7) Apktool did use Apache Commons as it had a more of a robust parser for spec - https://commons.apache.org/proper/commons-compress/zip.html

Ideally using the same ZIP package Android uses (modified java.util.zip or libziparchive?) would be the safest bet, but the amount of code, imports and low-level manipulation is quite messy. Android changes are tagged with "Android-changed" comments. https://android.googlesource.com/platform/libcore/+/refs/tags/android-16.0.0_r4/ojluni/src/main/java/java/util/zip Is it worth going that route for malware APKs though?

IgorEisberg avatar Dec 03 '25 17:12 IgorEisberg

I do a see a benefit of research for malware, but to your point - it seems painful to bring into Apktool.

iBotPeaches avatar Dec 03 '25 18:12 iBotPeaches

Same issue. Another apk https://drive.google.com/file/d/11oMjVJ0rjcbY6bmD5HuWhDuqoA8MbS0y/view 7z pass: apktool

apktool d 65c521753655bbd455913fcadb3bb03b0db1b6ec.apk
I: Using Apktool 2.12.1 on 65c521753655bbd455913fcadb3bb03b0db1b6ec.apk with 8 threads
Exception in thread "main" brut.androlib.exceptions.AndrolibException: brut.directory.DirectoryException: java.util.zip.ZipException: invalid CEN header (encrypted entry)
	at brut.androlib.ApkDecoder.decodeSources(SourceFile:271)
	at brut.androlib.ApkDecoder.decode(SourceFile:83)
	at brut.apktool.Main.main(SourceFile:532)
Caused by: brut.directory.DirectoryException: java.util.zip.ZipException: invalid CEN header (encrypted entry)
	at brut.directory.ZipRODirectory.<init>(SourceFile:54)
	at brut.directory.ExtFile.getDirectory(SourceFile:51)
	at brut.androlib.ApkDecoder.decodeSources(SourceFile:269)
	... 2 more
Caused by: java.util.zip.ZipException: invalid CEN header (encrypted entry)
	at java.base/java.util.zip.ZipFile$Source.zerror(ZipFile.java:1831)
	at java.base/java.util.zip.ZipFile$Source.checkAndAddEntry(ZipFile.java:1205)
	at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1767)
	at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1542)
	at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1506)
	at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:704)
	at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:204)
	at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:150)
	at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:164)
	at brut.directory.ZipRODirectory.<init>(SourceFile:52)
	... 4 more

4ft35t avatar Dec 11 '25 06:12 4ft35t

Like said above, this isn't an Apktool issue. Apktool uses the Java Standard Library to handle valid ZIP files. Those APKs were tempered with in a way only AOSP's libziparchive tolerates, but they are technically corrupted. Fix the APK first using this little script I've written, then Apktool can read it properly.

usage: apkfix.py <apkfile>

#!/usr/bin/env python3
# Applies the following changes to fields in Central Directory Entries and Local File Headers:
# - Sets encrypted file bit to 0 in the general purpose bit flag.
# - Sets compression method to STORE if it's not DEFLATE.

import sys
import struct

EOCD_SIG = b'\x50\x4b\x05\x06'
EOCD_MIN_LEN = 22
COMMENT_MAX_LEN = 0xFFFF
EOCD_MAX_SEARCH = EOCD_MIN_LEN + COMMENT_MAX_LEN
CDFH_SIG  = b'\x50\x4b\x01\x02'
CDFH_LEN = 46
LFH_SIG  = b'\x50\x4b\x03\x04'

def find_eocd(f):
    f.seek(0, 2)
    size = f.tell()
    max_search = min(size, EOCD_MAX_SEARCH)
    f.seek(-max_search, 2)
    data = f.read(max_search)
    idx = data.rfind(EOCD_SIG)
    if idx < 0:
        raise RuntimeError('EOCD not found')
    data = data[idx:]
    if len(data) < EOCD_MIN_LEN:
        raise RuntimeError('EOCD too short')
    return data

def fix_fields(f):
    flags, = struct.unpack('<H', f.read(2))
    if flags & 0x1:
        f.seek(-2, 1)
        f.write(struct.pack('<H', flags & ~0x1))
    compr, = struct.unpack('<H', f.read(2))
    if compr != 0 and compr != 8:
        f.seek(-2, 1)
        f.write(struct.pack('<H', 0))

def main():
    if len(sys.argv) != 2:
        print('usage: apkfix.py <apkfile>')
        sys.exit(1)

    with open(sys.argv[1], 'r+b') as f:
        eocd_data = find_eocd(f)
        cdfh_cnt, = struct.unpack('<H', eocd_data[10:12])
        cd_off, = struct.unpack('<I', eocd_data[16:20])
        f.seek(cd_off)
        cdfh_idx = 0
        while cdfh_idx < cdfh_cnt:
            cdfh_off = f.tell()
            cdfh_sig = f.read(4)
            if cdfh_sig != CDFH_SIG:
                raise RuntimeError('invalid CDFH')
            f.seek(4, 1)
            fix_fields(f)
            f.seek(16, 1)
            name_len, extra_len, comment_len = struct.unpack('<HHH', f.read(6))
            f.seek(8, 1)
            lfh_off, = struct.unpack('<I', f.read(4))
            f.seek(lfh_off)
            lfh_sig = f.read(4)
            if lfh_sig != LFH_SIG:
                raise RuntimeError('invalid LFH')
            f.seek(2, 1)
            fix_fields(f)
            f.seek(cdfh_off + CDFH_LEN + name_len + extra_len + comment_len)
            cdfh_idx += 1

if __name__ == '__main__':
    main()

IgorEisberg avatar Dec 13 '25 16:12 IgorEisberg

Like said above, this isn't an Apktool issue. Apktool uses the Java Standard Library to handle valid ZIP files. Those APKs were tempered with in a way only AOSP's libziparchive tolerates, but they are technically corrupted. Fix the APK first using this little script I've written, then Apktool can read it properly.

usage: apkfix.py <apkfile>

#!/usr/bin/env python3

Applies the following changes to fields in Central Directory Entries and Local File Headers:

- Sets encrypted file bit to 0 in the general purpose bit flag.

- Sets compression method to STORE if it's not DEFLATE.

import sys import struct

EOCD_SIG = b'\x50\x4b\x05\x06' EOCD_MIN_LEN = 22 COMMENT_MAX_LEN = 0xFFFF EOCD_MAX_SEARCH = EOCD_MIN_LEN + COMMENT_MAX_LEN CDFH_SIG = b'\x50\x4b\x01\x02' CDFH_LEN = 46 LFH_SIG = b'\x50\x4b\x03\x04'

def find_eocd(f): f.seek(0, 2) size = f.tell() max_search = min(size, EOCD_MAX_SEARCH) f.seek(-max_search, 2) data = f.read(max_search) idx = data.rfind(EOCD_SIG) if idx < 0: raise RuntimeError('EOCD not found') data = data[idx:] if len(data) < EOCD_MIN_LEN: raise RuntimeError('EOCD too short') return data

def fix_fields(f): flags, = struct.unpack('<H', f.read(2)) if flags & 0x1: f.seek(-2, 1) f.write(struct.pack('<H', flags & ~0x1)) compr, = struct.unpack('<H', f.read(2)) if compr != 0 and compr != 8: f.seek(-2, 1) f.write(struct.pack('<H', 0))

def main(): if len(sys.argv) != 2: print('usage: apkfix.py ') sys.exit(1)

with open(sys.argv[1], 'r+b') as f:
    eocd_data = find_eocd(f)
    cdfh_cnt, = struct.unpack('<H', eocd_data[10:12])
    cd_off, = struct.unpack('<I', eocd_data[16:20])
    f.seek(cd_off)
    cdfh_idx = 0
    while cdfh_idx < cdfh_cnt:
        cdfh_off = f.tell()
        cdfh_sig = f.read(4)
        if cdfh_sig != CDFH_SIG:
            raise RuntimeError('invalid CDFH')
        f.seek(4, 1)
        fix_fields(f)
        f.seek(16, 1)
        name_len, extra_len, comment_len = struct.unpack('<HHH', f.read(6))
        f.seek(8, 1)
        lfh_off, = struct.unpack('<I', f.read(4))
        f.seek(lfh_off)
        lfh_sig = f.read(4)
        if lfh_sig != LFH_SIG:
            raise RuntimeError('invalid LFH')
        f.seek(2, 1)
        fix_fields(f)
        f.seek(cdfh_off + CDFH_LEN + name_len + extra_len + comment_len)
        cdfh_idx += 1

if name == 'main': main()

Great, it works fine.

4ft35t avatar Dec 15 '25 01:12 4ft35t