Insight Interactive Inspector
Insight Interactive Inspector
Table of Contents
1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2 Analyzed program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
3 Basic Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
3.1 pynsight interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
3.2 Getting started. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
3.3 Step-by-step simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.4 Hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.5 Graphical views . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
4 Advanced features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4.1 Using stubs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4.2 Initialization file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
4.3 Examining memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.4 Assignment of abstract values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.5 Breakpoints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.6 Concretization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
5 Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
6 References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Appendix B Stubs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
B.1 __libc_start_main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
B.2 __printf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
B.3 __read . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1 Introduction
This document describes the use of the iii tool for the analysis of an obfuscated executable
file. This tutorial does not cover all features of the tool but presents the most usual ones. iii
is essentially a simulator built on top of Insight framework ([Insight], page 27). Symbolic
simulation can be an efficient tool for the debugging and the understanding of program
behaviors.
Insight framework permits to interpret program semantics over different domains. Even
if its design is quite independent of the interpretation domain, iii allows, for now, only
two kinds of values: concrete and formula. In the first case, the tool behaves roughly like a
classical debugger. However, iii is limited to the interpretation of the internal model of the
program i.e., its microcode. Many low-level aspects are not yet captured by the framework:
system calls, dynamic loading, multi-threading and so on. To be short, microcode model
has threes kind of instructions:
1. Assignments lv := E where lv is a l-value i.e., a register or a memory cell and E is a
bitvector expression.
2. Guarded static jumps change the program counter to an address known a priori, if the
guard is satisfied.
3. Dynamic jumps that change the program counter to an address computed on-the-fly
by the program.
By default, values are interpreted as formulas. In this case iii behaves like a sym-
bolic simulator ([JK76], page 27). Since the arising of efficient solvers, symbolic simulation
has became an effective tool for the analysis of programs. For more details on symbolic
simulation we refer the reader to the literature, this document focus only on main ideas.
Regardless of the interpretation domain, the simulator maintains a state of the simulation
that models the content of the memory and registers, and the value of the program counter.
In the context of symbolic simulation this state associates a formula to each byte of the
memory and to each register that have been accessed during the symbolic execution; the
program counter is a concrete address that points somewhere into the loaded memory. In
addition to these three components, a symbolic state possesses a fourth one: a formula
called the path-condition which is initially set to true.
Each time an instruction of the microcode is interpreted, the symbolic state is changed
as follows:
• If the instruction is an assignment lv = E where lv is an l-value (i.e., either a register
or a memory cell) and E is an expression then, each register and memory cell used by
E are replaced by their assigned value in the current state. If some register or memory
cell, says x, used by E is not assigned then a fresh variable fv is generated and assigned
to x in the new state and x is replaced by fv in E. Finally the formula obtained from
E when all substitutions have been done is assigned to lv in the new state.
• If the instruction is a static jump to the address tgt and if the jump is guarded by some
condition G, then the simulator check first if G is satisfiable in the current state. To
this aim, as for assignments, any occurrence of a register or memory cell is replaced by
its current value or a fresh variable in G; this gives a formula G’. The solver is then used
to verify the satiafiability of (G’ and pc) where pc is the current path-condition. If the
Chapter 1: Introduction 2
formula is satisfiable then the new path-condition becomes (G’ and pc) and program
counter is updated; else the simulator tries the next microcode instruction at the same
address or stops.
• If the instruction is a dynamic jump to an address obtained from an expression E, then,
as for others instructions, registers and memory cells are replaced by their value in E.
And, the solver is used to compute a valid value addr for E under the constraint of
the path condition. If this is the case, the constraint (E = addr) is added to the new
path condition and the program counter is set to addr.
Chapter 2: Analyzed program 3
2 Analyzed program
The program we are studying using iii is a crackme challenge, meaning that the program
implements a few security mechanisms to protect some critical parts of the software and we
have to bypass these to access the protected data.
The binary code has been obtained by compiling a small x86 assembly program using
fasm assembler ([FASM], page 27). The source code of this challenge is given in Appendix A
[Crackme source code], page 28.
As shown below, the behavior of this program is simple: It displays a prompt, reads
on the standard input a password and spawns a /bin/sh shell program if the password is
correct or exits in the other case.
$ ./crackme
Enter password:
toto
Wrong password
$ ./crackme
Enter password:
Iv6oCb2U
sh-4.2$
Our challenge is to discover, from the binary code, the password that permits the ex-
ecution of the shell program. In order to counter analysis of the binary file, the program
implements fences to protect itself from reverse-engineering:
1. The password is not stored as-is in the binary data but is hashed.
2. The algorithm that computes the hash-value of the input is cyphered and this part of
the binary is uncyphered on-the-fly at execution time.
The last point imposes to change the read/write/execute flags of the .text section of
the executable to allow self-modifying code (see elf (5)).
Chapter 3: Basic Features 4
3 Basic Features
iii>
The interpreter indicates that iiirc module has not been found. This point will be
clarified in Section 4.2 [Initialization file], page 14.
After the prompt, iii>, any Python script can be executed:
iii> for i in range(5):
... print 2*i
...
0
2
4
6
8
iii>
In the context of iii, some modules are pre-loaded. The most interesting one,
[Link], contains all functions proposed by iii. Since we are in a Python
interpreter, documentation related to Insight modules can be displayed using the help
function; as suggested by the banner try ’help([Link])’ to discover all
functions implemented in iii.
Since iii is an interactive tool, several shorcuts have been defined for most frequently
used commands e.g., run, step, ... The function aliases lists these shortcuts:
iii> aliases()
ms -> microstep
P -> prog
cond -> cond
ep -> entrypoint
...
Each function should be documented. For instance help(microstep), or equivalently
help(ms), describes the behavior of microstep function.
$ iii crackme
crackme has been successfully loaded. Another way to load a binary file is to use the
binfile function. We can get informations related to what kind of program is currently
loaded; to this purpose we use the function info():
iii> info()
address_size : 0x20(32)
memory_min_address : 0x8048000(134512640)
memory_max_address : 0x80ec4b3(135185587)
format : elf32-i386
inputname : crackme
registers : ’ac’: 1, ’gs’: 16, ’af’: 1, ’zf’: 1, ’edi’: 32,
’iopl’: 2, ’cf’: 1, ’vip’: 1, ’ebp’: 32, ’cs’: 16, ’vif’: 1, ’edx’: 32,
’ebx’: 32, ’id’: 1, ’es’: 16, ’if’: 1, ’esp’: 32, ’rf’: 1, ’pf’: 1,
’tf’: 1, ’nt’: 1, ’esi’: 32, ’fs’: 16, ’df’: 1, ’vm’: 1, ’eax’: 32,
’ds’: 16, ’ecx’: 32, ’ss’: 16, ’of’: 1, ’sf’: 1
entrypoint : 0x8048c18(134515736)
word_size : 0x20(32)
endianness : little
cpu : x86-32
iii>
Among other informations, info() gives the list of registers with their respective size in
number of bits.
In iii, most of functions are related to simulation. Among them, one cannot be avoided:
run(). This function starts the simulation of the loaded program. A simulation-related
function should fail if it has not been preceded by a call to run(). For instance, if we
request the execution of one assembler instruction using the step() function while the
simulation is not started we obtain:
iii> step()
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/point/LaBRI/Projects/dev/install/linux-x86_64/share/insight/pynsight/in
sight/[Link]", line 272, in step
__record(pc(), step, a)
File "/home/point/LaBRI/Projects/dev/install/linux-x86_64/share/insight/pynsight/in
sight/[Link]", line 583, in pc
return mcpc()[0]
File "/home/point/LaBRI/Projects/dev/install/linux-x86_64/share/insight/pynsight/in
sight/[Link]", line 594, in mcpc
return simulator.get_pc()
SimulationNotStartedException
iii>
The reader should have noticed that pynsight interpreter prints out its call stack. Re-
member that we are interacting with a Python interpreter; step() is a simple call to a
function which could have been invoked from a script.
Chapter 3: Basic Features 6
[esi{0;32} = [uv_3_0x8048c1a_32b{0;32}]{0;32}]
[zf{0;1} = 0x1{0;1}]
[cf{0;1} = 0x0{0;1}]
[ebp{0;32} = 0x0{0;32}]
[pf{0;1} = 0x1{0;1}]
condition = 0x1{0;1}
>
iii>
The displayed state indicates that the simulator is currently stopped at microcode ad-
dress (0x8048c1d,0). It also shows that no memory cell has been yet assigned by the
program; nothing is displayed after MemoryDump message1 . Up to now, only registers have
been assigned. Some have received constant values; for instance, zf flag has been set to
0x1{0;1}. Others registers are assigned with formulas e.g. ecx is assigned with the value
(ADD uv_3_0x8048c1a_32b{0;32} 0x4{0;32}){0;32}. uv_3_0x8048c1a_32b is the iden-
tifier of a fresh variable created by the simulator. The identifier gives us some informations
on its creation context:
• uv: This variable has been allocated when an unknown value had to be assigned to a
register or a memory cell.
• 3: This is the third fresh variable created so far.
• 0x8048c1a: This variable has been created by the instruction at the address 0x8048c1a
• 32b: This variable is a bitvector of size 32 bits.
If we have a look to instruction at 0x8048c1a we obtain:
iii> instr(0x8048c1a)
pop %esi
iii>
What is the connection with ecx ? Actually, this instruction pops the top of the stack
and stores the value into the register esi. The top of the stack is pointed out by the register
esp. When this instruction has been triggered, we has the following context:
• esp was not assigned. In order to continue, the simulator assigned to esp an unknown
value abstracted with a fresh variable: uv_3_0x8048c1a_32b.
• Then, the top of the stack can be assigned to esi; it is the memory cell located at
the address pointed by esp i.e. uv_3_0x8048c1a_32b. esi receives the value/formula:
[uv_3_0x8048c1a_32b{0;32}]{0;32}].
• The top of the stack must be removed thus, according to Intel specifications, esp is
increased to point 4 bytes forward: it is assigned the value (ADD uv_3_0x8048c1a_
32b{0;32} 0x4{0;32}){0;32}.
• Finally the value of ecx comes from the next instruction located at address
(0x8048c1b,0): mov %esp,%ecx.
Like other debuggers, we can let iii run the simulation until we interrupt it or something
enforces it to stop. iii’s continue function, is called cont() (or c()). The following example
restarts the simulation and execute cont() just after the run() call.
iii> run()
Arrows from (0x8048c18,0):
1
This does not mean that the memory is empty! Actually some parts of the memory is already occupied
by the loaded sections of the program itself.
Chapter 3: Basic Features 9
3.4 Hooks
In order to prevent the problem with an unknown esp, we should assign it just after the
call to run(). When the simulator is started, the memory and registers are not initialized;
thus, any value can be assigned to esp. However, it is preferable to choose a value that
have a sense for the program. Usually 0xFFFFFFF0 is a good candidate.
When debugging a program, run() is called quite often (for the entrypoint or elsewhere)
and initializing esp each time becomes a tedious task. Fortunately iii possesses a mean
to get rid of such repeative work.
2
Which is confirmed by a call to instr().
3
This assignment is usually done by the OS which is not described in our model.
Chapter 3: Basic Features 10
The reader can notice that the value of zf has been assigned to the path condition of
the state. Each time we enforce the simulator to follow an arrow with a guard, this guard
is conjuncted to the path condition of the current state.
Now, we continue a little bit more with cont().
iii> c()
stop in a configuration with several output arrows
Arrows from (0x8057c89,0):
0 : (0x8057c89,0) << %zf{0;1} >> Skip --> (0x8057dce,0)
1 : (0x8057c89,0) << (NOT %zf{0;1}){0;1} >> Skip --> (0x8057c8f,0)
iii>
Since we are yet stopped by a conditional jump, we could take a while to have a look
to the code of the program. This can be done using the disas() function. This function
accepts several parameters. The first one is a start address from which the function have
to display instructions; if it is omitted the whole program is displayed. A second one is l,
the number of instructions to be displayed (set by default to 20). Let’s have a look to the
ten first instructions from the entrypoint:
iii> disas(entrypoint(),l=10)
08048c18 <_start>:
8048c18: xor %ebp,%ebp
8048c1a: pop %esi
8048c1b: mov %esp,%ecx
8048c1d: and $0xfffffff0,%esp
8048c20: push %eax
8048c21: push %esp
8048c22: push %edx
8048c23: push $0x80494c0
8048c28: push $0x8049420
8048c2d: push %ecx
8048c2e:
iii>
Note that these instructions are those collected from the microcode built during the
simulation and not the output of a direct linear-sweep on the binary code.
4 Advanced features
create a microcode model that jump to the address 0x8048da9 but this is not re-usable at
all; we should prefer to jump to the address stored in the stack.
Microcode files are XML files. Even if the model for __libc_start_main is quite simple,
writing it by hands is a tedious task. The simplest way to proceed is to:
1. Write the abstraction into a small x86 assembly program:
$ cat stub_libc_start_main.s
jmp *4(%esp)
$
2. Compile it using gcc for instance;
$ gcc -m32 -c stub_libc_start_main.s -o stub_libc_start_main.o
3. Generate the microcode file using the cfgrecovery tool. A simple linear sweep disas-
sembly is sufficient to generate the exact microcode for this small program
$ cfgrecovery -f mc-xml -d linear stub_libc_start_main.o -o \
stub_libc_start_main.[Link]
Now we have the abstraction for __libc_start_main stored into the file stub_libc_
start_main.[Link]; it remains to load it at the address pointed by the __libc_start_main
symbol. The function load_stub() is used for this purpose. All stubs should be loaded
before the first call to run(). Actually stubs are merged into the microcode on demand
and not as a replacement of existing microcode. load_stub() takes three arguments: a
filename, an address and a Boolean that indicates whether the microcode must be relocated
at the same microcode address or not.
# initialization of register
valregs = {
’esp’ : 0xFFFFFFF0,
’df’ : 0 # mandatory for string operations
}
# useful hooks
def init_registers ():
global valregs
for r in P().info()[’registers’]:
if r in valregs:
val = valregs[r]
set(r, val)
Starting iii with the above initialization module will produce the following output:
$ iii
Insight Interactive Inspector
Try ’help([Link])’ to get information on debugger commands.
Type ’aliases()’ to display list of defined aliases.
iii>
And, thanks to the view_asm window, we can see on Figure 4.1 that the simulation has
gone yet farther. Indeed, the actual code of __lib_start_main has been skipped thanks to
its attached stub. However, iii is now simulating another function of the standard library:
printf. This function will be replaced by the following stub:
mov $0x0, %eax
ret
We assume that it has no interesting side effect and enforce its return value to 0 which
is stored to eax. Another standard function should appear later, read, which is called to
read the password. The stub for this function is the following:
mov 12(%esp), %eax
mov %eax, %ecx
Chapter 4: Advanced features 16
This stub simulates the read of n characters stored into a memory area at address p. n
and p are themself stored onto the stack at 12(%esp) and 8(%esp), respectively. Then the
stub assigns the n memory cells with the value 0x33. This special value is used to locate
the assignment into the microcode file. This value is replaced by hand (or a sed script) by
a RND expression that will be interpreted as an unknown value by the symbolic simulator.
Figure 4.1: Graphical view of the Microcode. __lib_start_main has been replaced by
its stub but, __printf is now decoded.
We add the new load_stub calls into the initialization file and restart the tool. The
simulation stops on a jne instruction. The CFG is the following:
Chapter 4: Advanced features 17
After the call to read i.e., the user has entered its password, the program enters into a
loop. The small piece of code that precedes the loop does the following things:
• At address 0x8048ddb, the return value of read is copied from eax into edx. edx is
then decremented and a null character is stored at the address 0x8048d44+eax. Indeed,
the last byte read from the standard input is a carriage-return character. At address
0x8048dde, it is replaced by 0 in order to form a valid null-terminated C string. edx
store the length of the password and must not take into account the last null byte.
• From addresses 0x8048de5 to 0x8048dec, registers esi, edi and ecx are prepared for
some string operation. esi and edi are the source and destination pointers of the
operation and ecx the number of iterations. We can see that, esi and edi have the
same starting value (0x8048e08).
According to the value assigned to ecx, the loop is iterated 28 times. At each iteration of
the loop, the byte pointed by esi is XOR-ed with 0xaa. The loop covers addresses between
0x8048e08 and 0x8048e24 which are located few bytes after the loop itsef. This means that
this loop is used to modify instructions just after the loop; it is a known trick to obfuscate
programs and prevent static-analysis of it.
We can compare on-the-fly decoded instructions by the simulator and the ones that were
loaded at start-up using functions disas (left) and [Link].pretty_disas_memory2
(right). The latter uses a linear sweep algorithm on the original binary file raw bytes.
2
prog() returns an opaque object that contains the loaded binary file.
Chapter 4: Advanced features 18
iii> cont(0)
sink node reached after(0x8048e59, 2)
Arrows from (0x8048c39,0):
iii>
The tool indicates that the simulator has reached a sink node i.e., no successor state
can be visited. If we have a look at the code visited after the conditional jump at address
0x8048e15 we obtain:
iii> disas(0x8048e3f,10)
08048e3f <L_2>:
8048e3f: mov %esp,%ebp
8048e41: sub $0x4,%esp
8048e44: and $0xfffffff0,%esp
8048e47: add $0x4,%esp
8048e4a: push $0x8048e62
8048e4f: call 0x8049dd0 # jump to : printf, __printf, _IO_printf
08048e54 <L_4>:
8048e54: mov %ebp,%esp
8048e56: xor %eax,%eax
8048e58: inc %eax
8048e59: ret # jump to : L_0
8048e5a:
The program prints out (using printf) a string stored at address 0x8048e62 and then
returns (to the termination instruction hlt). dump function is used to display values stored
in the memory for the current state. Let’s try to dump it directly from the memory:
iii> dump(0x8048e62, l = 10)
0x57{0;8}
0x72{0;8}
0x6f{0;8}
0x6e{0;8}
0x67{0;8}
0x20{0;8}
0x70{0;8}
0x61{0;8}
0x73{0;8}
Chapter 4: Advanced features 19
0x73{0;8}
iii>
We get 10 values. Actually these are symbolic values; even if they are concrete. Here we
were lucky to get constants and no abstract values. dump() accepts an additional parameter
that is a callback called to transform the value returned by the simulator. The following
Python script defines a function that we will use later to translate abstract constants into
printable characters. This code is added to the configuration file.
import re
p
a
s
s
iii>
We can deduce from the content of this string that we have followed the branch where
the user enters a wrong password.
We are back to the instruction at 0x8048e15 but this time we have visited a new loop
as show on the following CFG:
Chapter 4: Advanced features 20
Figure 4.2
The reader should wonder why the simulation did not stop at instruction 0x8048e17
while two outgoing arrows exist there. Indeed, the simulator follows #0 arrow because it
is the only one that is enabled ! Let’s see why. We make a single step from 0x8048e15 to
0x8048e17.
iii> arrows()
Arrows from (0x8048e15,0):
0 : (0x8048e15,0) << (NOT %zf{0;1}){0;1} >> Skip --> (0x8048e3f,0)
1 : (0x8048e15,0) << %zf{0;1} >> Skip --> (0x8048e17,0)
iii> s(1)
Arrows from (0x8048e17,0):
Chapter 4: Advanced features 21
On the CFG, we can see that, after the loop, the program reaches a call to the libc
function system(). It is invoked with the string located at address 0x8048e5a as parameter
(see instruction at 0x8048e2f).
iii> dump(0x8048e5a,l=10,filter=filter_abstract_byte)
/
b
i
n
/
s
h
W
r
iii>
Good news! The call to system() spawns a shell /bin/sh; this is where we have to go.
We, now, get back to the analysis of the loop.
4.5 Breakpoints
In order to study the loop between addresses 0x8048e08 and 0x8048e17 (see Figure 4.2),
we execute r(); c(). As suggested by instructions at 0x8048df7 and 0x8048dfc, we are
apparently in a loop that compares strings located at addresses 0x8048d45 and 0x8048e1b.
Since the instruction loop is used to iterate the comparisons, it means that the length of
Chapter 4: Advanced features 23
the strings is stored into ecx by instruction at 0x8048e01 which assigns edx to ecx. Now
remember that edx is the length of the input (see remarks related to read in Section 4.2
[Initialization file], page 14).
Let’s have a look to these strings:
iii> register("edx")
0x80;32
iii> dump(0x8048d45, l = 8, filter = filter_abstract_byte)
uv_58_0x8056020_3_8b{0;8}
uv_60_0x8056020_3_8b{0;8}
uv_62_0x8056020_3_8b{0;8}
uv_64_0x8056020_3_8b{0;8}
uv_66_0x8056020_3_8b{0;8}
uv_68_0x8056020_3_8b{0;8}
uv_70_0x8056020_3_8b{0;8}
uv_72_0x8056020_3_8b{0;8}
iii> dump(0x8048e1b, l = 8, filter = filter_abstract_byte)
?
Z
2
P
4
>
?
?
iii>
We can deduce that string at 0x8048d45 is the input string (fresh variable has been
generated by __read). String at 0x8048e1b is quite cryptic; it should be a hashed value of
the password.
Interpreting the code of the loop could be a complicated task. In order to understand it
we can try to wait the termination of the loop and have a look to the content of the state.
To this aim we could iterate the loop by hand until instruction 0x8048e17 permits us
to go to 0x8048e19 but this a tedious work. A more generic way is the following:
1. We create a conditional breakpoint at 0x8048e17 that will be enabled when ecx is
equal to 1 (loop first decreases ecx before checking its value).
2. We add a constraint at 0x8048e15 that enforces the simulator to stay in the loop.
The function breakpoint() requests the simulator to stop at a given address. Used
without argument, it sets a breakpoint at the current program point. Otherwise, it takes a
microcode address (a global and a local address); and the latter is by default set to 0. The
function returns the identifier of the breakpoint; it can be used later by a client script.
iii> breakpoint(0x8048e17)
breakpoint set at (0x8048e17,0) with id=1.
1
iii>
To make a breakpoint conditional we use the function cond(). It can be invoked with
one or two arguments. The first one is always an identifier of a breakpoint. The second
one is a string that contains the condition to enable the breakpoint. The syntax of these
expressions is given elsewhere in Insight documentation. If the second argument is omitted
the condition is removed from the breakpoint.
iii> cond (1, "(EQ %ecx 1)")
making breakpoint 1 conditional
Chapter 4: Advanced features 24
4.6 Concretization
After a while we get:
iii> cont()
stop condition 1 reached: breakpoint: (0x8048e17,0) cond = (EQ %ecx{0;32} 0x1{0;32})
{0;1}
Arrows from (0x8048e17,0):
0 : (0x8048e17,0) %ecx{0;32} := (SUB %ecx{0;32} 0x1{0;32}){0;32} --> (0x8048e17,1)
iii> s()
Arrows from (0x8048e19,0):
0 : (0x8048e19,0) Skip --> (0x8048e24,0)
iii>
The simulator says that it stops because it encounters the conditional breakpoint at
address 0x8048e17 while its condition enables it. Contrary to previous sections, this time,
if we make a step forward the simulator exits the loop. Now, we have to look at what has
been computed.
In Section 4.5 [Breakpoints], page 22, we have discovered that the string at address
0x8048d45 is the input given by the user. If we look at the content of the string we can
notice that it does not change.
iii> dump(0x8048d45, l = 8, filter = filter_abstract_byte)
uv_58_0x8056020_3_8b{0;8}
uv_60_0x8056020_3_8b{0;8}
uv_62_0x8056020_3_8b{0;8}
uv_64_0x8056020_3_8b{0;8}
uv_66_0x8056020_3_8b{0;8}
uv_68_0x8056020_3_8b{0;8}
uv_70_0x8056020_3_8b{0;8}
Chapter 4: Advanced features 25
uv_72_0x8056020_3_8b{0;8}
A quick look at the iterated code shows that it just compute a value that is then compared
at the corresponding offset in the hashed password located at 0x8048e1b.
iii> disas(0x8048e08,l=10)
08048e08 <L_3>:
8048e08: xor %eax,%eax
8048e0a: lods %ds:(%esi),%al
8048e0b: add %ebx,%eax
8048e0d: shl %eax
8048e0f: xor $0x12,%eax
8048e12: mov %al,%bl
8048e14: scas %es:(%edi),%al
8048e15: jne 0x8048e3f # jump to : L_2
8048e17: loop 0x8048e08 # jump to : L_3
8048e19: jmp 0x8048e24 # jump to : L_4
8048e1b:
iii>
Actually the data related to computed values is stored in the path condition accumulated
each time we enforced the simulator to follow the arrow #1 to stay in the loop.
Now, we are outside the loop. This means that the current state of the simulation
encodes all traces that can reach the current program point (i.e., 0x8048e19). Thanks
to the SMT-solver integrated to Insight, it is possible to compute an assignment of fresh
variables (i.e., a concrete input) that satisfies the path-condition of the current state.
Here, we reuse a function already encountered: set(). In Section 3.3 [Step-by-step
simulation], page 6, this section was used to assign a value to register esp; the function was
called with two arguments. This time, we use set without specifying a value. In this case,
iii requests the SMT solver to pick-up a value that is consistent with the current state.
The following script calls set for each byte of the input string. Then iii displays values
computed by the solver.
iii> for i in range(8): set (0x8048d45 + i)
...
iii> dump(0x8048d45, l = 8, filter = filter_abstract_byte)
I
v
6
o
C
b
2
U
iii>
It is the password; see Chapter 2 [Analyzed program], page 3.
Chapter 5: Acknowledgements 26
5 Acknowledgements
The crackme has been originally written by Renaud Tabary, a former member of the Insight
team and improved by Gérald Point. This tutorial has been first presented at Dagstuhl
seminar 14241, Challenges in Analysing Executables: Scalability, Self-Modifying Code and
Synergy in June 2014, at Dagstuhl Schloss, Germany. We would like to thanks all the
attenders for their precious feedback and comments on this tutorial, but also all the people
that sent us their feedback.
Chapter 6: References 27
6 References
[DOT] Graphviz - Graph Visualization Software. [Link] 2009.
[FASM] Tomasz Grysztar. Flat Assembler. [Link] 2014.
[XDot] Jose Fonseca. [Link] 2014.
[JK76] James C. King. Symbolic execution and program testing. Communications of
the ACM, volume 19, number 7, 385–394, 1976.
[Insight] Insight Framework. [Link] 2014.
Appendix A: Crackme source code 28
format ELF
include ’[Link]’
public main
extrn printf
extrn system
extrn read
extrn strcmp
main:
;pwd = Iv6oCb2U
mov esi,debut_crypt
mov edi,esi
mov ecx,to_crypt
decrypt:
lodsb ; obfuscation par chiffrement de code
xor al,0xAA
stosb
loop decrypt
mov esi,buffer
Appendix A: Crackme source code 29
mov edi,pwd
mov ecx,edx
mov ebx, 0x0015
debut_crypt:
teste:
xor eax, eax
lodsb
add eax, ebx
shl eax, 1
xor eax, 0x12
mov bl, al
scasb
jnz ko
loop teste
jmp ok
pwd db 174, 90, 50, 80, 52, 62, 242, 156, 0
to_crypt=$-debut_crypt
crypt debut_crypt,$-debut_crypt
ok:
ccall system,shell
xor eax,eax
jmp fin
ko:
ccall printf,msg2
xor eax,eax
inc eax
fin:
ret
shell db "/bin/sh",0
msg2 db "Wrong password",0xA,0
The example includes the following file containing the ccall macro:
Appendix B Stubs
Stubs have to be compiled into an object file (using gcc for instance) and then their mi-
crocode is generated using cfgrecovery.
B.1 __libc_start_main
jmp *4(%esp)
B.2 __printf
mov $0x0, %eax
ret
B.3 __read
mov 12(%esp), %eax
mov %eax, %ecx
mov 8(%esp), %ebx
label: movb $0x33, (%ebx)
inc %ebx
dec %ecx
jnz label
ret
For this stub, the constant 0x33 (51) has to be replaced by a random expression into the
generated XML file. To this aim you can use the following sed commands:
sed -e ’s+<const size="8" offset="0">51</const>+<random size="8" \
offset="0"></random>+g’ stub_read.[Link]
Appendix C: Script for automatic password recovery 32
# useful hooks
def init_registers ():
valregs = {
’esp’ : 0xFFFFFFF0,
’df’ : 0 # mandatory for string operations
}
for r in P().info()[’registers’]:
if r in valregs:
val = valregs[r]
set(r, val)
# filter functions
import re
# setting hooks
add_hook (run, init_registers)
for f in [cont, run, step]: add_hook(f, view_asm)
step()
# let the SMT solver gives us valid input character
for i in range(8): set(0x8048d45+i)
# display the password
dump(0x8048d45, l = 8, filter = filter_abstract_byte)