Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

What is the principle of using Swift Hook's virtual function table?

2025-01-19 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article mainly introduces "what is the principle of the use of the virtual function table of Swift Hook". In daily operation, I believe that many people have doubts about the principle of the use of the virtual function table of Swift Hook. The editor consulted all kinds of data and sorted out simple and easy-to-use operation methods. I hope it will be helpful to answer the doubts of "what is the principle of the use of the virtual function table of Swift Hook?" Next, please follow the editor to study!

1. Preface

Because of the historical burden, the mainstream large-scale APP basically takes Objective-C as the main development language.

But keen students should be able to find that after the stability of Swift's ABI, various big companies began to increase their investment in Swift one after another.

Although Swift is difficult to replace Objective-C in the short term, the trend of keeping pace with Objective-C is becoming more and more obvious, which can be seen from the perspective of recruitment.

In the recruitment process over the past year, we have concluded that there are a considerable number of candidates who are only familiar with Swift development and are not familiar with Objective-C development, and most of these candidates are young.

In addition, take new frameworks such as RealityKit as an example, which only supports Swift but not Objective-C. All these phenomena mean that with the passage of time, if the project can not well support Swift development, then a series of problems such as recruitment costs and application innovation will be highlighted.

Therefore, 58.com launched an inter-departmental collaborative project within the group at Q4 in 2020 to create a mixed ecological environment of Objective-C and Swift from all levels-the project code "mixed sky".

Once the mixed ecological construction is perfect, then many problems will be easily solved.

two。 Brief introduction of principle

The technical scheme of this paper only aims at the Hook of the function called through the virtual function table, and does not involve the direct address call and objc_msgSend call.

It is also important to note that if Swift Compiler is set to Optimize for speed (the Release default), the function address of TypeContext's VTable will be cleared.

If set to Optimize for size, Swfit may be converted to a direct address call.

Both of the above configurations will cause the solution to fail. Therefore, this paper focuses on the technical details rather than the promotion of the scheme.

If Swift implements method calls by skipping the virtual function table, then the method substitution can be achieved by modifying the virtual function table. Change the function address of a specific virtual function table to the function address to be replaced. However, because the virtual function table does not contain the mapping of address and symbol, we can not get the corresponding function address according to the function name as Objective-C does, so the virtual function of Swift is modified by function index.

The simple understanding is to understand the virtual function table as an array. Suppose there is a FuncTable [], we can only modify the function address by index value, just like FuncTable [index] = replaceIMP. But this also involves a problem, we can not guarantee that the code is unchanged during the iteration of the version, so the index function of this version may be function A, and the index function of the next version may become function B. Obviously, this will have a significant impact on the replacement of functions.

To this end, we use the OverrideTable of Swift to solve the problem of index change. In Swift's OverrideTable, each node records which function of which class is overridden by the current function, as well as the function pointer to the rewritten function.

So as long as we can get the OverrideTable, it means that we can get the rewritten function pointer IMP0 and the rewritten function pointer IMP1. As long as you find IMP0 in FuncTable [] and replace it with IMP1, you can complete method substitution.

Next, we will go through the details of Swift's function calls, TypeContext, Metadata, VTable, OverrideTable, and how they relate to each other. In order to facilitate reading and understanding, all the code and running results of this article are based on the arm64 architecture

3. Function call of Swift

First of all, we need to understand how Swift's functions are called. Different from Objective-C, there are three ways of function call in Swift: message mechanism based on Objective-C, access based on virtual function table, and direct address call.

Message Mechanism of ▐ 3.1Objective-C

First of all, we need to understand when the function call of Swift is through the message mechanism of Objective-C. If the method is decorated with @ objc dynamic, the function will be called through objc_msgSend after compilation.

Suppose you have the following code

Class MyTestClass: NSObject {@ objc dynamic func helloWorld () {print ("call helloWorld () in MyTestClass")}} let myTest = MyTestClass.init () myTest.helloWorld ()

After compilation, the corresponding assembly is

