• Tidak ada hasil yang ditemukan

Instructions

Dalam dokumen Practical Reverse Engineering (Halaman 98-105)

Every instruction in ARM state encodes an arithmetic condition to support conditional execution. By default, the condition is AL (always execute). This

condition is encoded in the four most signifi cant bits in the opcode (bits 28–31);

AL is defi ned as 0b1110, which is 0xE. If you pay close attention to the assembly snippets (in ARM state), you will notice that the byte code usually has an 0xE*

pattern at the end. In fact, if you look at the instructions in a hex editor, you will notice that 0xE* commonly occurs every four bytes. For example:

FE FF FF EA FE FF FF EA FE FF FF EA FE FF FF EA FE FF FF EA 1C F1 9F E5 00 00 A0 E1 18 01 9F E5 11 0F 0F EE 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1 78 00 A0 E3 10 0F 01 EE 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1 00 00 A0 E3 17 0F 08 EE 17 0F 07 EE

Why is it important to know this pattern? Because ARM code is sometimes embedded in ROM or fl ash memory and may not follow a specifi c fi le for- mat. In your reverse engineering journey, sometimes you will just be given a raw memory dump without much context, so it can be useful to guess the architecture by looking at the opcodes. The other reason is related to exploits.

Shellcode can be embedded inside an exploit delivered over the network or in a document; to analyze it, you must extract the shellcode from the rest of the network traffi c. Sometimes it is straightforward and the shellcode boundary is obvious, other times it is not. However, if you can recognize the pattern, you can quickly guess the start/end of code. The ability to recognize instruction boundaries in a seemingly random blob of data is important. Maybe you will appreciate it later.

Walk-Through

Having learned all the fundamentals, you can apply them in this section by fully decompiling an unknown function. This function encompasses many concepts and techniques covered in this chapter, so it is an excellent way to put your knowledge to the test. Along the way, you will also learn new skills that were only hinted at in the early sections. Because the function is somewhat long, we put it in graph form to save space and improve readability. The function body is shown in Figure 2-6, and all the code line numbers discussed in this section refer to this fi gure.

Following is the context in which it is called:

