Home Reverse Engineering macOS - How to patch app
Post
Cancel

Reverse Engineering macOS - How to patch app

Synopsis

This post is a basic introduction about how to patch a simple macOS app. It shows you how to disassemble a macOS app with Ghidra, identify the sweet spot and apply the patch manually and alternatively with the help of LIEF. Just follow the tutorial and implement the needed files yourself or download the final files to inspect them on your own.

Problem description

Some times you might run in a situation where you want to be able to amend a existing exectuable on macOS and extend or edit its behavior. Most of the times this amendment involves just a tiny little Spot or Part of the App and you really litteraly just need to flip one or two bytes. This is where patching an App comes into play. Here I gona show you some basic techniques how to do that.

Tutorial - How to patch a macOS App

Examining and disassembling the App

First Thing you want to Do when it comes to modifying an App is examining the original files. And there are quite a few free and preinstalled Tools in our toolbelt that will help US with this analysis. macOS, since Switching from ppc base to darwin has a lot of app we well know from the Linux World. As a first Thing to do I would install XCode and its command line Tools as well as homebrew because it will offer you a wide range of posibilities with it’s development environment and helper Tools. I also strongly advice you to Update python and get the latest Version via homebrew.

First inspection of the App

If the app you want to Analyse is an “*.app” bundle you will First have to show its content in finder and Goto the subfolder Content>MacOS here is the place where you will find the main executable that is launched when you open the app by double-clicking or such. This is also the executable you should disassemble First when You start your Reversing project.

Our target HELLO WORLD app

For simplicity we create a simple hello world app that asks the user for a secret and checks it with a hardcoded string. We’ll use this simple hello world app for our tutorials to find out how to reverse engineer macOS apps. So let’s take a look at our hello world c source code … (You also can downlaod all the files from this tutorial at the end of this page).

Create the hello world app by creating a file called hello_world.c with the following code in it:

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
// Standard include
#include <stdio.h>
// For scanf()
#include <curses.h> 
// For strcmp()
#include <string.h>

// This is the evil check function which decides if we get access or not
int checkInput(char input[256]) {

  // The secret to check against the user input
  char hardcoded_string[] = "S3CR3T";

  // Compare the value of the variable "input" with the variable "hardcoded_string"
  return strcmp(input, hardcoded_string);
}

// The main function / entry point of the executable
int main(int argc, char **argv) {

  // Variable to hold the user input
  char input[256];

  // This msg will prompt the user to enter his / her secret
  printf("Enter your secret:\n");

  // Will wait for user input and store the input in the variable "input"
  scanf("%s", input);

  // Compare the value of the variable "input" with the variable "hardcoded_string"
  int result = checkInput(input); // strcmp(input, hardcoded_string);

  // If the compare of the two values succeeds (or not) show a appropriate message
  if (result == 0) {
    printf("SUCCESS\n");
  } else {
    printf("ERROR\n");
  }
  return 0;
}

Compile the source code hello_world.c with:

1
clang -target x86_64-apple-macos -arch x86_64 -o hello_world hello_world.c

Make the file hello_world executable with:

1
chmod u+x hello_world 

Let’s see what our executable hello_world looks like when we run it. Run it with ./hello_world

1
2
3
4
5
6
7
8
9
dave@Aeon patching_macOS_app % ./hello_world       
Enter your secret:
12345
ERROR
dave@Aeon patching_macOS_app % ./hello_world
Enter your secret:
S3CR3T
SUCCESS
dave@Aeon patching_macOS_app % 

The pseudo code for our hello_world. If we compare it to our original c source code we’ll find, that the pseudo code is really close to the orignal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
undefined8 entry(void)

{
  int iVar1;
  undefined local_118 [264];
  long local_10;
  
  local_10 = *(long *)PTR____stack_chk_guard_100004008;
  _printf("Enter your secret:\n");
  _scanf("%s",local_118);
  iVar1 = _checkInput(local_118);
  if (iVar1 == 0) {
    _printf("SUCCESS\n");
  }
  else {
    _printf("ERROR\n");
  }
  if (*(long *)PTR____stack_chk_guard_100004008 == local_10) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  ___stack_chk_fail();
}

One thing we discover here is the assignment of PTR____stack_chk_guard_100004008 to var local_10. So what is this and why did the compiler insert it to the binary during compilation?

Stack_chk_guard is a security feature used in C and C++ programs to protect against stack-based buffer overflow attacks. A stack-based buffer overflow attack occurs when malicious code overflows a buffer on the stack, causing it to overwrite other memory locations on the stack and potentially taking control of the program execution.

