gen_apk_testdata.sh (15275B)
1 #!/bin/sh 2 # Regenerate src/plugins/testdata/apk_test.jar and apk_test.apk. 3 # 4 # This file is in the public domain (CC0); so are the files it produces. 5 # 6 # Requirements: python3 (standard library only). No Android SDK, no JDK: 7 # the binary AndroidManifest.xml is written out by hand, and the PKCS#7 8 # signature block is a pre-generated, self-contained test certificate 9 # chain embedded below (originally made with `openssl req`/`openssl cms`, 10 # see the comment next to the blob). Everything is deterministic: run it 11 # twice and you get byte-identical files. 12 # 13 # Usage: contrib/gen_apk_testdata.sh [output-directory] 14 # (default output directory: src/plugins/testdata) 15 16 set -e 17 OUT="${1:-$(dirname "$0")/../src/plugins/testdata}" 18 mkdir -p "$OUT" 19 python3 - "$OUT" <<'PYEOF' 20 import base64, os, struct, sys, zipfile 21 22 OUT = sys.argv[1] 23 DATE = (2026, 1, 1, 0, 0, 0) # fixed: the archives must be reproducible 24 25 # ---------------------------------------------------------------- signature 26 # 27 # A PKCS#7 SignedData over a dummy .SF file, signed by a two-element test 28 # chain: "libextractor test CA" issues "libextractor test signer" 29 # (serial 0x1719). Note that the CA certificate comes *first* inside the 30 # certificates set, so a parser that simply takes the first certificate 31 # reports the wrong subject; the plugin has to resolve the SignerInfo's 32 # issuerAndSerialNumber. Regenerate with: 33 # 34 # openssl req -x509 -newkey rsa:2048 -keyout ca.key -nodes -out ca.pem \ 35 # -sha256 -set_serial 0x01 -subj "/CN=libextractor test CA/O=GNU libextractor" \ 36 # -not_before 20260101000000Z -not_after 20360101000000Z 37 # openssl req -new -newkey rsa:2048 -keyout leaf.key -nodes -out leaf.csr \ 38 # -sha256 -subj "/CN=libextractor test signer/O=GNU libextractor/OU=Testing/C=CH" 39 # openssl x509 -req -in leaf.csr -CA ca.pem -CAkey ca.key -set_serial 0x1719 \ 40 # -sha256 -not_before 20260101000000Z -not_after 20360101000000Z -out leaf.pem 41 # openssl cms -sign -binary -in CERT.SF -signer leaf.pem -inkey leaf.key \ 42 # -certfile ca.pem -outform DER -md sha256 -noattr -nosmimecap -out CERT.RSA 43 # 44 # The private keys were thrown away; this signature protects nothing. 45 PKCS7_B64 = """ 46 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 83 YOGnCcC/2rXa 84 """ 85 PKCS7 = base64.b64decode("".join(PKCS7_B64.split())) 86 SF = (b"Signature-Version: 1.0\r\n" 87 b"Created-By: libextractor testdata generator\r\n" 88 b"SHA-256-Digest-Manifest: 0000\r\n\r\n") 89 90 91 # ------------------------------------------------------------ jar manifest 92 def fold (line): 93 """Wrap a manifest header the way the JAR spec does: no line, including 94 its line break, may exceed 72 bytes, and every continuation line starts 95 with a single space that is not part of the value.""" 96 raw = line.encode ("utf-8") 97 out = raw[:70] 98 raw = raw[70:] 99 while raw: 100 out += b"\r\n " + raw[:69] 101 raw = raw[69:] 102 return out + b"\r\n" 103 104 105 JAR_HEADERS = [ 106 "Manifest-Version: 1.0", 107 "Created-By: 17.0.9+9 (Eclipse Adoptium)", 108 "Build-Jdk-Spec: 17", 109 "Build-Jdk: 17.0.9+9", 110 "Main-Class: org.gnu.libextractor.TestMain", 111 "Class-Path: lib/support.jar lib/codec.jar", 112 # deliberately longer than 72 bytes, so that reading it back proves the 113 # continuation lines are unfolded 114 "Implementation-Title: GNU libextractor JAR test archive with a very long" 115 " title that has to be folded", 116 "Implementation-Version: 1.19.0", 117 "Implementation-Vendor: GNU libextractor project", 118 "Specification-Title: libextractor plugin test specification", 119 "Specification-Version: 1.19", 120 "Specification-Vendor: GNU", 121 "Bundle-SymbolicName: org.gnu.libextractor.test", 122 "Bundle-Name: libextractor test bundle", 123 "Bundle-Version: 1.19.0.qualifier", 124 "Bundle-Vendor: GNU libextractor project", 125 "Bundle-Description: test bundle produced by gen_apk_testdata.sh", 126 "Bundle-RequiredExecutionEnvironment: JavaSE-17", 127 ] 128 MANIFEST = b"".join (fold (h) for h in JAR_HEADERS) + b"\r\n" 129 130 # a syntactically valid but empty class file (major version 61 = Java 17) 131 CLASS = struct.pack (">IHHH", 0xCAFEBABE, 0, 61, 1) + \ 132 struct.pack (">HHHHH", 0x0021, 0, 0, 0, 0) 133 134 135 def zwrite (path, members, compress = zipfile.ZIP_DEFLATED): 136 buf = [] 137 with zipfile.ZipFile (path, "w") as z: 138 for name, data, method in members: 139 zi = zipfile.ZipInfo (name, DATE) 140 zi.compress_type = method 141 zi.create_system = 0 142 z.writestr (zi, data) 143 return buf 144 145 146 zwrite (os.path.join (OUT, "apk_test.jar"), [ 147 ("META-INF/MANIFEST.MF", MANIFEST, zipfile.ZIP_DEFLATED), 148 ("META-INF/CERT.SF", SF, zipfile.ZIP_DEFLATED), 149 ("META-INF/CERT.RSA", PKCS7, zipfile.ZIP_STORED), 150 ("org/gnu/libextractor/TestMain.class", CLASS, zipfile.ZIP_DEFLATED), 151 ]) 152 153 154 # ------------------------------------------------------- binary AndroidManifest 155 # 156 # Chunk types and value types are from AOSP 157 # frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h. 158 RES_XML = 0x0003 159 RES_STRING_POOL = 0x0001 160 RES_XML_START_NAMESPACE = 0x0100 161 RES_XML_END_NAMESPACE = 0x0101 162 RES_XML_START_ELEMENT = 0x0102 163 RES_XML_END_ELEMENT = 0x0103 164 RES_XML_RESOURCE_MAP = 0x0180 165 166 TYPE_REFERENCE = 0x01 167 TYPE_STRING = 0x03 168 TYPE_INT_DEC = 0x10 169 TYPE_INT_BOOLEAN = 0x12 170 171 ANDROID_NS = "http://schemas.android.com/apk/res/android" 172 173 # Attribute resource IDs, read off a real aapt2-built APK. The resource 174 # map has to cover a prefix of the string pool, so these strings are 175 # added to the pool first and in this order. 176 ATTR_IDS = [ 177 ("label", 0x01010001), 178 ("icon", 0x01010002), 179 ("name", 0x01010003), 180 ("debuggable", 0x0101000f), 181 ("minSdkVersion", 0x0101020c), 182 ("versionCode", 0x0101021b), 183 ("versionName", 0x0101021c), 184 ("targetSdkVersion", 0x01010270), 185 ("allowBackup", 0x01010280), 186 ("usesCleartextTraffic", 0x010104ec), 187 ("networkSecurityConfig", 0x01010527), 188 ("compileSdkVersion", 0x01010572), 189 ] 190 191 192 class Pool: 193 def __init__ (self): 194 self.strings = [] 195 for name, _ in ATTR_IDS: 196 self.add (name) 197 198 def add (self, s): 199 if s in self.strings: 200 return self.strings.index (s) 201 self.strings.append (s) 202 return len (self.strings) - 1 203 204 def build (self): 205 """UTF-16LE string pool, which is what aapt emits for 206 AndroidManifest.xml.""" 207 data = b"" 208 offs = [] 209 for s in self.strings: 210 offs.append (len (data)) 211 u = s.encode ("utf-16-le") 212 assert len (s) < 0x8000 213 data += struct.pack ("<H", len (s)) + u + b"\0\0" 214 while len (data) % 4: 215 data += b"\0" 216 hdr_len = 28 + 4 * len (offs) 217 size = hdr_len + len (data) 218 return (struct.pack ("<HHIIIIII", RES_STRING_POOL, 28, size, 219 len (offs), 0, 0, hdr_len, 0) 220 + struct.pack ("<%dI" % len (offs), *offs) 221 + data) 222 223 224 pool = Pool () 225 NOSTR = 0xFFFFFFFF 226 227 228 def attr (ns, name, value, vtype): 229 """One ResXMLTree_attribute (20 bytes).""" 230 ns_i = pool.add (ns) if ns else NOSTR 231 nm_i = pool.add (name) 232 if TYPE_STRING == vtype: 233 raw = pool.add (value) 234 data = raw 235 else: 236 raw = NOSTR 237 data = value & 0xFFFFFFFF 238 return struct.pack ("<IIIHBBI", ns_i, nm_i, raw, 8, 0, vtype, data) 239 240 241 def start (name, attrs): 242 nm = pool.add (name) 243 body = b"".join (attrs) 244 size = 16 + 20 + len (body) 245 return (struct.pack ("<HHIII", RES_XML_START_ELEMENT, 16, size, 1, NOSTR) 246 + struct.pack ("<IIHHHHHH", NOSTR, nm, 20, 20, len (attrs), 247 0, 0, 0) 248 + body) 249 250 251 def end (name): 252 nm = pool.add (name) 253 return struct.pack ("<HHIIIII", RES_XML_END_ELEMENT, 16, 24, 1, NOSTR, 254 NOSTR, nm) 255 256 257 PERMISSIONS = [ 258 "android.permission.INTERNET", 259 "android.permission.ACCESS_FINE_LOCATION", 260 "android.permission.READ_SMS", 261 "android.permission.CAMERA", 262 "android.permission.RECEIVE_BOOT_COMPLETED", 263 ] 264 265 # Strings referenced by attribute values have to exist before the chunks 266 # are serialised, but `attr' adds them as it goes, so simply build the 267 # chunk list first and the pool header afterwards. 268 body = b"" 269 body += start ("manifest", [ 270 attr (ANDROID_NS, "versionCode", 1719, TYPE_INT_DEC), 271 attr (ANDROID_NS, "versionName", "1.19.0", TYPE_STRING), 272 attr (ANDROID_NS, "compileSdkVersion", 33, TYPE_INT_DEC), 273 attr (None, "package", "org.gnu.libextractor.apktest", TYPE_STRING), 274 attr (None, "platformBuildVersionCode", 33, TYPE_INT_DEC), 275 ]) 276 body += start ("uses-sdk", [ 277 attr (ANDROID_NS, "minSdkVersion", 21, TYPE_INT_DEC), 278 attr (ANDROID_NS, "targetSdkVersion", 33, TYPE_INT_DEC), 279 ]) 280 body += end ("uses-sdk") 281 for p in PERMISSIONS: 282 body += start ("uses-permission", 283 [attr (ANDROID_NS, "name", p, TYPE_STRING)]) 284 body += end ("uses-permission") 285 body += start ("application", [ 286 # a literal label, not the usual @string reference, so that the plugin 287 # has something real to report 288 attr (ANDROID_NS, "label", "libextractor APK test", TYPE_STRING), 289 # a reference, which the plugin must *not* report as a title 290 attr (ANDROID_NS, "icon", 0x7F080001, TYPE_REFERENCE), 291 attr (ANDROID_NS, "debuggable", 0xFFFFFFFF, TYPE_INT_BOOLEAN), 292 attr (ANDROID_NS, "allowBackup", 0, TYPE_INT_BOOLEAN), 293 attr (ANDROID_NS, "usesCleartextTraffic", 0xFFFFFFFF, TYPE_INT_BOOLEAN), 294 attr (ANDROID_NS, "networkSecurityConfig", 0x7F110002, TYPE_REFERENCE), 295 ]) 296 body += end ("application") 297 body += end ("manifest") 298 299 ns_prefix = pool.add ("android") 300 ns_uri = pool.add (ANDROID_NS) 301 ns_start = struct.pack ("<HHIIIII", RES_XML_START_NAMESPACE, 16, 24, 1, 302 NOSTR, ns_prefix, ns_uri) 303 ns_end = struct.pack ("<HHIIIII", RES_XML_END_NAMESPACE, 16, 24, 1, 304 NOSTR, ns_prefix, ns_uri) 305 306 resmap = struct.pack ("<HHI", RES_XML_RESOURCE_MAP, 8, 8 + 4 * len (ATTR_IDS)) 307 resmap += struct.pack ("<%dI" % len (ATTR_IDS), *[i for _, i in ATTR_IDS]) 308 309 pool_chunk = pool.build () 310 rest = pool_chunk + resmap + ns_start + body + ns_end 311 AXML = struct.pack ("<HHI", RES_XML, 8, 8 + len (rest)) + rest 312 313 DEX = b"dex\n035\0" + b"\0" * 104 314 SO = b"\x7fELF\x02\x01\x01\x00" + b"\0" * 56 315 ARSC = struct.pack ("<HHII", 0x0002, 12, 12, 0) 316 317 APK_MANIFEST = (b"Manifest-Version: 1.0\r\n" 318 b"Created-By: 1.0 (Android SignApk)\r\n\r\n") 319 320 apk = os.path.join (OUT, "apk_test.apk") 321 zwrite (apk, [ 322 # AndroidManifest.xml first, the way aapt lays an APK out 323 ("AndroidManifest.xml", AXML, zipfile.ZIP_DEFLATED), 324 ("resources.arsc", ARSC, zipfile.ZIP_STORED), 325 ("classes.dex", DEX, zipfile.ZIP_DEFLATED), 326 ("classes2.dex", DEX, zipfile.ZIP_DEFLATED), 327 ("classes3.dex", DEX, zipfile.ZIP_DEFLATED), 328 ("lib/arm64-v8a/libtest.so", SO, zipfile.ZIP_DEFLATED), 329 ("lib/armeabi-v7a/libtest.so", SO, zipfile.ZIP_DEFLATED), 330 ("lib/x86_64/libtest.so", SO, zipfile.ZIP_DEFLATED), 331 ("res/xml/network_security_config.xml", b"<network-security-config/>", 332 zipfile.ZIP_DEFLATED), 333 ("META-INF/MANIFEST.MF", APK_MANIFEST, zipfile.ZIP_DEFLATED), 334 ("META-INF/ANDROIDD.SF", SF, zipfile.ZIP_DEFLATED), 335 ("META-INF/ANDROIDD.RSA", PKCS7, zipfile.ZIP_STORED), 336 ]) 337 338 339 # ------------------------------------------------------ APK Signing Block 340 # 341 # Splice an APK Signing Block (the v2/v3 signature container) in between 342 # the last local file entry and the central directory, and move the 343 # central directory pointer in the end-of-central-directory record along 344 # with it. Local header offsets are unaffected because everything before 345 # the central directory stays where it is. 346 def sig_block (ids): 347 pairs = b"" 348 for i in ids: 349 # the value is a real signer list in a real APK; here it only has to 350 # be present and correctly framed 351 value = struct.pack ("<I", i) + b"\0" * 60 352 pairs += struct.pack ("<Q", len (value)) + value 353 # size covers the pairs, the trailing size field and the magic 354 size = len (pairs) + 8 + 16 355 return (struct.pack ("<Q", size) + pairs + struct.pack ("<Q", size) 356 + b"APK Sig Block 42") 357 358 359 APK_SIG_V2 = 0x7109871A 360 APK_SIG_V3 = 0xF05368C0 361 362 with open (apk, "rb") as f: 363 data = f.read () 364 eocd = data.rindex (b"PK\5\6") 365 cd_off = struct.unpack_from ("<I", data, eocd + 16)[0] 366 block = sig_block ([APK_SIG_V2, APK_SIG_V3]) 367 data = (data[:cd_off] + block + data[cd_off:eocd] 368 + data[eocd:eocd + 16] 369 + struct.pack ("<I", cd_off + len (block)) 370 + data[eocd + 20:]) 371 with open (apk, "wb") as f: 372 f.write (data) 373 374 for n in ("apk_test.jar", "apk_test.apk"): 375 print ("%s: %d bytes" % (n, os.path.getsize (os.path.join (OUT, n)))) 376 PYEOF