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
Written on March 10, 2021