Stack_chk_guard works by placing a random value, called a canary, on the stack before each function is called. When the function returns, the canary value is read and compared to the original value. If the canary value has changed, it indicates that the stack has been corrupted and the program is terminated.

This canary value is typically stored in a reserved symbol called __stack_chk_guard. When a program is compiled with stack_chk_guard enabled, the compiler will insert code to store and compare the canary value. This code is transparent to the programmer and does not require any changes to the program’s source code.

The assembler code for our hello_world main (entry) function

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
                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined entry()
             undefined         AL:1           <RETURN>
             undefined8        Stack[-0x10]:8 local_10                                XREF[2]:     100003eb5(W), 
                                                                                                   100003f3d(R)  
             undefined1        Stack[-0x118   local_118                               XREF[2]:     100003ede(*), 
                                                                                                   100003ef3(*)  
             undefined4        Stack[-0x11c   local_11c                               XREF[1]:     100003eb9(W)  
             undefined4        Stack[-0x120   local_120                               XREF[1]:     100003ec3(W)  
             undefined8        Stack[-0x128   local_128                               XREF[1]:     100003ec9(W)  
             undefined4        Stack[-0x12c   local_12c                               XREF[2]:     100003eff(W), 
                                                                                                   100003f05(R)  
                             _main                                           XREF[2]:     Entry Point(*), 1000080ea(*)  
                             entry
       100003ea0 55              PUSH       RBP
       100003ea1 48 89 e5        MOV        RBP,RSP
       100003ea4 48 81 ec        SUB        RSP,0x130
                 30 01 00 00
       100003eab 48 8b 05        MOV        RAX,qword ptr [->___stack_chk_guard]             = 100010008
                 56 01 00 00
       100003eb2 48 8b 00        MOV        RAX=>___stack_chk_guard,qword ptr [RAX]          = ??
       100003eb5 48 89 45 f8     MOV        qword ptr [RBP + local_10],RAX
       100003eb9 c7 85 ec        MOV        dword ptr [RBP + local_11c],0x0
                 fe ff ff 
                 00 00 00 00
       100003ec3 89 bd e8        MOV        dword ptr [RBP + local_120],EDI
                 fe ff ff
       100003ec9 48 89 b5        MOV        qword ptr [RBP + local_128],RSI
                 e0 fe ff ff
       100003ed0 48 8d 3d        LEA        RDI,[s_Enter_your_secret:_100003f7b]             = "Enter your secret:\n"
                 a4 00 00 00
       100003ed7 b0 00           MOV        AL,0x0
       100003ed9 e8 84 00        CALL       <EXTERNAL>::_printf                              int _printf(char * param_1, ...)
                 00 00
       100003ede 48 8d b5        LEA        RSI=>local_118,[RBP + -0x110]
                 f0 fe ff ff
       100003ee5 48 8d 3d        LEA        RDI,[s_%s_100003f8f]                             = "%s"
                 a3 00 00 00
       100003eec b0 00           MOV        AL,0x0
       100003eee e8 75 00        CALL       <EXTERNAL>::_scanf                               int _scanf(char * param_1, ...)
                 00 00
       100003ef3 48 8d bd        LEA        RDI=>local_118,[RBP + -0x110]
                 f0 fe ff ff
       100003efa e8 61 ff        CALL       _checkInput                                      undefined _checkInput()
                 ff ff
       100003eff 89 85 dc        MOV        dword ptr [RBP + local_12c],EAX
                 fe ff ff
       100003f05 83 bd dc        CMP        dword ptr [RBP + local_12c],0x0
                 fe ff ff 00
       100003f0c 0f 85 13        JNZ        LAB_100003f25
                 00 00 00
       100003f12 48 8d 3d        LEA        RDI,[s_SUCCESS_100003f92]                        = "SUCCESS\n"
                 79 00 00 00
       100003f19 b0 00           MOV        AL,0x0
       100003f1b e8 42 00        CALL       <EXTERNAL>::_printf                              int _printf(char * param_1, ...)
                 00 00
       100003f20 e9 0e 00        JMP        LAB_100003f33
                 00 00
                             LAB_100003f25                                   XREF[1]:     100003f0c(j)  
       100003f25 48 8d 3d        LEA        RDI,[s_ERROR_100003f9b]                          = "ERROR\n"
                 6f 00 00 00
       100003f2c b0 00           MOV        AL,0x0
       100003f2e e8 2f 00        CALL       <EXTERNAL>::_printf                              int _printf(char * param_1, ...)
                 00 00
                             LAB_100003f33                                   XREF[1]:     100003f20(j)  
       100003f33 48 8b 05        MOV        RAX,qword ptr [->___stack_chk_guard]             = 100010008
                 ce 00 00 00
       100003f3a 48 8b 00        MOV        RAX=>___stack_chk_guard,qword ptr [RAX]          = ??
       100003f3d 48 8b 4d f8     MOV        RCX,qword ptr [RBP + local_10]
       100003f41 48 39 c8        CMP        RAX,RCX
       100003f44 0f 85 0b        JNZ        LAB_100003f55
                 00 00 00
       100003f4a 31 c0           XOR        EAX,EAX
       100003f4c 48 81 c4        ADD        RSP,0x130
                 30 01 00 00
       100003f53 5d              POP        RBP
       100003f54 c3              RET
                             LAB_100003f55                                   XREF[1]:     100003f44(j)  
       100003f55 e8 02 00        CALL       <EXTERNAL>::___stack_chk_fail                    undefined ___stack_chk_fail()
                 00 00
                             -- Flow Override: CALL_RETURN (CALL_TERMINATOR)
       100003f5a 0f              ??         0Fh
       100003f5b 0b              ??         0Bh

The disassembled code for the checkInput() function which is called from the main function after user types his secret

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
                             //
                             // __text 
                             // __TEXT
                             // ram:100003e60-ram:100003f5b
                             //
                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined _checkInput()
             undefined         AL:1           <RETURN>
             undefined8        Stack[-0x10]:8 local_10                                XREF[2]:     100003e68(W), 
                                                                                                   100003e89(R)  
             undefined1        Stack[-0x11]:1 local_11                                XREF[1]:     100003e86(W)  
             undefined2        Stack[-0x13]:2 local_13                                XREF[1]:     100003e7c(W)  
             undefined4        Stack[-0x17]:4 local_17                                XREF[2]:     100003e72(W), 
                                                                                                   100003e8d(*)  
                             _checkInput                                     XREF[3]:     Entry Point(*), 
                                                                                          entry:100003efa(c), 1000080e8(*)  
       100003e60 55              PUSH       RBP
       100003e61 48 89 e5        MOV        RBP,RSP
       100003e64 48 83 ec 10     SUB        RSP,0x10
       100003e68 48 89 7d f8     MOV        qword ptr [RBP + local_10],RDI
       100003e6c 8b 05 02        MOV        EAX,dword ptr [s_S3CR3T_100003f74]               = "S3CR3T"
                 01 00 00
       100003e72 89 45 f1        MOV        dword ptr [RBP + local_17],EAX
       100003e75 66 8b 05        MOV        AX,word ptr [s_3T_100003f74+4]                   = "3T"
                 fc 00 00 00
       100003e7c 66 89 45 f5     MOV        word ptr [RBP + local_13],AX
       100003e80 8a 05 f4        MOV        AL,byte ptr [s__100003f74+6]                     = ""
                 00 00 00
       100003e86 88 45 f7        MOV        byte ptr [RBP + local_11],AL
       100003e89 48 8b 7d f8     MOV        RDI,qword ptr [RBP + local_10]
       100003e8d 48 8d 75 f1     LEA        RSI=>local_17,[RBP + -0xf]
       100003e91 e8 d8 00        CALL       <EXTERNAL>::_strcmp                              int _strcmp(char * param_1, char
                 00 00
       100003e96 48 83 c4 10     ADD        RSP,0x10
       100003e9a 5d              POP        RBP
       100003e9b c3              RET
       100003e9c 0f              ??         0Fh
       100003e9d 1f              ??         1Fh
       100003e9e 40              ??         40h    @
       100003e9f 00              ??         00h

In the disassembly we can see the call to compare with CMP at 0x 100003efb (after CALL checkInput at 0x100003ef0) and decicion with JNZ at 0x100003f02 which is the point where the app decides if we entered the correct secret or not. So the answere to this challenge is quite simple. If we want to go the easiest way to get to the success functionality, we just have to make the app always go to the success branch. We can do this simply by NOP-ing out the descision branch. This is not very fancy, but yet very effective.

Patch using Ghidra

What we wana do in this situation is to NOP the complete instruction “JNZ LAB_100003f1b”. So we can do this by overwritting the instruction with the hex values “90” which is the opcode for NOP. The decision will look as follows after editing:

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
       100003eee e8 75 00        CALL       <EXTERNAL>::_scanf                               int _scanf(char * param_1, ...)
                 00 00
       100003ef3 48 8d bd        LEA        RDI=>local_118,[RBP + -0x110]
                 f0 fe ff ff
       100003efa e8 61 ff        CALL       _checkInput                                      undefined _checkInput()
                 ff ff
       100003eff 89 85 dc        MOV        dword ptr [RBP + local_12c],EAX
                 fe ff ff
       100003f05 83 bd dc        CMP        dword ptr [RBP + local_12c],0x0
                 fe ff ff 00


       // START OF NOP MODIFICATION
       
       100003f0c 90              NOP
       100003f0d 90              NOP
       100003f0e 90              NOP
       100003f0f 90              NOP
       100003f10 90              NOP
       100003f11 90              NOP

       // END OF NOP MODIFICATION


       100003f12 48 8d 3d        LEA        RDI,[s_SUCCESS_100003f92]                        = "SUCCESS\n"
                 79 00 00 00
       100003f19 b0 00           MOV        AL,0x0
       100003f1b e8 42 00        CALL       <EXTERNAL>::_printf                              int _printf(char * param_1, ...)
                 00 00
       100003f20 e9 0e 00        JMP        LAB_100003f33
                 00 00

After you modified the assembly you have to write the code back to an executable. In Ghidra you can do this by clicking on Menu “File” > “Export Programm…”. On the following screen you can choose a export location, the new filename and most important, the file type. Choose “Original File” as Format and click “Ok”. When everything went fine, you will find a new executable at the choosen location. Before you can run it you will have to make it executable with:

1
dave@Aeon patching_macOS_app % chmod u+x hello_world_new

You also will have to code sign the app so it successfully runs. Do this with the following command:

1
dave@Aeon patching_macOS_app % codesign --verbose=4 --timestamp --strict --options runtime -s "Apple Development" hello_world_new --force

After that, your new executable is ready to run. Start the patched app - enter any secret you like and you will see something like the following:

1
2
3
4
5
6
dave@Aeon patching_macOS_app % ./hello_world_new
Enter your secret:
12345
SUCCESS
dave@Aeon patching_macOS_app % 

SUCCESS - yes, that’s what we wanted to see and it’s what we get. Due to the fact, that we NOPED the JNZ instruction to the error branch of the app away, the programm will always goto the success branch, no matter what we enter as secret. That’s it - really simple and very basic, but also very effectiv indeed.

You can go ahead an play around with the hello_world app on your own to i.e. store the return value of the strcmp() function in checkInput() into a variable and then return this variable as result of checkInput(). From this you can again load the compiled hello_world into Ghidra and i.e. modify the checkInput() function in a way it always returns 0x0 so the following check in the main() function always succeeds.

1
2
3
4
5
6
7
8
9
10
11
12
// This is the evil check function which decides if we get access or not
int checkInput(char input[256]) {

  // The secret to check against the user input
  char hardcoded_string[] = "S3CR3T";

  // Compare the value of the variable "input" with the variable "hardcoded_string"
  int retVal = strcmp(input, hardcoded_string);

  // Return the result of strcmp()
  return retVal; // Patch this in disassembly so it always returns 0x0;
}

Another thing you could do, is to modify the string “s_S3CR3T_100003f74” at 0x100003f74 to hold another secret you choose by modifying the string data in the __cstring section.

1
2
3
4
5
6
7
8
9
10
11
                             //
                             // __cstring 
                             // __TEXT
                             // ram:100003f74-ram:100003fa3
                             //
                             s_3T_100003f78                                  XREF[1,2]:   _checkInput:100003e6c(R), 
                             s__100003f7a                                                 _checkInput:100003e75(R), 
                             s_S3CR3T_100003f74                                           _checkInput:100003e80(R)  
       100003f74 53 33 43        ds         "S3CR3T"
                 52 33 54 00

It’s up to you to push the posibilities of patching this demo app further and get more experience.

Patch using LIEF

To patch the app you also can use LIEF. This is particullarly usefull if you want to automate certain tasks and / or have mutliple files with recurring tasks you want to execute. You can i.e. write a little python script for using LIEF to patch the app that looks someting like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
import lief

# Open the original executable as LIEF binary
app = lief.parse("./hello_world")

# Patch the location of the JNZ decision with NOPs
app.patch_address(0x100003f0c, [0x90, 0x90, 0x90, 0x90, 0x90, 0x90])

# Remove original code signature of the executable
app.remove_signature()

# Create a new executable and save it to the filesystem
app.write("./hello_world_lief_patched")

Of course again you will have to make the newly patched app executable again and recodesign it to get it running. But that’s basically it. Try it yourself and play around with the LIEF script to get a feeling how it works.

Download files

Here you can find all files we use during this tutorial. The executables are compiled on macOS 14.2 M1 (ARM64).

Credits

This post is licensed under CC BY 4.0 by the author.