01: 17 9B LDR R3, [SP,#0x5c]

02: 16 9A LDR R2, [SP,#0x58]

03: 51 46 MOV R1, R10 04: 20 46 MOV R0, R4 05: FF F7 98 FF BL unk_function

07: unk_function

08: 2D E9 78 48 PUSH.W {R3-R6,R11,LR}

09: 0D F2 10 0B ADDW R11, SP, #0x10 10: 85 68 LDR R5, [R0,#8]

11: 8C 69 LDR R4, [R1,#0x18]

12: 1E 46 MOV R6, R3 13: A5 42 CMP R5, R4 14: 01 D0 BEQ loc_103C4BE

18: loc_103C4BE

19: 03 8A LDRH R3, [R0,#0x10]

20: 02 2B CMP R3, #2 21: FA D1 BNE loc_103C4BA

22: 83 69 LDR R3, [R0,#0x18]

23: 1A 40 ANDS R2, R3 24: C3 69 LDR R3, [R0,#0x1C]

25: 33 40 ANDS R3, R6 26: 13 43 ORRS R3, R2 27: F4 D1 BNE loc_103C4BA

28: C3 68 LDR R3, [R0,#0xC]

29: 00 68 LDR R0, [R0]

30: 03 EB 43 02 ADD.W R2, R3, R3,LSL#1 31: CB 68 LDR R3, [R1,#0xC]

32: DB 68 LDR R3, [R3,#0xC]

33: 03 EB C2 03 ADD.W R3, R3, R2,LSL#3 34: 93 F9 16 40 LDRSB.W R4, [R3,#0x16]

35: E9 F7 E6 F9 BL foo ; assume this takes l arg 36: 61 28 CMP R0, #0x61

37: 04 D0 BEQ loc_103C4F6

38: 62 28 CMP R0, #0x62 39: 04 D0 BEQ loc_103C4FA

43: loc_103C4F6

44: 61 2C CMP R4, #0x61 45: DF D1 BNE loc_103C4BA

40: 63 2C CMP R4, #0x63 41: 02 DA BGE loc_103C4FA

46: loc_103C4FA

47: 01 20 MOVS R0, #1 42: E1 E7 B loc_103C4BA

15: loc_103C4BA

16: 00 20 MOVS R0, #0 17: 1E E0 B locret_103C4FC

48: locret_103C4FC

49: BD E8 78 88 POP.W {R3-R6,R11,PC}

50: ; End of function unk_function

Figure 2-6

When approaching an unknown function (or any block of code), the fi rst step is to determine what you know for certain about it. The following list enumer- ates these facts and how you know them:

The code is Thumb state and the instruction set is Thumb-2. You know this because: 1) prologue and epilogue (lines 1 and 49) use the PUSH/POP pattern; 2) instruction size is either 16 or 32 bits in width; 3) the disas- sembler shows the .W prefi x for some instructions, indicating that they are

The function preserves R3–R6 and R11. You know this because they are saved and restored in the prologue (line 1) and epilogue (line 49), respectively.

The function takes at most four arguments (R0–R3) and returns a Boolean (R0). You know this because according to the ARM ABI (Application Binary Interface), the fi rst four parameters are passed in R0–R3 (the rest are pushed on the stack) and the return value is in R0. It is “at most four” in this case because you saw that before calling the function in line 5, R0–R3 are initialized with some values and you do not see any other instructions writing to the stack (for additional arguments). At this point, the function prototype is as follows:

BOOL unk_function(int, int, int, int)

The fi rst two arguments’ type is “pointer to an object.” You know this because R0 and R1 are the base address in a load instruction (lines 10–11).

The types are most likely structures because there is access to offset 0x10, 0x18, 0x1c, and so on (line 10, 11, 19, 22, 24, 28, etc.). You can be nearly certain that they are not arrays because the access/load pattern is not sequential. It is uncertain whether R0 and R1 are pointers to one or two different structure types without further context. For now, you can assume that they are two different types. You update the prototype as follows:

BOOL unk_function(struct1 *, struct2 *, int, int)

loc_103C4BA is the exit path to return 0; loc_103C4FA is the exit path to return 1; and locret_103C4FC returns from the function. Hence, branches to these locations indicate that you are done with the function.

The third and fourth arguments are of type integer. You know this because R2 and R3 are being used in AND/ORR operations (lines 23, 25, and 26). While there is indeed a possibility that they can be pointers, it is unlikely to be the case unless they were encoding/decoding pointers; and even if they were pointers, you should see them being used in load/store operations but you don’t.

Even though R11 is adjusted to be 0x10 bytes above the stack pointer, it is never used after that instruction. Hence, it can be ignored.

The function foo (line 35) takes one argument. Its entire body is not included here due to space constraints. Just assume this is a given for the sake of simplicity.

Having enumerated known facts, you now need to use them to logically derive other useful facts. The next important task is to delve into the two unknown structures identifi ed. Obviously you cannot recover its entire layout because only some of its elements are referenced in the function; however, you can still infer the fi eld type information.

R0 is of type struct1 *. In line 10, it loads a fi eld member at offset 0x8 and then compares it with R4 (line 13). R4 is a fi eld member at offset 0x18 in the structure

are of the same type. Line 13 compares these two fi elds. If they are equal, then execution proceeds to loc_103C4BE; otherwise, 0 is returned (line 15). Because of the equality compare, you can infer that these two fi elds are integers.

Line 19 loads another fi eld member from struct1 and compares it against 2; if it is not equal, then 0 is returned (line 21). You can infer that the fi eld type is a short because of the LDRH instruction (loads a half-word).

Lines 22–23 load another fi eld member from struct1 and ANDs it against the third argument (which is assumed to be an integer). Lines 25–27 do something similar with the fourth argument. Because of these operations, you can infer that fi eld members at offset 0x18 and 0x1c are integers.

The structure defi nitions so far are as follows:

struct1 ...

+0x008 field08_i ; same type as struct2.field18_i ...

+0x010 field10_s ; short ...

+0x018 field18_i ; int +0x01c field1c_i ; int struct2

...

+0x018 field18_i ; same type as struct1.field08_i

N O T E For struct fi eld names, you might follow the habit of indicating the off set and the “type.” For example, an “I” suffi x means integer (or some generic 32-bit type), “s”

means short (16-bit), “c” means char (1 byte), and “p” means pointer of some type. This enables you to quickly remember what their types are. When you determine their true purpose, you can then rename them to something more meaningful.

Given these types, you can already recover the pseudo-code of everything from line 1 to 27. It is as follows:

struct1 *arg1 = ...;

struct1 *arg2 = ...;

int arg3 = ...;

int arg4 = ...;

BOOL result = unk_function(arg1, arg2, arg3, arg4);

if (arg1->field08_i == arg2->field18_i) { if (arg1->field10_s != 2) return 0;

if ( ((arg1->field18_i & arg3) | (arg1->field1c_i & arg4) ) != 0

) return 0;

...

return 0;

}

N O T E It is a bit suspicious that the AND operation is being used on two adjacent integer fi elds. This usually means that they are actually 64-bit integers split into two registers/memory locations. This is a common pattern used to access 64-bit constants on 32-bit architectures.

Astute readers will notice that lines 25–27 may seem a bit redundant. ANDS sets the condition fl ags, ORRS immediately overwrites it, and BNE takes the fl ag from ORRS; hence, the conditions set by ANDS are really not necessary. The compiler generates this redundancy because it is optimizing for code density: AND will be 4 bytes long, but ANDS is only 2 bytes. MOV and MOVS are also subjected to the same optimization. You will often see this pattern in code optimized for Thumb.

Line 28 loads another fi eld from struct1 into R3; line 29 loads from offset zero of the same structure into R0; and line 30 sets R2 to R3*3 (=R3+(R3<<1)). Line 31 loads a fi eld from struct2 into R3 and then accesses another fi eld using that as a base pointer. This implies that you have a pointer to another structure inside struct2 at offset 0xC. Line 32 loads a fi eld from that new structure into R3; line 33 updates it to be R3+R2*8; and line 34 uses that as a base address and loads a signed short value at offset 0x16 of another structure into R4.

Let’s update the structure defi nition before continuing:

struct1

+0x000 field00_i ; int ...

+0x008 field08_i ; same type as struct2.field18_i +0x00c field0c_i ; integer

...

+0x010 field10_s ; short ...

+0x018 field18_i ; int +0x01c field1c_i ; int ...

struct2 ...

+0x00c field0c_p ; struct3 * ...

+0x018 field18_i ; same type as struct1.field08_i ...

struct3 ...

+0x00c field0c_p ; struct4 * ...

struct4 (size=0x18=24) // why?

...

+0x016 field16_c; char +0x017 end

You could deduce that there was an array involved because of the multiplica- tion/scaling factor (lines 30 and 33); there were not two arrays because R2R3 in line 30 is not a base address but an index. Also, it does not make sense for a base address to be multiplied by 3. The base address of the array is R3 in line 33 because it is being indexed with R2. You inferred that each array element must be 0x18 (24) because after simplifi cation, it was R2*3*8, where R2 is the index and 24 is the scale.

Figure 2-7 illustrates the relationships between the four structures.

struct1 +00 field00_i

+08 field08_i +0c field0c_i +10 field10_a

+18 field18_i +1c field1c_a

struct2 struct3

[0]

[1]

= i [2]

[3]

[i−1]

struct4

struct4

struct4

+18 field18_i +10 field10_a +0c field0c_p

+0c field0c_p

+17 field17_c +16 field16_c

0×18 bytes

Figure 2-7

Here is the pseudo-code for lines 28–35:

r3 = arg1->field0c_i;

r2 = r3 + r3<<1

= arg1->field0c_i*3;

r3 = arg2->field0c_p;

r3 = arg2->field0c_p->field0c_p;

r3 = arg2->field0c_p->field0c_p + r2*8

= arg2->field0c_p->field0c_p + arg1->field0c_i*24;

= arg2->field0c_p->field0c_p[arg1->field0c_i];

r4 = arg2->field0c_p->field0c_p[arg1->field0c_i].field16_c;

r0 = foo(arg1->field00_i);

The rest of the function is simply comparing the return value from foo and r4. The full pseudo-code now looks like this:

struct1 *arg1 = ...;

int arg3 = ...;

int arg4 = ...;

BOOL result = unk_function(arg1, arg2, arg3, arg4);

BOOL unk_function(struct1 *arg1, struct2 *arg2, int arg3, int arg4) {

char a;

int b;

if (arg1->field08_i == arg2->field18_i) { if (arg1->field10_s != 2) return 0;

if ( ((arg1->field18_i & arg3) | (arg1->field1c_i & arg4) ) != 0

) return 0;

b = foo(arg1->field00_i);

a = arg2->field0c_p->field0c_p[arg1->field0c_i].field16_c;

if (b == 0x61 && a != 0x61) { return 0;

} else { return 1;}

if (b == 0x62 && a >= 0x63) { return 1;

} else { return 0;}

} else { return 0;

} }

While this function used multiple, interconnected data structures whose full layout is unclear, you can see how you were still able to recover some of the fi eld types and their relationship with others. You also learned how to recognize a type’s width and signedness by considering the instruction and conditional code associated with them.

Dalam dokumen Practical Reverse Engineering (Halaman 98-105)