Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Frida deny (basic)

An LLVM pass that inserts an inline asm instruction sequence into (mov x16, x16\nmov x17, x17) the prologue of either all functions (-passes="frida-deny") or only the specified ones (-passes="frida-deny<main>"). On AArch64, Frida uses x16 and x17 internally (for an example, see last_stack_push in the docs). For this reason, it checks if these registers are being used in the function prologue and fails to hook in this case.

Known limitations:

  • increased code size (negligible)
  • increased runtime penalty (negligible)
  • can be patched easily

The source code is available here.

Generate the IR for our main() test code:

Note: we optimize the generated IR before applying our obfuscation pass.

$ clang test.m -O3 -S -emit-llvm -o test.ll

Check the output:

$ cat test.ll
; ModuleID = 'test.m'
source_filename = "test.m"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32"
target triple = "arm64-apple-macosx15.0.0"

%struct.__NSConstantString_tag = type { ptr, i32, ptr, i64 }

@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [14 x i8] c"Hello, World!\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { ptr @__CFConstantStringClassReference, i32 1992, ptr @.str, i64 13 }, section "__DATA,__cfstring", align 8 #0

; Function Attrs: ssp uwtable(sync)
define noundef i32 @main(i32 noundef %0, ptr noundef readnone captures(none) %1) local_unnamed_addr #1 {
  %3 = tail call ptr @llvm.objc.autoreleasePoolPush() #2
  notail call void (ptr, ...) @NSLog(ptr noundef nonnull @_unnamed_cfstring_)
  tail call void @llvm.objc.autoreleasePoolPop(ptr %3)
  ret i32 0
}

; Function Attrs: nounwind
declare ptr @llvm.objc.autoreleasePoolPush() #2

declare void @NSLog(ptr noundef, ...) local_unnamed_addr #3

; Function Attrs: nounwind
declare void @llvm.objc.autoreleasePoolPop(ptr) #2

attributes #0 = { "objc_arc_inert" }
attributes #1 = { ssp uwtable(sync) "frame-pointer"="non-leaf" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="apple-m1" "target-features"="+aes,+altnzcv,+ccdp,+ccidx,+ccpp,+complxnum,+crc,+dit,+dotprod,+flagm,+fp-armv8,+fp16fml,+fptoint,+fullfp16,+jsconv,+lse,+neon,+pauth,+perfmon,+predres,+ras,+rcpc,+rdm,+sb,+sha2,+sha3,+specrestrict,+ssbs,+v8.1a,+v8.2a,+v8.3a,+v8.4a,+v8a" }
attributes #2 = { nounwind }
attributes #3 = { "frame-pointer"="non-leaf" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="apple-m1" "target-features"="+aes,+altnzcv,+ccdp,+ccidx,+ccpp,+complxnum,+crc,+dit,+dotprod,+flagm,+fp-armv8,+fp16fml,+fptoint,+fullfp16,+jsconv,+lse,+neon,+pauth,+perfmon,+predres,+ras,+rcpc,+rdm,+sb,+sha2,+sha3,+specrestrict,+ssbs,+v8.1a,+v8.2a,+v8.3a,+v8.4a,+v8a" }

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10}
!llvm.ident = !{!11}

!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 15, i32 5]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 1, !"Objective-C Garbage Collection", i8 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"Objective-C Enforce ClassRO Pointer Signing", i8 0}
!7 = !{i32 1, !"wchar_size", i32 4}
!8 = !{i32 8, !"PIC Level", i32 2}
!9 = !{i32 7, !"uwtable", i32 1}
!10 = !{i32 7, !"frame-pointer", i32 1}
!11 = !{!"Homebrew clang version 21.1.3"}

Build the pass plugin:

$ clang++ -std=c++17 -O3 -Wall -Wextra -Wpedantic -Werror -Wno-deprecated-declarations -isystem $(llvm-config --includedir) -shared -fPIC $(llvm-config --cxxflags) obf.cpp $(llvm-config --ldflags --libs core support passes analysis transformutils target bitwriter) -o obf.dylib

Run the pass:

$ opt -load-pass-plugin=./obf.dylib -passes="frida-deny" -S test.ll -o obf.ll
FridaDenyPass: Injected frida deny prologue into function 'main'

Check the output, note that the raw bytes have been added. In this case, it makes more sense to look at the generated asm:

