Skip to content

Commit a9bfd36

Browse files
committed
fix: eliminate persistent Android RWX mappings
1 parent a9d0ee6 commit a9bfd36

8 files changed

Lines changed: 479 additions & 27 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ on:
3434
type: boolean
3535
default: false
3636
strict_wx:
37-
description: Disable persistent anonymous RWX code pools on Android
37+
description: Harden Frida-owned persistent anonymous RWX mappings on Android
3838
required: false
3939
type: boolean
4040
default: false
@@ -76,7 +76,7 @@ on:
7676
type: boolean
7777
default: false
7878
strict_wx:
79-
description: Disable persistent anonymous RWX code pools on Android
79+
description: Harden Frida-owned persistent anonymous RWX mappings on Android
8080
required: false
8181
type: boolean
8282
default: false

.github/workflows/scheduled-build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ jobs:
161161
echo "- Custom name: ${CUSTOM_NAME}"
162162
echo "- Custom port: ${CUSTOM_PORT}"
163163
echo "- Architectures: ${ARCHITECTURES}"
164-
echo '- Persistent anonymous RWX pools: disabled'
164+
echo '- Frida-owned persistent anonymous RWX mappings: hardened'
165165
echo "- Workflow run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
166166
echo
167167
echo "Verify the downloaded files with \`sha256sum --check SHA256SUMS\`."
@@ -181,5 +181,5 @@ jobs:
181181
echo "- Custom name: ${CUSTOM_NAME}"
182182
echo "- Custom port: ${CUSTOM_PORT}"
183183
echo "- Architectures: ${ARCHITECTURES}"
184-
echo '- Persistent anonymous RWX pools: disabled'
184+
echo '- Frida-owned persistent anonymous RWX mappings: hardened'
185185
} >> "$GITHUB_STEP_SUMMARY"

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Useful options:
7979
--arch, -a One or more supported Android architectures
8080
--port, -p Listening port; omitted keeps 27042
8181
--extended, -e Apply the optional extended identifier transformations
82-
--strict-wx Disable persistent anonymous RWX code pools on Android
82+
--strict-wx Harden Frida-owned persistent anonymous RWX mappings on Android
8383
--temp-fixes Apply opt-in, device-specific stability changes
8484
--verify Reject known forbidden markers in final artifacts
8585
--skip-build Patch source without compiling
@@ -226,9 +226,12 @@ tests/ Unit, contract, fixture, and workflow tests
226226
protocol probing and code-integrity checks may still identify instrumentation.
227227
- This builder does not hide root, automated app launch, Interceptor code
228228
changes, user-script strings, or failures reported by remote attestation.
229-
- `--strict-wx` disables Gum's persistent anonymous RWX code pools. A runtime
230-
`/proc/maps` sample does not prove that no transient RWX permission change occurs
231-
while executable code is being patched.
229+
- `--strict-wx` separates the Android injector's executable code from writable
230+
data and stack pages, disables Gum's persistent RWX code pools, and finalizes
231+
newly generated code as RX. It preserves pages that were already RWX and does
232+
not reject an explicit user-script request for RWX memory. A runtime
233+
`/proc/maps` sample does not prove that no transient RWX permission change
234+
occurs while executable code is being patched.
232235
- The external memory gate scans the mapped agent/Gadget images for its explicit
233236
marker set; it is not a general-purpose scan of every anonymous heap page.
234237
- Frida 17 raw agents need explicit bridge imports or bundling. The harness uses

build.py

Lines changed: 286 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ def apply_targeted_patches(frida_dir: Path, custom_name: str, frida_major: int):
771771

772772

