blob: fde1cb80e8877d34a180076c4e86c3ae95d8e2c3 (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
|
#!/bin/sh
# Verify the release APK's identity, alignment, native payload, and signature.
set -eu
root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
# shellcheck source=versions
. "$root/versions"
apk=${1:-$root/app/build/outputs/apk/release/app-release-unsigned.apk}
[ -f "$apk" ] || { echo "verify-apk: not found: $apk" >&2; exit 1; }
command -v readelf >/dev/null 2>&1 || { echo "verify-apk: readelf is required" >&2; exit 1; }
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' 0 1 2 3 15
entries=$(unzip -Z1 "$apk")
actual=$(printf '%s\n' "$entries" | sed -n 's|^lib/\([^/]*\)/.*|\1|p' | sort -u | tr '\n' ' ' | sed 's/ $//')
expected=$(printf '%s\n' "$ABIS" | tr ' ' '\n' | sed '/^$/d' | sort -u | tr '\n' ' ' | sed 's/ $//')
[ "$actual" = "$expected" ] || {
echo "verify-apk: native ABIs are '$actual', expected '$expected'" >&2
exit 1
}
for abi in $ABIS; do
for lib in libxrsync.so libxrsh.so; do
path="lib/$abi/$lib"
printf '%s\n' "$entries" | grep -Fqx "$path" || {
echo "verify-apk: missing lib/$abi/$lib" >&2
exit 1
}
elf="$tmp/$abi-$lib"
unzip -p "$apk" "$path" > "$elf"
header=$(readelf -W -h "$elf")
printf '%s\n' "$header" | grep -Fq 'DYN (Position-Independent Executable file)' || {
echo "verify-apk: $path is not PIE" >&2
exit 1
}
case "$abi" in
arm64-v8a) machine=AArch64 ;;
x86_64) machine='Advanced Micro Devices X86-64' ;;
*) echo "verify-apk: no machine check for $abi" >&2; exit 1 ;;
esac
actual_machine=$(printf '%s\n' "$header" | sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')
[ "$actual_machine" = "$machine" ] || {
echo "verify-apk: $path has the wrong machine type" >&2
exit 1
}
program_headers=$(readelf -W -l "$elf")
printf '%s\n' "$program_headers" | grep -Fq 'GNU_RELRO' || {
echo "verify-apk: $path has no RELRO segment" >&2
exit 1
}
load_alignment=$(printf '%s\n' "$program_headers" |
awk '$1 == "LOAD" { print $NF }' | sort -u)
[ "$load_alignment" = "0x4000" ] || {
echo "verify-apk: $path LOAD alignment is '$load_alignment', expected 0x4000" >&2
exit 1
}
stack=$(printf '%s\n' "$program_headers" | sed -n '/GNU_STACK/p')
if [ -z "$stack" ] || printf '%s\n' "$stack" | grep -Eq 'RWE|RWX'; then
echo "verify-apk: $path has an executable or missing stack marker" >&2
exit 1
fi
dynamic=$(readelf -W -d "$elf")
printf '%s\n' "$dynamic" | grep -Fq 'BIND_NOW' || {
echo "verify-apk: $path does not use immediate binding" >&2
exit 1
}
! printf '%s\n' "$dynamic" | grep -Eq 'RPATH|RUNPATH|TEXTREL' || {
echo "verify-apk: $path has an unsafe dynamic-linker flag" >&2
exit 1
}
needed=$(printf '%s\n' "$dynamic" | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p' | LC_ALL=C sort)
if [ "$lib" = libxrsh.so ]; then
expected_needed=$(printf '%s\n' libc.so libdl.so liblog.so | LC_ALL=C sort)
else
expected_needed=$(printf '%s\n' libc.so libdl.so | LC_ALL=C sort)
fi
[ "$needed" = "$expected_needed" ] || {
echo "verify-apk: $path system libraries changed:$needed" >&2
exit 1
}
sections=$(readelf -W -S "$elf")
! printf '%s\n' "$sections" | grep -Eq '\.debug_|\.symtab' || {
echo "verify-apk: $path is not stripped" >&2
exit 1
}
if [ "$lib" = libxrsync.so ]; then
symbols=$(readelf -W -s "$elf")
printf '%s\n' "$symbols" | grep -Fq '__stack_chk_fail' || {
echo "verify-apk: $path has no stack protector" >&2
exit 1
}
printf '%s\n' "$symbols" | grep -Fq '__memcpy_chk' || {
echo "verify-apk: $path has no fortified libc calls" >&2
exit 1
}
fi
done
done
native_entries=$(printf '%s\n' "$entries" | sed -n '/^lib\//p')
unexpected=
if unexpected=$(printf '%s\n' "$native_entries" |
grep -Ev '^lib/[^/]+/(libxrsync|libxrsh)\.so$'); then
:
else
status=$?
[ "$status" -eq 1 ] || exit "$status"
fi
[ -z "$unexpected" ] || { echo "verify-apk: unexpected native files:$unexpected" >&2; exit 1; }
! printf '%s\n' "$entries" | grep -Fqx META-INF/version-control-info.textproto || {
echo "verify-apk: APK contains a VCS stamp" >&2
exit 1
}
for notice in assets/APACHE-2.0 assets/LICENSE assets/THIRD_PARTY; do
printf '%s\n' "$entries" | grep -Fqx "$notice" || {
echo "verify-apk: missing $notice" >&2
exit 1
}
asset="$tmp/$(basename "$notice")"
unzip -p "$apk" "$notice" > "$asset"
cmp "$asset" "$root/$(basename "$notice")"
done
zipalign -c -P 16 4 "$apk" >/dev/null
badging=$(aapt dump badging "$apk")
printf '%s\n' "$badging" | grep -Fq "package: name='invalid.lena.rsend' versionCode='$APP_VERSION_CODE' versionName='$APP_VERSION'" || {
echo "verify-apk: package version does not match versions" >&2
exit 1
}
printf '%s\n' "$badging" | grep -Fqx "sdkVersion:'$ANDROID_MIN_SDK'" || {
echo "verify-apk: minimum SDK does not match versions" >&2
exit 1
}
printf '%s\n' "$badging" | grep -Fqx "targetSdkVersion:'$ANDROID_TARGET_SDK'" || {
echo "verify-apk: target SDK does not match versions" >&2
exit 1
}
! printf '%s\n' "$badging" | grep -Fq 'application-debuggable' || {
echo "verify-apk: release APK is debuggable" >&2
exit 1
}
permission_dump=$(aapt dump permissions "$apk")
permissions=$(printf '%s\n' "$permission_dump" |
sed -n "s/^uses-permission: name='\(.*\)'$/\1/p" | LC_ALL=C sort)
expected_permissions=$(cat <<EOF | LC_ALL=C sort
android.permission.ACCESS_NETWORK_STATE
android.permission.FOREGROUND_SERVICE
android.permission.FOREGROUND_SERVICE_DATA_SYNC
android.permission.INTERNET
android.permission.MANAGE_EXTERNAL_STORAGE
android.permission.POST_NOTIFICATIONS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
android.permission.WAKE_LOCK
invalid.lena.rsend.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
EOF
)
[ "$permissions" = "$expected_permissions" ] || {
echo "verify-apk: permission set changed:$permissions" >&2
exit 1
}
signature="$tmp/signature"
if apksigner verify --min-sdk-version "$ANDROID_MIN_SDK" --verbose "$apk" >"$signature" 2>&1; then
case "$(basename "$apk")" in
*-unsigned.apk)
echo "verify-apk: unsigned APK unexpectedly has a valid signature" >&2
exit 1
;;
esac
# At API 30 apksigner reports only v3 as used, even when valid v1 and v2
# signatures are present. API 23 makes it verify v1 for that platform and
# v2/v3 for later platforms in the same pass.
schemes="$tmp/schemes"
apksigner verify --min-sdk-version 23 --verbose "$apk" >"$schemes" 2>&1
grep -Fq 'Verified using v1 scheme (JAR signing): true' "$schemes"
grep -Fq 'Verified using v2 scheme (APK Signature Scheme v2): true' "$schemes"
grep -Fq 'Verified using v3 scheme (APK Signature Scheme v3): true' "$schemes"
echo "verify-apk: signed APK verified (v1, v2, v3)"
else
case "$(basename "$apk")" in
*-unsigned.apk) ;;
*)
cat "$signature" >&2
echo "verify-apk: signed APK failed verification" >&2
exit 1
;;
esac
! printf '%s\n' "$entries" | grep -Eq '^META-INF/.*\.(RSA|DSA|EC)$' || {
echo "verify-apk: unsigned APK contains a signature file" >&2
exit 1
}
echo "verify-apk: unsigned APK"
fi
echo "verify-apk: ok"
|