obfusk/fdroid-fakesigner-poc: F-Droid Fake Signer PoC

PoC for fdroidserver
AllowedAPKSigningKeys
certificate pinning bypass.
Published: 2024-04-08; updated: 2024-04-14, 2024-04-20, 2024-12-30.
We started seeing Android APK Signing Block oddities in the F-Droid request (1) on 2021-08-25; we opened the F-Droid issue “APK Signing Block considerations” (2) on 2022-10-19. No action was taken as a result.
We published the “Android APK Signing Block Payload PoC” (3) on the Reproducible Builds mailing list (4) on 2023-01-31.
But the Android APK Signature Scheme v2/v3 actually allows embedding arbitrary data (or code) in the signing block, which means that two APKs with exactly the same valid signature — even if not a bit- bit-by-bit same signing block – can work. different
Jason Donenfeld reports “Potential security risk: apk_signer_fingerprint()
looking at certifications in reverse order Android checks them” (5) on 2023-05-05; no action taken to fix this bug.
However, there is a difference between how these certificates are obtained and how Android actually implements signature checks. (…) Notice how (the google flowchart (6)) checks v3, then v2, and then v1. But the (F-Droid) code above looks at v1, then v2, and then v3, in reverse order. So v1 may have a bogus signer that some versions of Android don’t even look at, yet fdroid makes a security decision based on it. Yes! Also, it’s worth noting that apk_signer_fingerprint() also doesn’t bother to validate that the signatures are correct.
Andreas Itzchak Rehberg (IzzyOnDroid) reported about “BLOBs in APK signing blocks” in “Increasing security: more APK checks are in place in the IzzyOnDroid repo” (7) in 2024- 03-25. The accompanying German article “Android apps on the dissecting table: An in-depth look” (8) points out that we noticed that apksigner
and androguard
handle duplicate signature blocks in a different way: the former sees only the first, the latter only the last, which allows all kinds of fraud.
We have observed that embedding a v1 (JAR) signature file in an APK with minSdk >= 24
ignore on Android/apksigner
which only checks v2/v3 in that case. But, since fdroidserver
check v1 first, regardless of minSdk
and does not verify the signature, it accepts a “fake” certificate and finds a wrong fingerprint on the certificate.
We also realize that the above mentioned difference between apksigner
and
androguard
(that fdroidserver
used to obtain v2/v3 certificates) can also be abused here. Simply copying the v2/v3 signature from another APK and adding it to the APK Signing Block will not affect apksigner
the verification, however androguard
and likewise fdroidserver
only the second block is visible. Also, the signature was not verified, a “fake” certificate was accepted, and a wrong fingerprint was detected.
As a result, it is no reason to bypass AllowedAPKSigningKeys
certificate pinning, as we can fdroidserver
see any certificate we want instead of the Android one /apksigner
doing. Note that we don’t need a valid signature for the APK (we really only need a copy of the DER certificate, although having another APK signed with the certificate we want to use will make things easier).
Asked about multiple certificates in APK signatures (5), we know that, like v2/v3 signatures, v1 signatures can also contain multiple certificates (for example a certificate chain, even if it doesn’t exist. jarsigner
of
apksigner
seems to enforce any relationship between certificates). However, unlike v2/v3 — which guarantees that the certificate used for signature is always the first in the sequence — v1 does not specify an ordering: the signature block file is a PKCS#7 DER-encoded ASN.1 data structure (per RFC 2315) and uses SET for the list of certificates.
Android/apksigner
find and use the first certificate that matches the relevant one SignerInfo
doesn’t care about any other certificates, but fdroidserver
always returns the first certificate found in the signature block file. That way we can do this trick again to see any certificate we want — as long as it only checks the v1 certificate (eg if the fdroidserver.patch
not yet available or the APK only has a v1 signature).
NB: apps with targetSdk >= 30
required to have a v2/v3 signature.
NB: Android SignerInfo
later versions choose the first one that confirms if there are more.
Despite repeated warnings (5) that using the last certificate instead of the first does not in any way fix the vulnerability described in the 2024-04-14 update (PoC #3), the proposed patches for on fdroidserver
(10) and androguard
(11) do it properly. With that patch, version A (which inserted the fake certificate first) of the PoC now fails, but version B (which inserted it last) now works.
Instead of using the fixes we suggested, F-Droid wrote and compiled their own patch (10), ignoring repeated warnings that it had many errors (including an incorrect implementation in v1 signature verification and made it impossible to have APKs with rotated keys in a repository. ). As a result it is possible to establish a valid v1 signature fdroidserver
match the wrong certificate.
We do this by simply creating and prepending a second SignerInfo with our own certificate, with the same serial number and roughly the same issuer — for example a common name with a space (0x20) replaced by a tab ( 0x09) or DEL ( 0x7f) added — to exploit an implementation that matches SignerInfo against the wrong certificate through incorrect canonicalization.
Fortunately, the impact is lower than other vulnerabilities because it requires a valid signature from the certificate that a spoof wants.
Unfortunately, we found another more serious vulnerability as well, due to a regex that doesn’t properly handle newlines in file names. This allows another trivial bypass of certificate pinning, as we can now fdroidserver
see any certificate we want instead of the Android one /apksigner
(as long as we have a valid v1 signature for the other APK).
The regex in question, ^META-INF/.*\.(DSA|EC|RSA)$
must match all filenames starting with META-INF/
and end with .DSA
, .EC,
or .RSA
. Unfortunately, the .*
does not match newlines, and the $
matches not just the end of the string but “the end of the string or before the newline at the end of the string”. As a result we can use a new line in the filename of the real signature file (before the extension), which is Android/apksigner
look but
fdroidserver
no, and a new line after the .RSA
extension for spoofed signature files, which fdroidserver
see but Android/apksigner
no.
NB: androguard
seems to be using the same incorrect regex.
We can do almost the same thing with NUL bytes instead of newlines, independent of the wrong regex, because Python’s ZipInfo.filename
is sanitized by removing any NUL bytes and everything after it. It has the same result for fdroidserver
and apksigner
(which happily accepts NUL bytes in filenames) as above, but luckily Android rejects APKs with NUL bytes in filenames, and such an APK will fail to install.
NB: in the light of all the above we reiterate that we recommend the use of the official apksig
library (used by apksigner
) to verify APK signatures and return the certificate to the first signer to avoid these implementation errors and inconsistencies and thus additional vulnerabilities. Handling common cases correctly is easy, but handling edge cases correctly is difficult; it is irresponsible to roll your own implementation without the necessary skill and care to get it right.
NB: you now need the signing
branch of apksigtool
(9).
NB: the “fake” signature shown here is from the official F-Droid client (its APK has v1+v2+v3 signature), the one apksigner
see the random made of
make-key.sh
; THE app.apk
used to test have minSdk 26
and a v2 signature only. Using APKs in other signature scheme combinations is possible, but the PoC code may need to be adjusted accordingly.
$ ./make-key.sh # generates a dummy key
$ python3 make-poc-v1.py # uses app.apk (needs minSdk >= 24) as base, adds fake.apk .RSA
$ python3 fdroid.py # verifies and has fake.apk as signer according to F-Droid
True
43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab
$ python3 make-poc-v2.py # uses app.apk as base, adds signing block from fake.apk
$ python3 fdroid.py # verifies and has fake.apk as signer according to F-Droid
True
43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab
$ apksigner verify -v --print-certs poc.apk | grep -E '^Verified using|Signer #1 certificate (DN|SHA-256)'
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v4 scheme (APK Signature Scheme v4): false
Signer #1 certificate DN: CN=oops
Signer #1 certificate SHA-256 digest: 029df1354735e81eb97c9bbef2185c8ead3bc78ae874c03a6e96e1e1435ac519
$ mkdir fakesigner
$ cd fakesigner
$ fdroid init -d oops --repo-keyalias fakesigner
$ mkdir metadata
$ printf 'Name: MyApp\nAllowedAPKSigningKeys: 43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab\n' > metadata/some.app.id.yml
$ cp /path/to/poc.apk repo/
$ fdroid update
$ jq '.packages().versions().manifest.signer.sha256' < repo/index-v2.json
(
"43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab"
)
NB: version A, for fdroidserver
using the first v1 certificate.
$ python3 make-poc-v3a.py # uses app2.apk (needs targetSdk < 30) as base, adds fake.apk .RSA cert
$ python3 fdroid.py # verifies and has fake.apk as signer according to F-Droid
True
43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab
NB: version B, for fdroidserver
using the last v1 certificate.
$ python3 make-poc-v3b.py # uses app2.apk (needs targetSdk < 30) as base, adds fake.apk .RSA cert
$ python3 fdroid.py # verifies and has fake.apk as signer according to F-Droid
True
43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab
NB: for convenience we create our own key also for the spoofed certificate; for a real exploit we have a v1-signed APK to use here instead of signing one ourselves.
$ ./make-key-v4.sh # generates a dummy key
$ sha256sum cert-rsa-fake.der cert-rsa-orig.der
29c6fc6cfa20c2726721944a659a4293c5ac7e8090ab5faa8e26f64ba007bea4 cert-rsa-fake.der
1e8a45fa677f82755b63edee209fee92081ba822d4f425c3792a1980bfa3fca9 cert-rsa-orig.der
$ python3 make-poc-v4.py # uses app3.apk (needs minSdk >= 24 & targetSdk < 30)
$ python3 fdroid.py # verifies and has the wrong signer according to F-Droid
True
ERROR:root:"Signature is invalid", skipping:
1e8a45fa677f82755b63edee209fee92081ba822d4f425c3792a1980bfa3fca9
Common Name: Foo Bar
1e8a45fa677f82755b63edee209fee92081ba822d4f425c3792a1980bfa3fca9
$ apksigner verify -v --print-certs poc.apk | grep -E '^Verified using|Signer #1 certificate (DN|SHA-256)'
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): false
Verified using v3 scheme (APK Signature Scheme v3): false
Verified using v4 scheme (APK Signature Scheme v4): false
Signer #1 certificate DN: CN=Foo Bar
Signer #1 certificate SHA-256 digest: 29c6fc6cfa20c2726721944a659a4293c5ac7e8090ab5faa8e26f64ba007bea4
NB: version A uses newlines, version B NUL bytes (which makes it fail to actually install on Android devices despite verifying with apksigner
).
$ python3 make-poc-v5a.py # uses app3.apk (needs targetSdk < 30) as base, adds fake.apk .RSA
$ python3 fdroid.py # verifies and has fake.apk as signer according to F-Droid
True
43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab
$ python3 make-poc-v5b.py # uses app3.apk (needs targetSdk < 30) as base, adds fake.apk .RSA
$ python3 fdroid.py # verifies and has fake.apk as signer according to F-Droid
True
43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab
the fdroidserver.patch
reordered so it fits Android v3 before v2 before v1, and monkey patches androguard
to see the first block instead of the last if there are duplicates. It is likely incomplete, but prevents the known bypasses described here.
the fdroidserver-multicert.patch
just rejects any v1 signature with multiple certificates. It may reject some valid APKs, but handling properly is not important and there should be some APKs with multiple certificates and no v2/v3 signature in the wild (for example the IzzyOnDroid repository does not found in its catalog). We recommend using the official apksig
library (used by
apksigner
) to verify APK signatures and return the certificate to the first signer to avoid these types of implementation conflicts and thus additional vulnerabilities like this.
the fdroidserver-regex.patch
fixed the regex to handle newlines correctly.
the fdroidserver-null-v1.patch
(for fdroidserver
before the changes we recommend against) and fdroidserver-null-v2.patch
(for now
fdroidserver
) use ZipInfo.orig_filename
to handle NUL bytes properly (and avoid other potential issues).
the scan.py
script can check APKs for possible signature issues: this will flag APKs that are ambiguously signed with an ambiguous certificate, which CAN results in some sort of accidental misidentification of the signer — despite successful verification by apksigner
— which we show here. Unfortunately, such misidentification can easily happen because even the official documentation of the various signature schemes does not fully cover how Android/apksigner
handles such cases.
NB: it will also flag some valid APKs, for example those with certificate chains, those that use key rotation, or those with multiple signers; since the IzzyOnDroid repository doesn’t find anything in its catalog, these cases of luck seem a bit rare.
$ python3 scan.py poc*.apk
'poc1.apk': Mismatch between v1 and v2/v3 certificates
'poc2.apk': Duplicate block IDs
'poc3a.apk': Multiple certificates in signature block file
'poc3b.apk': Multiple certificates in signature block file
the scan.py
The script has been updated to check APK Signature Scheme v3.1 blocks (which will likely give false positives that require manual inspection because that is expected to vary with key rotation) as well as NUL / LF / CR in those filename and use ZipInfo.orig_filename
.
NB: for now, not at all fdroidserver
of androguard
see APK Signature Scheme v3.1 blocks.
$ python3 scan.py poc(45)*.apk
'poc4.apk': Multiple certificates in signature block file
'poc5a.apk': NUL, LF, or CR in filename
'poc5b.apk': NUL, LF, or CR in filename
https://opengraph.githubassets.com/cdec345f81bc354c29888e8be945fef2c93d54efb0eb045ac13131e92f849cbb/obfusk/fdroid-fakesigner-poc
2025-01-03 22:47:00