0x1042b8824: bl 0x1042b9578; type metadata accessor for SwiftDemo.MyTestClass at 0x1042b8828: mov x20, x0 0x1042b882c: bl 0x1042b8998 SwiftDemo.MyTestClass.__allocating_init ()-> SwiftDemo.MyTestClass at ViewController.swift:22 0x1042b8830: stur x0, [x29, #-0x30] 0x1042b8834: adrp x8, 13 0x1042b8838: ldr x9, [x8, # 0x320] 0x1042b883c: stur x0, [x29, #-0x58] 0x1042b8840: mov x1, x9 0x1042b8844: str x8, [sp, # 0x60] 0x1042b8848: bl 0x1042bce88 Symbol stub for: objc_msgSend 0x1042b884c: mov W11, # 0x1 0x1042b8850: mov x0, x11 0x1042b8854: ldur x1, [x29, #-0x48] 0x1042b8858: bl 0x1042bcd5c; symbol stub for:

From the assembly code above, we can easily see that the objc_msgSend function with the address 0x1042bce88 is called.

Access to ▐ 3.2virtual function table

The access of virtual function table is also a form of dynamic invocation, but it is called by accessing virtual function table.

Assuming the same code as above, we remove @ objc dynamic and no longer inherit it from NSObject.

Class MyTestClass {func helloWorld () {print ("call helloWorld () in MyTestClass")}} let myTest = MyTestClass.init () myTest.helloWorld ()

The assembly code becomes like this?

0x1026207ec: bl 0x102621548; type metadata accessor for SwiftDemo.MyTestClass at 0x1026207f0: mov x20, x0 0x1026207f4: bl 0x102620984 SwiftDemo.MyTestClass.__allocating_init ()-> SwiftDemo.MyTestClass at ViewController.swift:22 0x1026207f8: stur x0, [x29, #-0x30] 0x1026207fc: ldr x8, [x0] 0x102620800: adrp x9, 8 0x102620804: ldr x9, [x9, # 0x40] 0x102620808: ldr x10, [x9] 0x10262080c: and x8, x8, x10 0x102620810: ldr x8, [x8, # 0x50] 0x102620814: mov x20, x0 0x102620818: stur x0, [x29, #-0x58] 0x10262081c: str x9 [sp, # 0x60] 0x102620820: blr x8 0x102620824: mov w11, # 0x1 0x102620828: mov x0, x11

As you can see from the assembly code above, the function stored in the x8 register is finally called through the blr instruction after compilation. As for where the data in the x8 register comes from, leave it to the later chapters.

▐ 3.3Direct address call

Assuming the same code as above, we change the Swift Compiler-Code Generaation-> Optimization Level in Build Setting to Optimize for Size [- Osize], and the assembly code looks like this?

0x1048c2114: bl 0x1048c24b8; type metadata accessor for SwiftDemo.MyTestClass at 0x1048c2118: add x1, sp, # 0x10; = 0x10 0x1048c211c: bl 0x1048c5174; symbol stub for: swift_initStackObject 0x1048c2120: bl 0x1048c2388; SwiftDemo.MyTestClass.helloWorld ()-> () at ViewController.swift:23 0x1048c2124: adr x0, # 0xc70c; demangling cache variable for type metadata for Swift._ContiguousArrayStorage

You will find that the bl instruction is followed by a constant address and the function address of SwiftDemo.MyTestClass.helloWorld ().

4. Thinking

Since the distribution form based on the virtual function table is also a kind of dynamic call, do you think that as long as we modify the function address in the virtual function table, the function replacement will be realized?

5. Method Exchange based on TypeContext

In the previous article "talking about the storage differences between Swift and OC from the perspective of Mach-O", we can learn that in the Mach-O file, you can find the ClassContextDescriptor of each Class through _ _ swift5_types, and you can find the virtual function table corresponding to the current class through ClassContextDescriptor, and dynamically call the functions in the table.

Note: (in Swift, Class/Struct/Enum is collectively referred to as Type, and for convenience, the TypeContext and ClassContextDescriptor we mentioned in this article refer to ClassContextDescriptor.)

First of all, let's review the structure description of the Swift class. The structure ClassContextDescriptor is the storage structure of the Swift class in Section64 (_ _ TEXT,__const).

Struct ClassContextDescriptor {uint32_t Flag; uint32_t Parent; int32_t Name; int32_t AccessFunction; int32_t FieldDescriptor; int32_t SuperclassType; uint32_t MetadataNegativeSizeInWords; uint32_t MetadataPositiveSizeInWords; uint32_t NumImmediateMembers; uint32_t NumFields; uint32_t FieldOffsetVectorOffset / / the number of bytes is related to the number of parameters and constraints of generics / / add 4 bytes / / add 4 bytes of VTableList [] / / use 4 bytes to store offset/pointerSize, then use 4 bytes to describe the number, and then N 4 bytes to describe the function type and function address. OverrideTableList [] / / first describes the quantity in 4 bytes, and then N 4'4 bytes describe the class that is currently being rewritten, the description of the function being rewritten, and the current address of the rewritten function. }

As can be seen from the above structure, the length of ClassContextDescriptor is not fixed, and the length of different classes of ClassContextDescriptor may be different. So how do we know if the current class is generic? And whether there are ResilientSuperclass, MetadataInitialization characteristics? In fact, it has been explained in the previous article "talking about the storage differences between Swift and OC from the perspective of Mach-O" that we can get the relevant information through the tag bits of Flag.

For example, if the generic tag bit of Flag is 1, it is generic.

| | TypeFlag (16bit) | version (8bit) | generic (1bit) | unique (1bit) | unknow (1bi) | Kind (5bit) | / / Flag & 0x80 = = 0x80 |

So how many bytes can a generic signature take? the storage of generics is explained in Swift's GenMeta.cpp file, which is summarized as follows:

Suppose there are generics with paramsCount parameters and requeireCount constraints / * * 16B = 4B + 4B + 2B + 2B + 2B + 2B addMetadataInstantiationCache-> 4B addMetadataInstantiationPattern-> 4B GenericParamCount-> 2B GenericRequirementCount-> 2B GenericKeyArgumentCount-> 2B GenericExtraArgumentCount-> 2B * / short pandding = (unsigned)-paramsCount & 3; generic signature bytes = (16 + paramsCount + pandding + 3 * 4 * (requeireCount) + 4)

So as long as we know the meaning of each mark bit of Flag and the storage length rule of generics, we can calculate the position of virtual function table VTable and the byte position of each function.

Does knowing the layout of generics and the location of VTable mean that you can modify function pointers? The answer is, of course, no, because VTable is stored in the _ _ TEXT segment, and _ _ TEXT is read-only, so we cannot modify it directly. However, in the end, we modified the code snippet through remap to change the address of the function in VTable, but found that the function was not replaced by our modified function at run time. Then what on earth is going on?

6. Method Exchange based on Metadata

The failure of the above experiment is of course caused by our lack of rigor. At the beginning of the project, we first studied the type storage description TypeContext, mainly the class storage description ClassContextDescriptor. After finding VTable, we take it for granted that the runtime Swift makes function calls by accessing VTable in ClassContextDescriptor. But this is not the case.

7. VTable function call

Next we will answer the question raised in the function invocation section of Swift, where the function address of the x8 register comes from. Again, Demo in the previous article, we break the point before calling the helloWorld () function.

Let myTest = MyTestClass.init ()-> myTest.helloWorld ()

The breakpoint stays at 0x100230ab0?

0x100230aac: stur x0, [x29, #-0x30] 0x100230ab0: ldr x8, [x0] 0x100230ab4: ldr x8, [x8, # 0x50] 0x100230ab8: mov x20, x0 0x100230abc: str x0, [sp, # 0x58] 0x100230ac0: blr x8

At this point, what is stored in the x0 register is the address of myTest x0 = 0x0000000280d08ef0jidr x8, and [x0] is to put the data stored at 0x280d08ef0 into x8 (note that only * myTest is stored in x8, not 0x280d08ef0 in x8). After stepping, you will find that x8 stores the address of type metadata, not the address of TypeContext, after looking at the data of each register through re read.

X0 = 0x0000000280d08ef0 x1 = 0x0000000280d00234 x2 = 0x0000000000000000 x3 = 0x00000000000008fd x4 = 0x0000000000000010 x5 = 0x000000016fbd188f x6 = 0x00000002801645d0 x7 = 0x0000000000000000 x8 = 0x000000010023e708 type metadata for SwiftDemo.MyTestClass x9 = 0x0000000000000003 x10 = 0x0000000280d08ef0 x11 = 0x0000000079c00000

After single-step execution in the previous step, what the current program needs to do is ldr x8, [x8, # 0x50], that is, the data at type metadata + 0x50 is stored in x8. This step is the jump table, which means that after this step, the address of helloWorld () is stored in the x8 register.

0x100230aac: stur x0, [x29, #-0x30] 0x100230ab0: ldr x8, [x0]-> 0x100230ab4: ldr x8, [x8, # 0x50] 0x100230ab8: mov x20, x0 0x100230abc: str x0, [sp, # 0x58] 0x100230ac0: blr x8

Is that really the case? after ldr x8, [x8, # 0x50] is executed, let's look at x8 again to see if there is a function address in the register?

X0 = 0x0000000280d08ef0 x1 = 0x0000000280d00234 x2 = 0x0000000000000000 x3 = 0x00000000000008fd x4 = 0x0000000000000010 x5 = 0x000000016fbd188f x6 = 0x00000002801645d0 x7 = 0x0000000000000000 x8 = 0x0000000100231090 SwiftDemo`SwiftDemo.MyTestClass.helloworld ()-> () at ViewController.swift:23 x9 = 0x0000000000000003

It turns out that x8 does store the function address of helloWorld (). The above experiments show that after jumping to the 0x50 position, the program finds the address of the helloWorld () function. The Metadata of the class is located in the _ _ DATA segment and is readable and writable. Its structure is as follows:

Struct SwiftClass {NSInteger kind; id superclass; NSInteger reserveword1; NSInteger reserveword2; NSUInteger rodataPointer; UInt32 classFlags; UInt32 instanceAddressPoint; UInt32 instanceSize; UInt16 instanceAlignmentMask; UInt16 runtimeReservedField; UInt32 classObjectSize; UInt32 classObjectAddressPoint; NSInteger nominalTypeDescriptor; NSInteger ivarDestroyer / / func [0] / / func [1] / / func [2] / / func [3] / / func [4] / / func [5] / / func [6].... }

The above code is exactly at the position of func [0] after the offset of 0x50 bytes. So you need to modify the data in Metadata if you want to modify the function dynamically.

After the experiment, it is found that the modified function has indeed changed after running. But this is not over, because the virtual function table is different from the message sending, there is no mapping between the function name and the function address in the virtual function table, so we can only modify the function address through offset.

For example, if I want to modify the first function, I want to find Meatadata and modify the 8-byte data at 0x50. By the same token, if I want to modify the second function, I want to modify the 8-byte data at 0x58. This leads to a problem that once the number or order of functions changes, the offset index needs to be corrected again.

For example, assume that the current 1.0 version of the code is

Class MyTestClass {func helloWorld () {print ("call helloWorld () in MyTestClass")}}

At this point, we modify the function pointer at 0x50. When version 2.0 is changed to the following code, our offset should be changed to 0x58, otherwise there will be an error in our function replacement.

Class MyTestClass {func sayhi () {print ("call sayhi () in MyTestClass")} func helloWorld () {print ("call helloWorld () in MyTestClass")}}

In order to solve the problem of virtual function changes, we need to understand the relationship between TypeContext and Metadata.

8. The relationship between TypeContext and Metadata

The nominalTypeDescriptor in the Metadata structure points to TypeContext, which means that when we get the Metadata address, we can offset the 0x40 bytes to get the corresponding TypeContext address of the current class. So how do you find Metadata through TypeContext?

Let's take a look at the Demo just now, when we type the breakpoint on the init () function, and we want to know where the Metadata of MyTestClass comes from.

-> let myTest = MyTestClass.init () myTest.helloWorld ()

At this point, when we expand to assemble, we will find that the program is ready to call a function.

-> 0x1040f0aa0: bl 0x1040f16a8; type metadata accessor for SwiftDemo.MyTestClass at 0x1040f0aa4: mov x20, x0 0x1040f0aa8: bl 0x1040f0c18; SwiftDemo.MyTestClass.__al

Before executing the bl 0x1040f16a8 instruction, the x0 register is 0.

X0 = 0x0000000000000000

At this point, through si step debugging, you will find that you have jumped to the function 0x1040f16a8 with fewer function instructions, as shown below.

SwiftDemo`type metadata accessor for MyTestClass:-> 0x1040f16a8: stp x29, x30, [sp, #-0x10]! 0x1040f16ac: adrp x8, 13 0x1040f16b0: add x8, x8, # 0x6f8; = 0x6f8 0x1040f16b4: add x8, x8, # 0x10; = 0x10 0x1040f16b8: mov x0, x8 0x1040f16bc: bl 0x1040f4e68 Symbol stub for: objc_opt_self 0x1040f16c0: mov x8, # 0x0 0x1040f16c4: mov x1, x8 0x1040f16c8: ldp x29, x30, [sp], # 0x10 0x1040f16cc: ret

After the 0x1040f16a8 function is executed, the x0 register stores the Metadata address of the MyTestClass.

X0 = 0x00000001047e6708 type metadata for SwiftDemo.MyTestClass

So what exactly is this function marked type metadata accessor for SwiftDemo.MyTestClass at?

It seems that one of the members of the struct ClassContextDescriptor introduced above is AccessFunction, so is the AccessFunction in this ClassContextDescriptor the access function of Metadata? This is actually easy to verify.

We run Demo again, where the metadata accessor is 0x1047d96a8, and the Metadata address is 0x1047e6708 after we continue.

X0 = 0x00000001047e6708 type metadata for SwiftDemo.MyTestClass

Look at the 0x1047e6708 and continue to offset the 0x40 bytes to get the nominalTypeDescriptor address 0x1047e6708 + 0x40 = 0x1047e6748 in the Metadata structure.

Check that the data stored by 0x1047e6748 is 0x1047df4a0.

(lldb) x 0x1047e6748 0x1047e6748: a0f4 7d 04 01 00 00 00..}. 0x1047e6758: 90 90 7d 04 01 00 00 00 18 8c 7d 04 01 00 00 00..}.}.

The AccessFunction in ClassContextDescriptor is at byte 12, so the position of AccessFunction is 0x1047df4ac for 0x1047df4a0 + 12. Continue to view the data stored in 0x1047df4ac as

(lldb) x 0x1047df4ac 0x1047df4ac: fc A1 ff ff 70 04 00 00 00 02 00 00.... p. 0x1047df4bc: 0c 00 00 00 02 00 00 00 0a 00 00 00.

Because in ClassContextDescriptor, AccessFunction is the relative address, so we do an address calculation 0x1047df4ac + 0xffffa1fc-0x10000000 = 0x1047d96a8, which is the same as metadata accessor 0x1047d96a8, which means that TypeContext obtains the address of the corresponding Metadata through AccessFunction.

Of course, there are exceptions, and sometimes the compiler will directly use the address of the cached cache Metadata instead of getting the Metadata of the class through AccessFunction.

9. Method Exchange based on TypeContext and Metadata

After understanding the relationship between TypeContext and Metadata, we can make some assumptions. Although the address of the function is stored in Metadata, we don't know the type of the function. The function type here refers to the ordinary function, initialization function, getter, setter, and so on.

In TypeContext's VTable, method stores a total of 8 bytes, the Flag of the first 4-byte stored function, and the relative address of the second 4-byte stored function.

Struct SwiftMethod {uint32_t Flag; uint32_t Offset;}

Through Flag, we can easily know whether it is dynamic, whether it is an instance method, and the function type Kind.

| | ExtraDiscriminator (16bit) |. | Dynamic (1bit) | instanceMethod (1bit) | Kind (4bit) |

The Kind enumeration is as follows?

Typedef NS_ENUM (NSInteger, SwiftMethodKind) {SwiftMethodKindMethod = 0, / method SwiftMethodKindInit = 1, / / init SwiftMethodKindGetter = 2, / / get SwiftMethodKindSetter = 3, / / set SwiftMethodKindModify = 4, / / modify SwiftMethodKindRead = 5, / / read}

It is clear from the source code of Swift that the functions overridden by the class are stored separately, that is, there is a separate OverrideTable.

And OverrideTable is stored after VTable. Unlike the method structure in VTable, functions in OverrideTable require three 4-byte descriptions:

Struct SwiftOverrideMethod {uint32_t OverrideClass;// record is the function of which class to rewrite, point to which function the TypeContext uint32_t OverrideMethod;// record rewrites, point to the relative address of the SwiftMethod uint32_t Method;// function}

In other words, SwiftOverrideMethod can contain a binding relationship between two functions, regardless of the compilation order and number of functions.

If Method records the address of the function used for Hook, and OverrideMethod is used as the function of Hook, does that mean that no matter how to change the order and number of virtual function tables, as long as Swift still makes function calls by skipping tables, then we don't have to pay attention to function changes.

To verify the feasibility, let's write a Demo test:

Class MyTestClass {func helloWorld () {print ("call helloWorld () in MyTestClass")}} / / as Hook class and function class HookTestClass: MyTestClass {override func helloWorld () {print ("\ n* call helloWorld () in HookTestClass *") super.helloWorld () print ("* * * call helloWorld () in HookTestClass end *\ n ")} / / Hook let myTest = MyTestClass.init () myTest.helloWorld () / / do hook print ("\ n-replace MyTestClass.helloWorld () with HookTestClass.helloWorld () -\ n ") WBOCTest.replace (HookTestClass.self) by inheritance and rewriting / / hook takes effect myTest.helloWorld ()

After running, you can see that helloWorld () has been replaced successfully?

2021-03-09 17 call helloWorld 2515 SwiftDemo 36.321318 0800 SwiftDemo [59714 call helloWorld 5168073] _ mh_execute_header = 4368482304 call helloWorld () in MyTestClass-replace MyTestClass.helloWorld () with HookTestClass.helloWorld ()-* call helloWorld () in HookTestClass * call helloWorld () in MyTestClass * call helloWorld () in HookTestClass end * so far The study on "what is the principle of the use of Swift Hook's virtual function table" is over. I hope to be able to solve your doubts. The collocation of theory and practice can better help you learn, go and try it! If you want to continue to learn more related knowledge, please continue to follow the website, the editor will continue to work hard to bring you more practical articles!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report