The second exercise on pwnable.kr is the col one. It seems to suggest we can generate a hash without knowing the original phrase. But let us see what is on the server provided for us.
col@pwnable:~$ ls -ls
total 16
8 -r-sr-x--- 1 col_pwn col 7341 Jun 11 2014 col
4 -rw-r--r-- 1 root root 555 Jun 12 2014 col.c
4 -r--r----- 1 col_pwn col_pwn 52 Jun 11 2014 flag
Again we have an executable with the S flag, a flag file and the source code. Now I am not going to look in the source code yet, I want to disassemble col first without any knowledge.
We copy the col file locally and open it up with Ghidra. We navigate to the main function and do a quick diagonal look over it. Here it seems that the main function will call a function named check_password which seems to return a hashcode. The next line will compare the hashcode to one saved in the program.
Out of the strings defined in the program we can see that the program expects one argument and that argument should be 20 chars long. Of course this could be a lie.
We skip the function prologue and notice the next opcodes
MOV EAX,dword ptr [EBP + param_2]
080484da MOV dword ptr [ESP + local_84],EAX
080484dd 080484e1 MOV EAX,GS:[0x14]
080484e7 MOV dword ptr [ESP + local_14],EAX
XOR EAX,EAX 080484ee
Now, what is this weird GS:[0x14]
and what is it doing? If we look through the code we will notice nothing is accessing this local_14 until the very end.
...
08048592 MOV EDX,dword ptr [ESP + local_14]
08048599 XOR EDX,dword ptr GS:[0x14]
JZ LAB_080485a7
080485a0 CALL __stack_chk_fail 080485a2
So just before the function epilogue we suddenly do these instructions.
GS:[0x14]
from the start (we saved it into local_14) into EDX.GS:[0x14]
GS:[0x14]
it will call the __stack_chk_fail function. So the purposes of this was to check if someone tried to be naughty and had caused a stack overflow! (The overflow has happened, this just tells the programmer that it happened.)This is something we can ignore now but do know that a lot of programs have these stack checks. These are put in place to try and prevent an attacker from changing for example the return address or enter other malicious code.
Back to the top after our stack_check
CMP dword ptr [EBP + param_1],0x1
080484f0 JG LAB_08048514
080484f4 MOV EAX,dword ptr [ESP + local_84]
080484f6 MOV EDX,dword ptr [EAX]
080484fa MOV EAX,s_usage_:_%s_[passcode]_08048680 = "usage : %s [passcode]\n"
080484fc 08048501 MOV dword ptr [ESP + local_9c],EDX
08048505 MOV dword ptr [ESP]=>local_a0,EAX=>s_usage_:_%s_[p = "usage : %s [passcode]\n"
08048508 CALL printf
MOV EAX,0x0
0804850d 08048512 JMP LAB_08048592
Just like previous function, this is a simple if structure to check if there is at least one parameter passed to the function. If not it will pass a string with helpful text into the stack and call printf, after that we set EAX to zero and go the the function epilogue (with the stack-check we described above).
08048514 MOV EAX,dword ptr [ESP + local_84]
08048518 ADD EAX,0x4
MOV EAX,dword ptr [EAX]
0804851b MOV dword ptr [ESP + local_88],0xffffffff
0804851d 08048525 MOV EDX,EAX
08048527 MOV EAX,0x0
MOV ECX,dword ptr [ESP + local_88]
0804852c 08048530 MOV EDI,EDX
08048532 SCASB.REPNE ES:EDI
08048534 MOV EAX,ECX
08048536 NOT EAX
08048538 SUB EAX,0x1
CMP EAX,0x14
0804853b JZ LAB_08048553
0804853e 08048540 MOV dword ptr [ESP]=>local_a0,s_passcode_length_sh = "passcode length should be 20
08048547 CALL puts
MOV EAX,0x0
0804854c 08048551 JMP LAB_08048592
The next block is a bit bigger but I copied until again LAB_08048592
which we now have seen many times as the “if this code is executed we will jump to the function epilogue”. So this code probably does another check. The pre-defined string in this section shows us that this will probably check if our input is of length 20.
local_84
into EAX (in the first code block we saw that the char array pointer was saved into local_84
)0x4
to have the pointer to the second item in argv (which is the first argument as the first element of argv is the program name)argv[1]
) into EAX0xffffffff
into local_88
argv[1]
) into EDXlocal_88
into ECX ( which was 0xffffffff
)argv[1]
) into EDICMP 0x14
(20 decimal) it seems this is a strlen function. But let us continue to see how it works. REPNE is Repeat while not equal so it will continue going further in the string until 0x0 is found (in combination with SCASB)0xffffffff
and went -1 every iteration of REPNE so if our string was 20 chars long it would be 0xffffffea
0xffffffea
to 0x00000015
(which is… decimal 21)0x1
is minus 1, but why does it do this? Simple, the SCASB went 21 times for it to find 0x0
so we still need to reduce it with one if we don’t want to count the 0x0
if(strlen(argv[1]) == 20)
argv[1]
was not equal to strlen = 20 then an error message would be shown and a JMP to the function epilogue would happen. But now we skip over this fatal jump thanks to our JZ into the next code!08048553 MOV EAX,dword ptr [ESP + local_84]
08048557 ADD EAX,0x4
MOV EAX,dword ptr [EAX]
0804855a MOV dword ptr [ESP]=>local_a0,EAX
0804855c CALL check_password 0804855f
When we have passed the check if our argv[1]
is a string of length 20 we come here. Now here is something I made a mistake first and only after a closer look got it right. The pointer to the char array is added to EAX, we add 0x4
to it so we get the pointer to the second item of this array and that ADDRESS is passed to check_password
, not the value! argv is actually a char[][]
and I was treating it as a char[]
08048494 PUSH EBP
08048495 MOV EBP,ESP
08048497 SUB ESP,0x10
MOV EAX,dword ptr [EBP + param_1]
0804849a MOV dword ptr [EBP + local_8],EAX
0804849d MOV dword ptr [EBP + local_c],0x0
080484a0 MOV dword ptr [EBP + local_10],0x0
080484a7 JMP LAB_080484c2
080484ae
LAB_080484b0 MOV EAX,dword ptr [EBP + local_10]
080484b0 SHL EAX,0x2
080484b3 ADD EAX,dword ptr [EBP + local_8]
080484b6 MOV EAX,dword ptr [EAX]
080484b9 ADD dword ptr [EBP + local_c],EAX
080484bb ADD dword ptr [EBP + local_10],0x1
080484be
LAB_080484c2 CMP dword ptr [EBP + local_10],0x4
080484c2 JLE LAB_080484b0
080484c6 MOV EAX,dword ptr [EBP + local_c]
080484c8 LEAVE
080484cb RET 080484cc
Ok, I’ve just copied the whole check_password
function as it doesn’t seem that big. We have again first the function prologue and we reserve a stack of only 10 bytes this time for this function stackframe (SUB ESP, 0x10
)
char* local_8 = param1
0
local_c, local_10 = while(local_10 <= 4) {
4) // or maybe local_8[local_10]
local_c += *(local_8 + local_10* }
So now we return to our main function were immediately after we see
08048564 MOV EDX,dword ptr [hashcode] = 21DD09ECh
CMP EAX,EDX
0804856a JNZ LAB_08048581 0804856c
This puts a pre-defined hashcode into EDX, does a CMP with the return value of check_password and if they are the same will execute the /bin/cat flag
as our previous post explained. So the only thing to do now is find the string we must input were the first 5 values summed up are equal to a hashcode of 0x21DD09EC
Because the value ends in 0xC
we can already know that this value isn’t divisble by 5 without a remainder so our passcode will be 4 times the same dword char and a fifth
First four dwords: 0x21DD09EC / 5 = 0x06C5CEC8
Fifth dword: 0x21DD09EC – 0x06C5CEC8 * 4 = 0x06C5CECC
Our passcode is 0x06C5CEC8 * 4 + 0x06C5CECC
Now I must confess, with the next part I was stuck myself and started looking up on the internet how to do it. Apparently the easiest way to pass hex values to a program is to use python. Another thing they said was that you must pass them as little endians. This is something easily missed especially as a beginner myself. The best way to see this is also by doing a live analyse. When stuck I’ll do that next time!
./col "$(python -c "print '\xc8\xce\xc5\x06'*4+'\xcc\xce\xc5\x06'")" $
Now why is this exercise called collissions? Because the hashcode has multiple answers. We could also do
First three dwords: 0x06C5CEC8
Last two dwords: (21DD09EC – 0x06C5CEC8 * 3) / 2 = 0x06C5CECA
Our passcode 0x06C5CEC8 * 3 + 0x06C5CECA * 2
./col "$(python -c "print '\xc8\xce\xc5\x06'*3+'\xca\xce\xc5\x06'*2")" $
Multiple values can give the correct password.
< Home