773773
def apply_strict_wx_patch(frida_dir: Path) -> None:
774-
"""Disable Gum's persistent anonymous RWX code pools on Android."""
774+
"""Disable persistent anonymous RWX mappings owned by Frida on Android."""
775775
allocator_boxed_types = (
776776
"G_DEFINE_BOXED_TYPE (GumCodeSlice, gum_code_slice, gum_code_slice_ref,\n"
777777
" gum_code_slice_unref)\n"
@@ -803,6 +803,24 @@ def apply_strict_wx_patch(frida_dir: Path) -> None:
803803
1,
804804
"Android allocator policy scoped",
805805
),
806+
(
807+
Path("subprojects/frida-gum/gum/gummemory.c"),
808+
" restored = ((original_protections[i] & GUM_PAGE_WRITE) != 0)\n"
809+
" ? GUM_PAGE_RWX\n"
810+
" : GUM_PAGE_RX;",
811+
"#if defined (HAVE_ANDROID)\n"
812+
" restored = ((original_protections[i] & GUM_PAGE_WRITE) != 0 &&\n"
813+
" (original_protections[i] & GUM_PAGE_EXECUTE) != 0)\n"
814+
" ? GUM_PAGE_RWX\n"
815+
" : GUM_PAGE_RX;\n"
816+
"#else\n"
817+
" restored = ((original_protections[i] & GUM_PAGE_WRITE) != 0)\n"
818+
" ? GUM_PAGE_RWX\n"
819+
" : GUM_PAGE_RX;\n"
820+
"#endif",
821+
1,
822+
"new Android code pages finish RX",
823+
),
806824
*(
807825
(
808826
Path(relative_path),
@@ -821,6 +839,272 @@ def apply_strict_wx_patch(frida_dir: Path) -> None:
821839
"subprojects/frida-gum/gum/backend-x86/gumstalker-x86.c",
822840
)
823841
),
842+
(
843+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
844+
"\t\tprivate static uint64 mmap_offset;\n\t\tprivate static uint64 munmap_offset;",
845+
"\t\tprivate static uint64 mmap_offset;\n"
846+
"\t\tprivate static uint64 mprotect_offset;\n"
847+
"\t\tprivate static uint64 munmap_offset;",
848+
1,
849+
"track remote mprotect",
850+
),
851+
(
852+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
853+
'\t\t\tmmap_offset = (uint64) (uintptr) libc.find_export_by_name ("mmap")'
854+
" - local_libc.start;\n"
855+
'\t\t\tmunmap_offset = (uint64) (uintptr) libc.find_export_by_name ("munmap")'
856+
" - local_libc.start;",
857+
'\t\t\tmmap_offset = (uint64) (uintptr) libc.find_export_by_name ("mmap")'
858+
" - local_libc.start;\n"
859+
"\t\t\tmprotect_offset = (uint64) (uintptr) libc.find_export_by_name "
860+
'("mprotect") - local_libc.start;\n'
861+
'\t\t\tmunmap_offset = (uint64) (uintptr) libc.find_export_by_name ("munmap")'
862+
" - local_libc.start;",
863+
1,
864+
"resolve remote mprotect",
865+
),
866+
(
867+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
868+
"\t\t\tuint64 loader_base = (uintptr) bres.context.allocation_base;\n"
869+
"\t\t\tGPRegs regs = saved_regs;",
870+
"\t\t\tuint64 loader_base = (uintptr) bres.context.allocation_base;\n"
871+
"#if ANDROID\n"
872+
"\t\t\tyield protect_memory ((uintptr) bres.libc.mprotect,\n"
873+
"\t\t\t\tloader_base + loader_layout.ctx_offset,\n"
874+
"\t\t\t\tloader_layout.size - loader_layout.ctx_offset,\n"
875+
"\t\t\t\tPosix.PROT_READ | Posix.PROT_WRITE, cancellable);\n"
876+
"#endif\n"
877+
"\t\t\tGPRegs regs = saved_regs;",
878+
1,
879+
"loader context is writable but not executable",
880+
),
881+
(
882+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
883+
"\t\t\tuint64 remote_mmap = 0;\n\t\t\tuint64 remote_munmap = 0;",
884+
"\t\t\tuint64 remote_mmap = 0;\n"
885+
"\t\t\tuint64 remote_mprotect = 0;\n"
886+
"\t\t\tuint64 remote_munmap = 0;",
887+
1,
888+
"track target mprotect",
889+
),
890+
(
891+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
892+
"\t\t\t\tremote_mmap = remote_libc.start + mmap_offset;\n"
893+
"\t\t\t\tremote_munmap = remote_libc.start + munmap_offset;",
894+
"\t\t\t\tremote_mmap = remote_libc.start + mmap_offset;\n"
895+
"\t\t\t\tremote_mprotect = remote_libc.start + mprotect_offset;\n"
896+
"\t\t\t\tremote_munmap = remote_libc.start + munmap_offset;",
897+
1,
898+
"locate target mprotect",
899+
),
900+
(
901+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
902+
"\t\t\tif (remote_mmap != 0) {\n"
903+
"\t\t\t\tallocation_base = yield allocate_memory (remote_mmap, allocation_size,\n"
904+
"\t\t\t\t\tPosix.PROT_READ | Posix.PROT_WRITE | Posix.PROT_EXEC, cancellable);\n"
905+
"\t\t\t} else {",
906+
"\t\t\tif (remote_mmap != 0) {\n"
907+
"#if ANDROID\n"
908+
"\t\t\t\tallocation_base = yield allocate_memory (remote_mmap, allocation_size,\n"
909+
"\t\t\t\t\tPosix.PROT_READ | Posix.PROT_EXEC, cancellable);\n"
910+
"\t\t\t\ttry {\n"
911+
"\t\t\t\t\tyield protect_memory (remote_mprotect,\n"
912+
"\t\t\t\t\t\tallocation_base + allocation_size - stack_size, stack_size,\n"
913+
"\t\t\t\t\t\tPosix.PROT_READ | Posix.PROT_WRITE, cancellable);\n"
914+
"\t\t\t\t} catch (GLib.Error e) {\n"
915+
"\t\t\t\t\ttry {\n"
916+
"\t\t\t\t\t\tyield deallocate_memory (remote_munmap, allocation_base,\n"
917+
"\t\t\t\t\t\t\tallocation_size, null);\n"
918+
"\t\t\t\t\t} catch (GLib.Error ignored) {\n"
919+
"\t\t\t\t\t}\n"
920+
"\t\t\t\t\tthrow_api_error (e);\n"
921+
"\t\t\t\t}\n"
922+
"#else\n"
923+
"\t\t\t\tallocation_base = yield allocate_memory (remote_mmap, allocation_size,\n"
924+
"\t\t\t\t\tPosix.PROT_READ | Posix.PROT_WRITE | Posix.PROT_EXEC, cancellable);\n"
925+
"#endif\n"
926+
"\t\t\t} else {",
927+
1,
928+
"bootstrap code and stack use disjoint permissions",
929+
),
930+
(
931+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
932+
"\t\t\t\tbootstrap_ctx.allocation_size = allocation_size;\n"
933+
"\t\t\t\twrite_memory (bootstrap_ctx_location, (uint8[]) &bootstrap_ctx);",
934+
"\t\t\t\tbootstrap_ctx.allocation_size = allocation_size;\n"
935+
"\t\t\t\tbootstrap_ctx.stack_size = stack_size;\n"
936+
"\t\t\t\twrite_memory (bootstrap_ctx_location, (uint8[]) &bootstrap_ctx);",
937+
1,
938+
"pass allocation bootstrap stack size",
939+
),
940+
(
941+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
942+
"\t\t\t\t\tbootstrap_ctx.allocation_size = allocation_size;\n"
943+
"\t\t\t\t\tbootstrap_ctx.page_size = Gum.query_page_size ();",
944+
"\t\t\t\t\tbootstrap_ctx.allocation_size = allocation_size;\n"
945+
"\t\t\t\t\tbootstrap_ctx.stack_size = stack_size;\n"
946+
"\t\t\t\t\tbootstrap_ctx.page_size = Gum.query_page_size ();",
947+
1,
948+
"pass runtime bootstrap stack size",
949+
),
950+
(
951+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
952+
"\t\tvoid * allocation_base;\n\t\tsize_t allocation_size;\n\n\t\tsize_t page_size;",
953+
"\t\tvoid * allocation_base;\n"
954+
"\t\tsize_t allocation_size;\n"
955+
"\t\tsize_t stack_size;\n\n"
956+
"\t\tsize_t page_size;",
957+
1,
958+
"bootstrap context records stack size",
959+
),
960+
(
961+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
962+
"\t\tvoid * mmap;\n\t\tvoid * munmap;",
963+
"\t\tvoid * mmap;\n\t\tvoid * mprotect;\n\t\tvoid * munmap;",
964+
1,
965+
"bootstrap resolves mprotect",
966+
),
967+
(
968+
Path("subprojects/frida-core/src/linux/frida-helper-backend.vala"),
969+
"\t\tpublic async void deallocate_memory (uint64 munmap_impl, uint64 address, "
970+
"size_t size, Cancellable? cancellable)",
971+
"\t\tpublic async void protect_memory (uint64 mprotect_impl, uint64 address, "
972+
"size_t size, int prot,\n"
973+
"\t\t\t\tCancellable? cancellable) throws Error, IOError {\n"
974+
"\t\t\tvar builder = new RemoteCallBuilder (mprotect_impl, saved_regs);\n"
975+
"\t\t\tbuilder\n"
976+
"\t\t\t\t.add_argument (address)\n"
977+
"\t\t\t\t.add_argument (size)\n"
978+
"\t\t\t\t.add_argument (prot);\n"
979+
"\t\t\tRemoteCallResult res = yield builder.build (this).execute (cancellable);\n"
980+
"\t\t\tif (res.status != COMPLETED)\n"
981+
'\t\t\t\tthrow new Error.NOT_SUPPORTED ("Unexpected crash while trying to '
982+
'protect memory");\n'
983+
"\t\t\tif (res.return_value != 0)\n"
984+
'\t\t\t\tthrow new Error.NOT_SUPPORTED ("Unexpected failure while trying to '
985+
'protect memory");\n'
986+
"\t\t}\n\n"
987+
"\t\tpublic async void deallocate_memory (uint64 munmap_impl, uint64 address, "
988+
"size_t size, Cancellable? cancellable)",
989+
1,
990+
"add remote mprotect call",
991+
),
992+
(
993+
Path("subprojects/frida-core/src/linux/helpers/bootstrapper.c"),
994+
"static int frida_socketpair (int domain, int type, int protocol, int sv[2]);\n"
995+
"static int frida_prctl (int option, unsigned long arg2, unsigned long arg3, "
996+
"unsigned long arg4, unsigned long arg5);",
997+
"static int frida_mprotect (void * address, size_t size, int prot);\n"
998+
"static int frida_socketpair (int domain, int type, int protocol, int sv[2]);\n"
999+
"static int frida_prctl (int option, unsigned long arg2, unsigned long arg3, "
1000+
"unsigned long arg4, unsigned long arg5);",
1001+
1,
1002+
"declare raw mprotect helper",
1003+
),
1004+
(
1005+
Path("subprojects/frida-core/src/linux/helpers/bootstrapper.c"),
1006+
" if (ctx->allocation_base == NULL)\n"
1007+
" {\n"
1008+
" ctx->allocation_base = mmap (NULL, ctx->allocation_size, "
1009+
"PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n"
1010+
" return (ctx->allocation_base == MAP_FAILED)\n"
1011+
" ? FRIDA_BOOTSTRAP_ALLOCATION_ERROR\n"
1012+
" : FRIDA_BOOTSTRAP_ALLOCATION_SUCCESS;\n"
1013+
" }",
1014+
" if (ctx->allocation_base == NULL)\n"
1015+
" {\n"
1016+
"#ifdef __ANDROID__\n"
1017+
" void * stack_base;\n\n"
1018+
" ctx->allocation_base = mmap (NULL, ctx->allocation_size, "
1019+
"PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n"
1020+
" if (ctx->allocation_base == MAP_FAILED)\n"
1021+
" return FRIDA_BOOTSTRAP_ALLOCATION_ERROR;\n\n"
1022+
" stack_base = (uint8_t *) ctx->allocation_base + ctx->allocation_size - "
1023+
"ctx->stack_size;\n"
1024+
" if (frida_mprotect (stack_base, ctx->stack_size, "
1025+
"PROT_READ | PROT_WRITE) != 0)\n"
1026+
" {\n"
1027+
" munmap (ctx->allocation_base, ctx->allocation_size);\n"
1028+
" ctx->allocation_base = MAP_FAILED;\n"
1029+
" return FRIDA_BOOTSTRAP_ALLOCATION_ERROR;\n"
1030+
" }\n\n"
1031+
" return FRIDA_BOOTSTRAP_ALLOCATION_SUCCESS;\n"
1032+
"#else\n"
1033+
" ctx->allocation_base = mmap (NULL, ctx->allocation_size, "
1034+
"PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n"
1035+
" return (ctx->allocation_base == MAP_FAILED)\n"
1036+
" ? FRIDA_BOOTSTRAP_ALLOCATION_ERROR\n"
1037+
" : FRIDA_BOOTSTRAP_ALLOCATION_SUCCESS;\n"
1038+
"#endif\n"
1039+
" }",
1040+
1,
1041+
"fallback bootstrap splits code and stack",
1042+
),
1043+
(
1044+
Path("subprojects/frida-core/src/linux/helpers/bootstrapper.c"),
1045+
" ctx.total_missing = 17;",
1046+
" ctx.total_missing = 18;",
1047+
1,
1048+
"require mprotect",
1049+
),
1050+
(
1051+
Path("subprojects/frida-core/src/linux/helpers/bootstrapper.c"),
1052+
" FRIDA_TRY_COLLECT (mmap)\n FRIDA_TRY_COLLECT (munmap)",
1053+
" FRIDA_TRY_COLLECT (mmap)\n"
1054+
" FRIDA_TRY_COLLECT (mprotect)\n"
1055+
" FRIDA_TRY_COLLECT (munmap)",
1056+
1,
1057+
"collect mprotect",
1058+
),
1059+
(
1060+
Path("subprojects/frida-core/src/linux/helpers/bootstrapper.c"),
1061+
"static int\nfrida_socketpair (int domain, int type, int protocol, int sv[2])",
1062+
"static int\n"
1063+
"frida_mprotect (void * address, size_t size, int prot)\n"
1064+
"{\n"
1065+
"#ifdef NOLIBC\n"
1066+
" return my_syscall3 (__NR_mprotect, address, size, prot);\n"
1067+
"#else\n"
1068+
" return mprotect (address, size, prot);\n"
1069+
"#endif\n"
1070+
"}\n\n"
1071+
"static int\n"
1072+
"frida_socketpair (int domain, int type, int protocol, int sv[2])",
1073+
1,
1074+
"implement raw mprotect helper",
1075+
),
1076+
(
1077+
Path("subprojects/frida-core/src/linux/helpers/inject-context.h"),
1078+
" void * allocation_base;\n size_t allocation_size;\n\n size_t page_size;",
1079+
" void * allocation_base;\n"
1080+
" size_t allocation_size;\n"
1081+
" size_t stack_size;\n\n"
1082+
" size_t page_size;",
1083+
1,
1084+
"align C bootstrap context",
1085+
),
1086+
(
1087+
Path("subprojects/frida-core/src/linux/helpers/inject-context.h"),
1088+
" void * (* mmap) (void * addr, size_t length, int prot, int flags, int fd, "
1089+
"off_t offset);\n"
1090+
" int (* munmap) (void * addr, size_t length);",
1091+
" void * (* mmap) (void * addr, size_t length, int prot, int flags, int fd, "
1092+
"off_t offset);\n"
1093+
" int (* mprotect) (void * addr, size_t length, int prot);\n"
1094+
" int (* munmap) (void * addr, size_t length);",
1095+
1,
1096+
"align C libc API",
1097+
),
1098+
(
1099+
Path("subprojects/frida-core/src/linux/proc-mem-injector.vala"),
1100+
'\t\t\tapi.table.mmap = resolve_one (remote_maps, "mmap");\n'
1101+
'\t\t\tapi.table.munmap = resolve_one (remote_maps, "munmap");',
1102+
'\t\t\tapi.table.mmap = resolve_one (remote_maps, "mmap");\n'
1103+
'\t\t\tapi.table.mprotect = resolve_one (remote_maps, "mprotect");\n'
1104+
'\t\t\tapi.table.munmap = resolve_one (remote_maps, "munmap");',
1105+
1,
1106+
"align proc-mem libc API",
1107+
),
8241108
)
8251109
for relative_path, old, new, expected_count, description in patches:
8261110
count = replace_in_file(frida_dir / relative_path, old, new)
@@ -1441,7 +1725,7 @@ def main():
14411725
parser.add_argument(
14421726
"--strict-wx",
14431727
action="store_true",
1444-
help="Disable persistent anonymous RWX code pools on Android",
1728+
help="Harden Frida-owned persistent anonymous RWX mappings on Android",
14451729
)
14461730
parser.add_argument(
14471731
"--work-dir", "-w", default=None, help="Working directory (default: ./build)"

tests/test_build_commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def test_cli_exposes_strict_wx_as_an_explicit_opt_in() -> None:
1717
)
1818

1919
assert "--strict-wx" in result.stdout
20-
assert "persistent anonymous RWX" in result.stdout
20+
assert "Frida-owned persistent anonymous RWX" in result.stdout
2121

2222

2323
def test_run_passes_argument_vector_without_shell(

0 commit comments

Comments
 (0)