Exploiting a simple binary!
Catering my interest in offensive security and reverse engineering, I stumbled upon an excellent video by security YouTuber LiveOverflow. I decided to try my hand at exploiting the binary he covers in this video (the licence_2 program), without following his guidance in order to build my understanding of reverse engineering and actually getting to solve a problem myself. We’re only going to be looking at the executable here, so all the other files can be ignored.
STEP 0: Taking a first glance at the executable
When there are no arguments, the executable prints it’s usage message.
When a single argument is passed, the executable seems to run code that checks wether or not the string is valid (big deja vu here, it’s exactly the same as the previous exercise), and outputs “WRONG!” since we haven’t produced a valid key. I assume there is a success message and that we won’t see what it is untill we get a valid key.
When there are two or more arguments, it also prints it’s usage message.
It’s safe to assume it doesn’t do anything crazy in regards to argument handling: it checks the argument count, if it’s different from 2 it prints the usage.
STEP 1: A cheeky peek under the hood
I started by running the executable through GDB. To no one’s surprise, there are no debug symbols in there. let’s dissasemble the main to see what was going on in there!
0x00000000004005bd <+0>: push rbp
0x00000000004005be <+1>: mov rbp,rsp
0x00000000004005c1 <+4>: push rbx
0x00000000004005c2 <+5>: sub rsp,0x28
0x00000000004005c6 <+9>: mov DWORD PTR [rbp-0x24],edi
0x00000000004005c9 <+12>: mov QWORD PTR [rbp-0x30],rsi
0x00000000004005cd <+16>: cmp DWORD PTR [rbp-0x24],0x2
0x00000000004005d1 <+20>: jne 0x400663 <main+166>
0x00000000004005d7 <+26>: mov rax,QWORD PTR [rbp-0x30]
0x00000000004005db <+30>: add rax,0x8
0x00000000004005df <+34>: mov rax,QWORD PTR [rax]
0x00000000004005e2 <+37>: mov rsi,rax
0x00000000004005e5 <+40>: mov edi,0x400704
0x00000000004005ea <+45>: mov eax,0x0
0x00000000004005ef <+50>: call 0x4004a0 <printf@plt>
0x00000000004005f4 <+55>: mov DWORD PTR [rbp-0x18],0x0
0x00000000004005fb <+62>: mov DWORD PTR [rbp-0x14],0x0
0x0000000000400602 <+69>: jmp 0x400624 <main+103>
0x0000000000400604 <+71>: mov rax,QWORD PTR [rbp-0x30]
0x0000000000400608 <+75>: add rax,0x8
0x000000000040060c <+79>: mov rdx,QWORD PTR [rax]
0x000000000040060f <+82>: mov eax,DWORD PTR [rbp-0x14]
0x0000000000400612 <+85>: cdqe
0x0000000000400614 <+87>: add rax,rdx
0x0000000000400617 <+90>: movzx eax,BYTE PTR [rax]
0x000000000040061a <+93>: movsx eax,al
0x000000000040061d <+96>: add DWORD PTR [rbp-0x18],eax
0x0000000000400620 <+99>: add DWORD PTR [rbp-0x14],0x1
0x0000000000400624 <+103>: mov eax,DWORD PTR [rbp-0x14]
0x0000000000400627 <+106>: movsxd rbx,eax
0x000000000040062a <+109>: mov rax,QWORD PTR [rbp-0x30]
0x000000000040062e <+113>: add rax,0x8
0x0000000000400632 <+117>: mov rax,QWORD PTR [rax]
0x0000000000400635 <+120>: mov rdi,rax
0x0000000000400638 <+123>: call 0x400490 <strlen@plt>
0x000000000040063d <+128>: cmp rbx,rax
0x0000000000400640 <+131>: jb 0x400604 <main+71>
0x0000000000400642 <+133>: cmp DWORD PTR [rbp-0x18],0x394
0x0000000000400649 <+140>: jne 0x400657 <main+154>
0x000000000040064b <+142>: mov edi,0x40071a
0x0000000000400650 <+147>: call 0x400480 <puts@plt>
0x0000000000400655 <+152>: jmp 0x40066d <main+176>
0x0000000000400657 <+154>: mov edi,0x40072a
0x000000000040065c <+159>: call 0x400480 <puts@plt>
0x0000000000400661 <+164>: jmp 0x40066d <main+176>
0x0000000000400663 <+166>: mov edi,0x400731
0x0000000000400668 <+171>: call 0x400480 <puts@plt>
0x000000000040066d <+176>: mov eax,0x0
0x0000000000400672 <+181>: add rsp,0x28
0x0000000000400676 <+185>: pop rbx
0x0000000000400677 <+186>: pop rbp
0x0000000000400678 <+187>: ret
We can see that the usual prologue from <+0> to <+9> (establishing execution frame, getting the arguments) is directly followed our first comparison at <+16> (after loading ac and av into the relevant registers) which triggers a jump to <+166> (where data is loaded at an address and then puts is called so it’s most definetly a string) if ac is indeed different from 2. This confirms our earlier assumption that there is nothing crazy going on with the arguments, the program only checks wether the count is equals to two or not before either printing the usage or executing the key check.
from <+26> to <+45>, we can see what looks like an initialization phase so we’ll ignore that.
<+50> is a call to printf, the one notifying us that we’re about to check wether the key is valid or not. <+55> and <+62> initialize some variables to 0, <+69> unconditionally jumps to <+103> and sets up more stuff by moving data around registers untill <+113> where we do some arithmetic, then more register moves untill we reach <+123> where we can see a call to strlen. Function calls like this are usually done for a reason, so we’ll keep that the length of a string (most likely the one we passed as an argument) is relevant here.
We compare that length with the value held at register RAX, (which I think may be a pointer or an index to a string). this is then followed by a jb (jump if below), which means that this part of the code (<+123> … <+131>) checks wether we have reached the end index of a string and if not, we jump back to <+71>, where it seems like an arithmetic loop of sorts is happening untill we’re back at that strlen comparison.
Looking closely there, we see that base pointer offsetted data is being manipulated, and I have a strong suspision that [rbp-0x18] contains the arithmetic loop result as it’s getting it’s value increased by the value of EAX (itself containing the result of an addition) at every iteration of this loop.
once that <+131> jb condition is not met anymore, we get to <+133> which compares our [rbp-0x18] value with the hexadecimal value 0x394. What is 0x394 you ask? I have no clue, but if the result of that arithmetic loop stored at [rbp-0x18] does not match it, we jump to <+154> which loads data at address 0x40071a into the edi register and then call puts, so that loaded data is most definetly a string.
Single stepping through this code, I see that the outputted string is “WRONG!”… Looks like we found it bois, we found the condition that tells us wether a key is valid or not, and it’s all happening at the CMP at <+133>.
dopesauce.
STEP 2: PWNED
reading the spec, I saw that the CMP instruction sets the eflags accordingly, and that’s what the JMP family of instructions look at to decide wether to jump or not. It’s a real shame we don’t have a tool for changing the value of regist- oh, what’s that? GDB has us covered? How unsurprising.
Using GDB’s set
command, we can alter the values of registers at runtime.
So, putting a breakpoint at 0x0000000000400649 <+140>
, turning on the zero flag in the eflags with
set $eflags |= (1 << 6)
and continuing execution, we get the cookie — an “Access granted!” message.
And there you have it: this trivial little binary now has a consistent, reproduceable exploit!
Don’t believe it? try it for yourself!
run the following script through gdb like this: gdb -i=mi ./license_2 --command=exploit.gdb
The exploit.gdb script
set disassembly-flavor intel
b *0x0000000000400649
r lol
set $eflags |= (1 << 6)
c