Unicorn Engine tutorial
In this tutorial you will learn how to use Unicorn Engine by solving practical exercises.
There are 4
exercises and I will solve the first exercise for you.
For the others I am providing hints and solutions, which you can obtain by clicking the spoiler buttons.
Fast FAQ:
- What is Unicorn Engine?
It is an emulator. Not usual though. You don’t emulate whole program or system. Also, it doesn’t support syscalls. You have to map memory and write data into it manually, then you can start the emulation from a chosen address.
- When is it useful?
- You can call an interesting function from the malware, without creating a harmfull process
- CTF’s
- Fuzzing
- Plugin for gdb that predicts the future, for example further jumps.
- Emulating obfuscated code.
- What do I need to have installed for this tutorial?
- Unicorn Engine with python binding
- A disassembler
Table of Contents
Task 1
This is a task from hxp CTF 2017 called Fibonacci
.
The binary can be downloaded here.
When we run this program, we can notice that it computes and prints our flag but very slowly. Every next byte of the flag is computed slower and slower.
The flag is: hxp{F
It means it is necessary to optimize the program to get the flag (in a reasonable amount of time).
We’ve decompiled the code to C-like pseudocode, with help of IDA Pro. Although the code is not necessarily decompiled properly, we can get some idea of what is happening.
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
void *v3; // rbp@1
int v4; // ebx@1
signed __int64 v5; // r8@2
char v6; // r9@3
__int64 v7; // r8@3
char v8; // cl@3
__int64 v9; // r9@5
int a2a; // [sp+Ch] [bp-1Ch]@3
v3 = &encrypted_flag;
v4 = 0;
setbuf(stdout, 0LL);
printf("The flag is: ", 0LL);
while ( 1 )
{
LODWORD(v5) = 0;
do
{
a2a = 0;
fibonacci(v4 + v5, &a2a);
v8 = v7;
v5 = v7 + 1;
}
while ( v5 != 8 );
v4 += 8;
if ( (unsigned __int8)(a2a << v8) == v6 )
break;
v3 = (char *)v3 + 1;
_IO_putc((char)(v6 ^ ((_BYTE)a2a << v8)), stdout);
v9 = *((char *)v3 - 1);
}
_IO_putc(10, stdout);
return 0LL;
}
unsigned int __fastcall fibonacci(int i, _DWORD *a2)
{
_DWORD *v2; // rbp@1
unsigned int v3; // er12@3
unsigned int result; // eax@3
unsigned int v5; // edx@3
unsigned int v6; // esi@3
unsigned int v7; // edx@4
v2 = a2;
if ( i )
{
if ( i == 1 )
{
result = fibonacci(0, a2);
v5 = result - ((result >> 1) & 0x55555555);
v6 = ((result - ((result >> 1) & 0x55555555)) >> 2) & 0x33333333;
}
else
{
v3 = fibonacci(i - 2, a2);
result = v3 + fibonacci(i - 1, a2);
v5 = result - ((result >> 1) & 0x55555555);
v6 = ((result - ((result >> 1) & 0x55555555)) >> 2) & 0x33333333;
}
v7 = v6 + (v5 & 0x33333333) + ((v6 + (v5 & 0x33333333)) >> 4);
*v2 ^= ((BYTE1(v7) & 0xF) + (v7 & 0xF) + (unsigned __int8)((((v7 >> 8) & 0xF0F0F) + (v7 & 0xF0F0F0F)) >> 16)) & 1;
}
else
{
*a2 ^= 1u;
result = 1;
}
return result;
}
Here is assembly code of main
function:
.text:0x4004E0 main proc near ; DATA XREF: start+1Do
.text:0x4004E0
.text:0x4004E0 var_1C = dword ptr -1Ch
.text:0x4004E0
.text:0x4004E0 push rbp
.text:0x4004E1 push rbx
.text:0x4004E2 xor esi, esi ; buf
.text:0x4004E4 mov ebp, offset unk_4007E1
.text:0x4004E9 xor ebx, ebx
.text:0x4004EB sub rsp, 18h
.text:0x4004EF mov rdi, cs:stdout ; stream
.text:0x4004F6 call _setbuf
.text:0x4004FB mov edi, offset format ; "The flag is: "
.text:0x400500 xor eax, eax
.text:0x400502 call _printf
.text:0x400507 mov r9d, 49h
.text:0x40050D nop dword ptr [rax]
.text:0x400510
.text:0x400510 loc_400510: ; CODE XREF: main+8Aj
.text:0x400510 xor r8d, r8d
.text:0x400513 jmp short loc_40051B
.text:0x400513 ; ---------------------------------------------------------------------------
.text:0x400515 align 8
.text:0x400518
.text:0x400518 loc_400518: ; CODE XREF: main+67j
.text:0x400518 mov r9d, edi
.text:0x40051B
.text:0x40051B loc_40051B: ; CODE XREF: main+33j
.text:0x40051B lea edi, [rbx+r8]
.text:0x40051F lea rsi, [rsp+28h+var_1C]
.text:0x400524 mov [rsp+28h+var_1C], 0
.text:0x40052C call fibonacci
.text:0x400531 mov edi, [rsp+28h+var_1C]
.text:0x400535 mov ecx, r8d
.text:0x400538 add r8, 1
.text:0x40053C shl edi, cl
.text:0x40053E mov eax, edi
.text:0x400540 xor edi, r9d
.text:0x400543 cmp r8, 8
.text:0x400547 jnz short loc_400518
.text:0x400549 add ebx, 8
.text:0x40054C cmp al, r9b
.text:0x40054F mov rsi, cs:stdout ; fp
.text:0x400556 jz short loc_400570
.text:0x400558 movsx edi, dil ; c
.text:0x40055C add rbp, 1
.text:0x400560 call __IO_putc
.text:0x400565 movzx r9d, byte ptr [rbp-1]
.text:0x40056A jmp short loc_400510
.text:0x40056A ; ---------------------------------------------------------------------------
.text:0x40056C align 10h
.text:0x400570
.text:0x400570 loc_400570: ; CODE XREF: main+76j
.text:0x400570 mov edi, 0Ah ; c
.text:0x400575 call __IO_putc
.text:0x40057A add rsp, 18h
.text:0x40057E xor eax, eax
.text:0x400580 pop rbx
.text:0x400581 pop rbp
.text:0x400582 retn
.text:0x400582 main endp
Assembly code of fibonacci
function looks as follows:
.text:0x400670 fibonacci proc near ; CODE XREF: main+4Cp
.text:0x400670 ; fibonacci+19p ...
.text:0x400670 test edi, edi
.text:0x400672 push r12
.text:0x400674 push rbp
.text:0x400675 mov rbp, rsi
.text:0x400678 push rbx
.text:0x400679 jz short loc_4006F8
.text:0x40067B cmp edi, 1
.text:0x40067E mov ebx, edi
.text:0x400680 jz loc_400710
.text:0x400686 lea edi, [rdi-2]
.text:0x400689 call fibonacci
.text:0x40068E lea edi, [rbx-1]
.text:0x400691 mov r12d, eax
.text:0x400694 mov rsi, rbp
.text:0x400697 call fibonacci
.text:0x40069C add eax, r12d
.text:0x40069F mov edx, eax
.text:0x4006A1 mov ebx, eax
.text:0x4006A3 shr edx, 1
.text:0x4006A5 and edx, 55555555h
.text:0x4006AB sub ebx, edx
.text:0x4006AD mov ecx, ebx
.text:0x4006AF mov edx, ebx
.text:0x4006B1 shr ecx, 2
.text:0x4006B4 and ecx, 33333333h
.text:0x4006BA mov esi, ecx
.text:0x4006BC
.text:0x4006BC loc_4006BC: ; CODE XREF: fibonacci+C2j
.text:0x4006BC and edx, 33333333h
.text:0x4006C2 lea ecx, [rsi+rdx]
.text:0x4006C5 mov edx, ecx
.text:0x4006C7 shr edx, 4
.text:0x4006CA add edx, ecx
.text:0x4006CC mov esi, edx
.text:0x4006CE and edx, 0F0F0F0Fh
.text:0x4006D4 shr esi, 8
.text:0x4006D7 and esi, 0F0F0Fh
.text:0x4006DD lea ecx, [rsi+rdx]
.text:0x4006E0 mov edx, ecx
.text:0x4006E2 shr edx, 10h
.text:0x4006E5 add edx, ecx
.text:0x4006E7 and edx, 1
.text:0x4006EA xor [rbp+0], edx
.text:0x4006ED pop rbx
.text:0x4006EE pop rbp
.text:0x4006EF pop r12
.text:0x4006F1 retn
.text:0x4006F1 ; ---------------------------------------------------------------------------
.text:0x4006F2 align 8
.text:0x4006F8
.text:0x4006F8 loc_4006F8: ; CODE XREF: fibonacci+9j
.text:0x4006F8 mov edx, 1
.text:0x4006FD xor [rbp+0], edx
.text:0x400700 mov eax, 1
.text:0x400705 pop rbx
.text:0x400706 pop rbp
.text:0x400707 pop r12
.text:0x400709 retn
.text:0x400709 ; ---------------------------------------------------------------------------
.text:0x40070A align 10h
.text:0x400710
.text:0x400710 loc_400710: ; CODE XREF: fibonacci+10j
.text:0x400710 xor edi, edi
.text:0x400712 call fibonacci
.text:0x400717 mov edx, eax
.text:0x400719 mov edi, eax
.text:0x40071B shr edx, 1
.text:0x40071D and edx, 55555555h
.text:0x400723 sub edi, edx
.text:0x400725 mov esi, edi
.text:0x400727 mov edx, edi
.text:0x400729 shr esi, 2
.text:0x40072C and esi, 33333333h
.text:0x400732 jmp short loc_4006BC
.text:0x400732 fibonacci endp
There are many possible ways of solving this task. We can for example reconstruct the code in one of the programming languages and then apply optimizations there. The process of reconstructing code is not easy and of course, we can introduce bugs and errors. Staring at code to spot a mistake is not funny at all. By solving this task with Unicorn Engine we can skip the process of reconstructing code and avoid problems like mentioned above. We could skip rewriting code in a few other ways - for example by scripting gdb or using Frida.
Before applying optimizations we will first emulate normal program, without optimizations in Unicorn Engine. After the success, we will optimize it.
Part 1: Let’s emulate the program.
Let’s create a file named fibonacci.py and put the binary in the same folder.
Add the following code to the file:
from unicorn import *
from unicorn.x86_const import *
The first line loads main binary and basic unicorn constants. The second loads constants specific for both architectures x86 and x86-64.
Next, add the following lines:
import struct
def read(name):
with open(name) as f:
return f.read()
def u32(data):
return struct.unpack("I", data)[0]
def p32(num):
return struct.pack("I", num)
Here, we only added some usual functions that will be helpful later.
read
just returns contents of the whole file.
u32
takes a 4-byte string and converts it to an integer which represents this data in little endian.
p32
makes the opposite - it takes a number and converts it to representing 4-byte string in little endian.
If you have pwntools installed, you don’t need to create these functions.
Just do from pwn import *
.
Let’s initialize our Unicorn Engine class for architecture x86-64
:
mu = Uc (UC_ARCH_X86, UC_MODE_64)
We need to call function Uc
with following arguments:
- first - main architecture branch. The constant starts with
UC_ARCH_
- second - further architecture specification. The constant starts with
UC_MODE_
You can find a full list of architecture constants in Cheatsheet.
As I wrote previously, to use Unicorn Engine we need to initialize virtual memory manually. For this binary we need to write code somewhere and also allocate a stack.
The base of the binary is 0x400000
.
Let’s say our stack will start at address 0x0
and have size 1024*1024
.
Probably we don’t need so much space, but it won’t hurt us.
We can map our memory by calling the mem_map
method.
Add following lines:
BASE = 0x400000
STACK_ADDR = 0x0
STACK_SIZE = 1024*1024
mu.mem_map(BASE, 1024*1024)
mu.mem_map(STACK_ADDR, STACK_SIZE)
Now, we need to load the binary at our base address, like loader does. Then we need to set RSP
to point at the end of our stack.
mu.mem_write(BASE, read("./fibonacci"))
mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1)
We can start the emulation and run our code, but we need to know what is the start address and where emulator should stop.
We can start emulating the code at address 0x00000000004004E0
which is the first address of main
.
The end can be 0x0000000000400575
. This is putc("\n")
which is called after our whole flag is printed out. Look:
.text:0x400570 mov edi, 0Ah ; c
.text:0x400575 call __IO_putc
We can begin our simulation:
mu.emu_start(0x00000000004004E0, 0x0000000000400575)
Now, we can run this script:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py
Traceback (most recent call last):
File "solve.py", line 32, in <module>
mu.emu_start(0x00000000004004E0, 0x0000000000400575)
File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start
raise UcError(status)
unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
Oooops, something is wrong and we don’t know what. Right before mu.emu_start
we can add:
def hook_code(mu, address, size, user_data):
print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size))
mu.hook_add(UC_HOOK_CODE, hook_code)
This code adds a hook. We define our own function hook_code
that is called before emulation of each instruction.
It takes following arguments:
- our
Uc
instance - address of the instruction
- size of the instruction
- user data (we can pass this value in optional argument of
hook_add()
)
At this point, our script should look like solve1.py
When we run it, we can see:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py
>>> Tracing instruction at 0x4004e0, instruction size = 0x1
>>> Tracing instruction at 0x4004e1, instruction size = 0x1
>>> Tracing instruction at 0x4004e2, instruction size = 0x2
>>> Tracing instruction at 0x4004e4, instruction size = 0x5
>>> Tracing instruction at 0x4004e9, instruction size = 0x2
>>> Tracing instruction at 0x4004eb, instruction size = 0x4
>>> Tracing instruction at 0x4004ef, instruction size = 0x7
Traceback (most recent call last):
File "solve.py", line 41, in <module>
mu.emu_start(0x00000000004004E0, 0x0000000000400575)
File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start
raise UcError(status)
unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
This means that our script fails while executing following instruction:
.text:0x4004EF mov rdi, cs:stdout ; stream
This instruction reads memory from address 0x601038
(You can see it in IDA Pro).
This is .bss
section and it is not allocated by us.
My solution for this problem is just to skip all instructions that are problematic.
Below there is an instruction:
.text:0x4004F6 call _setbuf
We can’t call any glibc function because we don’t have glibc loaded to virtual memory. We don’t need to call this function anyway so we can also skip it.
This is a full list of instructions to skip:
.text:0x4004EF mov rdi, cs:stdout ; stream
.text:0x4004F6 call _setbuf
.text:0x400502 call _printf
.text:0x40054F mov rsi, cs:stdout ; fp
We can skip instructions by writing to the register RIP
address of next instruction:
mu.reg_write(UC_X86_REG_RIP, address+size)
hook_code
should now look like this:
instructions_skip_list = [0x00000000004004EF, 0x00000000004004F6, 0x0000000000400502, 0x000000000040054F]
def hook_code(mu, address, size, user_data):
print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size))
if address in instructions_skip_list:
mu.reg_write(UC_X86_REG_RIP, address+size)
We also have to do something with the instructions that print out the flag byte-by-byte.
.text:0x400558 movsx edi, dil ; c
.text:0x40055C add rbp, 1
.text:0x400560 call __IO_putc
__IO_putc
takes a byte to print out in the first argument
(that is register RDI
).
We can read a value from register RDI
, print it out and skip emulating this instruction.
The hook_code
function at this point should look like below:
instructions_skip_list = [0x00000000004004EF, 0x00000000004004F6, 0x0000000000400502, 0x000000000040054F]
def hook_code(mu, address, size, user_data):
#print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size))
if address in instructions_skip_list:
mu.reg_write(UC_X86_REG_RIP, address+size)
elif address == 0x400560: #that instruction writes a byte of the flag
c = mu.reg_read(UC_X86_REG_RDI)
print(chr(c))
mu.reg_write(UC_X86_REG_RIP, address+size)
At this point the whole code should look like solve2.py
We can run it and see that it works, although still slowly.
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py
h
x
Part 2: Improve the speed!
Let’s think about the speed improvements. Why is this program so slow?
Looking at the decompiled code, we can see that main()
calls fibonacci()
several times and fibonacci()
is a recursive function.
Taking look at this function we can see that it takes 2 arguments and it returns 2 values.
The first return value in passed via RAX
register, the second via reference through the second argument.
Taking a deeper look at both main()
and fibonacci()
we can notice that the second argument can only take value of 0
or 1
.
If we don’t see it, we can run gdb
and set a breakpoint at the beginning of fibonacci
function.
To optimize this function we can use dynamic programming
to remember return values for given arguments.
Since the second argument takes only 2 values, it is enough to remember only 2*MAX_OF_FIRST_ARGUMENT
pairs.
When RIP
points to the beginning of the fibonacci
function, we can obtain the function arguments.
We know function return values when exiting the function.
Since we don’t know both things at once we need to use a stack that will help us to obtain both things when exiting -
in fibonacci
entry we need to push arguments to stack and pop at the end.
To remember pairs we can use a dictionary.
How to hold the values of pairs?
- At the beginning of the function we can check if return values are memorized in the dictionary for these arguments
- If they are, we can return this pair. We just write return values to reference and
RAX
. We also setRIP
to address of someRET
instruction to exit the function. We cannot jump toRET
infibonacci
function because this instruction is hooked. That’s why we jump toRET
inmain
. - If they are not present in the dictionary, we add arguments to the stack.
- If they are, we can return this pair. We just write return values to reference and
- While exiting the function, we can save return values. We know arguments and reference pointer, because we can read them from our stack structure (here called “stack”).
The code is shown below:
FIBONACCI_ENTRY = 0x0000000000400670
FIBONACCI_END = [0x00000000004006F1, 0x0000000000400709]
stack = [] # Stack for storing the arguments
d = {} # Dictionary that holds return values for given function arguments
def hook_code(mu, address, size, user_data):
#print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size))
if address in instructions_skip_list:
mu.reg_write(UC_X86_REG_RIP, address+size)
elif address == 0x400560: # That instruction writes a byte of the flag
c = mu.reg_read(UC_X86_REG_RDI)
print(chr(c))
mu.reg_write(UC_X86_REG_RIP, address+size)
elif address == FIBONACCI_ENTRY: # Are we at the beginning of fibonacci function?
arg0 = mu.reg_read(UC_X86_REG_RDI) # Read the first argument. Tt is passed via RDI
r_rsi = mu.reg_read(UC_X86_REG_RSI) # Read the second argument which is a reference
arg1 = u32(mu.mem_read(r_rsi, 4)) # Read the second argument from reference
if (arg0,arg1) in d: # Check whether return values for this function are already saved.
(ret_rax, ret_ref) = d[(arg0,arg1)]
mu.reg_write(UC_X86_REG_RAX, ret_rax) # Set return value in RAX register
mu.mem_write(r_rsi, p32(ret_ref)) # Set retun value through reference
mu.reg_write(UC_X86_REG_RIP, 0x400582) # Set RIP to point at RET instruction. We want to return from fibonacci function
else:
stack.append((arg0,arg1,r_rsi)) # If return values are not saved for these arguments, add them to stack.
elif address in FIBONACCI_END:
(arg0, arg1, r_rsi) = stack.pop() # We know arguments when exiting the function
ret_rax = mu.reg_read(UC_X86_REG_RAX) # Read the return value that is stored in RAX
ret_ref = u32(mu.mem_read(r_rsi,4)) # Read the return value that is passed reference
d[(arg0, arg1)]=(ret_rax, ret_ref) # Remember the return values for this argument pair
Just in case, the full script you can download here. solve3.py
Hurrah! We’ve managed to successfully optimize the program using Unicorn Engine. Good job.
Some notes
Now, I encourage you to do a small homework. Below you can find 3 tasks, every one has a hint and solution available (through clicking spoiler button). You can look at Cheatsheet when solving the tasks.
I think that one of the problems is to know the name of interesting constant.
The best way to deal with it is to use IPython tab completion.
When you have IPython installed you can type from unicorn import UC_ARCH_
and press TAB
- all consatnts starting with this prefix will be printed out.
Task 2
Analyze the following shellcode:
shellcode = "\xe8\xff\xff\xff\xff\xc0\x5d\x6a\x05\x5b\x29\xdd\x83\xc5\x4e\x89\xe9\x6a\x02\x03\x0c\x24\x5b\x31\xd2\x66\xba\x12\x00\x8b\x39\xc1\xe7\x10\xc1\xef\x10\x81\xe9\xfe\xff\xff\xff\x8b\x45\x00\xc1\xe0\x10\xc1\xe8\x10\x89\xc3\x09\xfb\x21\xf8\xf7\xd0\x21\xd8\x66\x89\x45\x00\x83\xc5\x02\x4a\x85\xd2\x0f\x85\xcf\xff\xff\xff\xec\x37\x75\x5d\x7a\x05\x28\xed\x24\xed\x24\xed\x0b\x88\x7f\xeb\x50\x98\x38\xf9\x5c\x96\x2b\x96\x70\xfe\xc6\xff\xc6\xff\x9f\x32\x1f\x58\x1e\x00\xd3\x80"
As you can see, the assembly is obfuscated (command disasm
is a part of pwntools) :
a@x:~/Desktop/unicorn_engine_lessons$ disasm e8ffffffffc05d6a055b29dd83c54e89e96a02030c245b31d266ba12008b39c1e710c1ef1081e9feffffff8b4500c1e010c1e81089c309fb21f8f7d021d86689450083c5024a85d20f85cfffffffec37755d7a0528ed24ed24ed0b887feb509838f95c962b9670fec6ffc6ff9f321f581e00d380
0: e8 ff ff ff ff call 0x4
5: c0 5d 6a 05 rcr BYTE PTR [ebp+0x6a], 0x5
9: 5b pop ebx
a: 29 dd sub ebp, ebx
c: 83 c5 4e add ebp, 0x4e
f: 89 e9 mov ecx, ebp
11: 6a 02 push 0x2
13: 03 0c 24 add ecx, DWORD PTR [esp]
16: 5b pop ebx
17: 31 d2 xor edx, edx
19: 66 ba 12 00 mov dx, 0x12
1d: 8b 39 mov edi, DWORD PTR [ecx]
1f: c1 e7 10 shl edi, 0x10
22: c1 ef 10 shr edi, 0x10
25: 81 e9 fe ff ff ff sub ecx, 0xfffffffe
2b: 8b 45 00 mov eax, DWORD PTR [ebp+0x0]
2e: c1 e0 10 shl eax, 0x10
31: c1 e8 10 shr eax, 0x10
34: 89 c3 mov ebx, eax
36: 09 fb or ebx, edi
38: 21 f8 and eax, edi
3a: f7 d0 not eax
3c: 21 d8 and eax, ebx
3e: 66 89 45 00 mov WORD PTR [ebp+0x0], ax
42: 83 c5 02 add ebp, 0x2
45: 4a dec edx
46: 85 d2 test edx, edx
48: 0f 85 cf ff ff ff jne 0x1d
4e: ec in al, dx
4f: 37 aaa
50: 75 5d jne 0xaf
52: 7a 05 jp 0x59
54: 28 ed sub ch, ch
56: 24 ed and al, 0xed
58: 24 ed and al, 0xed
5a: 0b 88 7f eb 50 98 or ecx, DWORD PTR [eax-0x67af1481]
60: 38 f9 cmp cl, bh
62: 5c pop esp
63: 96 xchg esi, eax
64: 2b 96 70 fe c6 ff sub edx, DWORD PTR [esi-0x390190]
6a: c6 (bad)
6b: ff 9f 32 1f 58 1e call FWORD PTR [edi+0x1e581f32]
71: 00 d3 add bl, dl
73: 80 .byte 0x80
Note that the architecture is x86-32
now.
List of syscalls numbers can be found here.
Hint:
Solution:
Task 3
Download this binary.
It was compiled with the following command:
gcc function.c -m32 -o function
.
The code of this binary is presented below:
int strcmp(char *a, char *b)
{
//get length
int len = 0;
char *ptr = a;
while(*ptr)
{
ptr++;
len++;
}
//comparestrings
for(int i=0; i<=len; i++)
{
if (a[i]!=b[i])
return 1;
}
return 0;
}
__attribute__((stdcall))
int super_function(int a, char *b)
{
if (a==5 && !strcmp(b, "batman"))
{
return 1;
}
return 0;
}
int main()
{
super_function(1, "spiderman");
}
The task is to call super_function
in a way that it will return 1
.
The assembly code is:
.text:0x8048464 super_function proc near ; CODE XREF: main+16p
.text:0x8048464
.text:0x8048464 arg_0 = dword ptr 8
.text:0x8048464 arg_4 = dword ptr 0Ch
.text:0x8048464
.text:0x8048464 push ebp
.text:0x8048465 mov ebp, esp
.text:0x8048467 call __x86_get_pc_thunk_ax
.text:0x804846C add eax, 1B94h
.text:0x8048471 cmp [ebp+arg_0], 5
.text:0x8048475 jnz short loc_8048494
.text:0x8048477 lea eax, (aBatman - 804A000h)[eax] ; "batman"
.text:0x804847D push eax
.text:0x804847E push [ebp+arg_4]
.text:0x8048481 call strcmp
.text:0x8048486 add esp, 8
.text:0x8048489 test eax, eax
.text:0x804848B jnz short loc_8048494
.text:0x804848D mov eax, 1
.text:0x8048492 jmp short locret_8048499
.text:0x8048494 ; ---------------------------------------------------------------------------
.text:0x8048494
.text:0x8048494 loc_8048494: ; CODE XREF: super_function+11j
.text:0x8048494 ; super_function+27j
.text:0x8048494 mov eax, 0
.text:0x8048499
.text:0x8048499 locret_8048499: ; CODE XREF: super_function+2Ej
.text:0x8048499 leave
.text:0x804849A retn 8
.text:0x804849A super_function endp
Hint:
Solution:
Task 4
This task is similar to the first one. The difference is that the architecture is not x86
anymore.
It is ARM32
little-endian.
a@x:~/Desktop/unicorn_engine_lessons$ file task4
task4: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=3dbf508680ba3d023d3422025954311e1d8fb4a1, not stripped
You can download the binary here.
Arm calling convention will help you.
Right answer:
Hint:
Solution:
CheatSheet
from unicorn import *
- Loads main unicorn library. It contains functions and basic constants.
from unicorn.x86_const import *
- Loads constants specific for architectures x86
and x86-64
All consts in module unicorn
:
UC_API_MAJOR UC_ERR_VERSION UC_MEM_READ UC_PROT_ALL
UC_API_MINOR UC_ERR_WRITE_PROT UC_MEM_READ_AFTER UC_PROT_EXEC
UC_ARCH_ARM UC_ERR_WRITE_UNALIGNED UC_MEM_READ_PROT UC_PROT_NONE
UC_ARCH_ARM64 UC_ERR_WRITE_UNMAPPED UC_MEM_READ_UNMAPPED UC_PROT_READ
UC_ARCH_M68K UC_HOOK_BLOCK UC_MEM_WRITE UC_PROT_WRITE
UC_ARCH_MAX UC_HOOK_CODE UC_MEM_WRITE_PROT UC_QUERY_MODE
UC_ARCH_MIPS UC_HOOK_INSN UC_MEM_WRITE_UNMAPPED UC_QUERY_PAGE_SIZE
UC_ARCH_PPC UC_HOOK_INTR UC_MILISECOND_SCALE UC_SECOND_SCALE
UC_ARCH_SPARC UC_HOOK_MEM_FETCH UC_MODE_16 UC_VERSION_EXTRA
UC_ARCH_X86 UC_HOOK_MEM_FETCH_INVALID UC_MODE_32 UC_VERSION_MAJOR
UC_ERR_ARCH UC_HOOK_MEM_FETCH_PROT UC_MODE_64 UC_VERSION_MINOR
UC_ERR_ARG UC_HOOK_MEM_FETCH_UNMAPPED UC_MODE_ARM Uc
UC_ERR_EXCEPTION UC_HOOK_MEM_INVALID UC_MODE_BIG_ENDIAN UcError
UC_ERR_FETCH_PROT UC_HOOK_MEM_PROT UC_MODE_LITTLE_ENDIAN arm64_const
UC_ERR_FETCH_UNALIGNED UC_HOOK_MEM_READ UC_MODE_MCLASS arm_const
UC_ERR_FETCH_UNMAPPED UC_HOOK_MEM_READ_AFTER UC_MODE_MICRO debug
UC_ERR_HANDLE UC_HOOK_MEM_READ_INVALID UC_MODE_MIPS3 m68k_const
UC_ERR_HOOK UC_HOOK_MEM_READ_PROT UC_MODE_MIPS32 mips_const
UC_ERR_HOOK_EXIST UC_HOOK_MEM_READ_UNMAPPED UC_MODE_MIPS32R6 sparc_const
UC_ERR_INSN_INVALID UC_HOOK_MEM_UNMAPPED UC_MODE_MIPS64 uc_arch_supported
UC_ERR_MAP UC_HOOK_MEM_VALID UC_MODE_PPC32 uc_version
UC_ERR_MODE UC_HOOK_MEM_WRITE UC_MODE_PPC64 unicorn
UC_ERR_NOMEM UC_HOOK_MEM_WRITE_INVALID UC_MODE_QPX unicorn_const
UC_ERR_OK UC_HOOK_MEM_WRITE_PROT UC_MODE_SPARC32 version_bind
UC_ERR_READ_PROT UC_HOOK_MEM_WRITE_UNMAPPED UC_MODE_SPARC64 x86_const
UC_ERR_READ_UNALIGNED UC_MEM_FETCH UC_MODE_THUMB
UC_ERR_READ_UNMAPPED UC_MEM_FETCH_PROT UC_MODE_V8
UC_ERR_RESOURCE UC_MEM_FETCH_UNMAPPED UC_MODE_V9
A few examples of constants from unicorn.x86_const
:
UC_X86_REG_EAX
UC_X86_REG_RIP
UC_X86_REG_RAX
mu = Uc(arch, mode)
- get an instance of Uc
class. Here you specify the architecture.
Examples:
-
mu = Uc(UC_ARCH_X86, UC_MODE_64)
- get an Uc instance for architecturex86-64
-
mu = Uc(UC_ARCH_X86, UC_MODE_32)
- get an Uc instance for architecturex86-32
mu.mem_map(ADDRESS, 4096)
- map a memory region.
mu.mem_write(ADDRESS, DATA)
- write data to memory.
tmp = mu.mem_read(ADDRESS, SIZE)
- read data from memory.
mu.reg_write(UC_X86_REG_ECX, 0x0)
- set a register to a new value.
r_esp = mu.reg_read(UC_X86_REG_ESP)
- read a value from a register.
mu.emu_start(ADDRESS_START, ADDRESS_END)
- start emulation.
instruction tracking:
def hook_code(mu, address, size, user_data):
print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size))
mu.hook_add(UC_HOOK_CODE, hook_code)
This code adds a hook. We define our own function hook_code
that is called before emulation of each instruction.
It takes following arguments:
- our
Uc
instance - address of the instruction
- size of the instruction
- user data (we can pass this value in optional argument of
hook_add()
)