$ clang -S obf.ll -o obf.s
$ cat obf.s
	.build_version macos, 15, 0	sdk_version 15, 5
	.section	__TEXT,__text,regular,pure_instructions
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
	.byte	240                             ; 0xf0
	.byte	3                               ; 0x3
	.byte	16                              ; 0x10
	.byte	170                             ; 0xaa
	.byte	241                             ; 0xf1
	.byte	3                               ; 0x3
	.byte	17                              ; 0x11
	.byte	170                             ; 0xaa
; %bb.0:
	sub	sp, sp, #32
	stp	x29, x30, [sp, #16]             ; 16-byte Folded Spill
	add	x29, sp, #16
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	bl	_objc_autoreleasePoolPush
	str	x0, [sp, #8]                    ; 8-byte Folded Spill
	adrp	x0, l__unnamed_cfstring_@PAGE
	add	x0, x0, l__unnamed_cfstring_@PAGEOFF
	bl	_NSLog
	ldr	x0, [sp, #8]                    ; 8-byte Folded Reload
	bl	_objc_autoreleasePoolPop
	mov	w0, #0                          ; =0x0
	ldp	x29, x30, [sp, #16]             ; 16-byte Folded Reload
	add	sp, sp, #32
	ret
	.cfi_endproc
                                        ; -- End function
	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"Hello, World!"

	.section	__DATA,__cfstring
	.p2align	3, 0x0                          ; @_unnamed_cfstring_
l__unnamed_cfstring_:
	.quad	___CFConstantStringClassReference
	.long	1992                            ; 0x7c8
	.space	4
	.quad	l_.str
	.quad	13                              ; 0xd

	.section	__DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
	.long	0
	.long	64

.subsections_via_symbols

Build the modified IR and run the executable:

Note: do not pass -O3 or other optimization-related options at this point as they might interfere with the applied obfuscation methods.

$ clang -framework Foundation obf.ll -o obf && ./obf
2025-10-16 12:51:05.959 obf[7264:276542] Hello, World!

Run the executables with Frida:

$ clang test.m -O3 -framework Foundation -o test
$ frida-trace -f test -i main
Instrumenting...                                                        
main: Loaded handler at "/Users/gemesa/git-repos/phantom-pass/src/10-frida-deny-basic/__handlers__/test/main.js"
Started tracing 1 function. Web UI available at http://localhost:50206/ 
2025-10-16 12:51:52.000 test[7280:277197] Hello, World!
           /* TID 0x103 */
    14 ms  main()
Process terminated
$ frida-trace -f obf -i main 
Instrumenting...                                                        
main: Loaded handler at "/Users/gemesa/git-repos/phantom-pass/src/10-frida-deny-basic/__handlers__/obf/main.js"
Warning: Skipping "main": unable to intercept function at 0x104a08610; please file a bug
Started tracing 1 function. Web UI available at http://localhost:50210/ 
2025-10-16 12:51:57.320 obf[7290:277334] Hello, World!
Process terminated

If we disassemble main, we can see the instruction opcodes and operands instead of the raw bytes:

$ llvm-objdump --disassemble-symbols=_main obf

obf:	file format mach-o arm64

Disassembly of section __TEXT,__text:

0000000100000610 <_main>:
100000610: aa1003f0    	mov	x16, x16
100000614: aa1103f1    	mov	x17, x17
100000618: d10083ff    	sub	sp, sp, #0x20
10000061c: a9017bfd    	stp	x29, x30, [sp, #0x10]
100000620: 910043fd    	add	x29, sp, #0x10
100000624: 9400000f    	bl	0x100000660 <_objc_autoreleasePoolPush+0x100000660>
100000628: f90007e0    	str	x0, [sp, #0x8]
10000062c: 90000020    	adrp	x0, 0x100004000 <_objc_autoreleasePoolPush+0x100004000>
100000630: 91006000    	add	x0, x0, #0x18
100000634: 94000005    	bl	0x100000648 <_objc_autoreleasePoolPush+0x100000648>
100000638: f94007e0    	ldr	x0, [sp, #0x8]
10000063c: 94000006    	bl	0x100000654 <_objc_autoreleasePoolPush+0x100000654>
100000640: 14000001    	b	0x100000644 <_main+0x34>
100000644: 14000000    	b	0x100000644 <_main+0x34>