< Home

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

080484da                 MOV        EAX,dword ptr [EBP + param_2]
080484dd                 MOV        dword ptr [ESP + local_84],EAX
080484e1                 MOV        EAX,GS:[0x14]
080484e7                 MOV        dword ptr [ESP + local_14],EAX
080484ee                 XOR        EAX,EAX
  1. It saves the param_2 (argv which is the pointer to an array) into EAX and afterwards in local_84
  2. We move a value from a general segment with offset 0x14 to EAX and store it in local_14
  3. We set EAX back to zero (XORing a registry with itself will set all bits to zero)

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]
080485a0                 JZ         LAB_080485a7
080485a2                 CALL       __stack_chk_fail      

So just before the function epilogue we suddenly do these instructions.

  1. We move the saved GS:[0x14] from the start (we saved it into local_14) into EDX.
  2. We XOR it with the value present in GS:[0x14]
  3. Now a Jump if Zero will skip the next instruction and continue to the function epilogue but if the saved value is different from the value still in 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

080484f0                 CMP        dword ptr [EBP + param_1],0x1
080484f4                 JG         LAB_08048514
080484f6                 MOV        EAX,dword ptr [ESP + local_84]
080484fa                 MOV        EDX,dword ptr [EAX]
080484fc                 MOV        EAX,s_usage_:_%s_[passcode]_08048680             = "usage : %s [passcode]\n"
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                                           
0804850d                 MOV        EAX,0x0
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
0804851b                 MOV        EAX,dword ptr [EAX]
0804851d                 MOV        dword ptr [ESP + local_88],0xffffffff
08048525                 MOV        EDX,EAX
08048527                 MOV        EAX,0x0
0804852c                 MOV        ECX,dword ptr [ESP + local_88]
08048530                 MOV        EDI,EDX
08048532                 SCASB.REPNE ES:EDI
08048534                 MOV        EAX,ECX
08048536                 NOT        EAX
08048538                 SUB        EAX,0x1
0804853b                 CMP        EAX,0x14
0804853e                 JZ         LAB_08048553
08048540                 MOV        dword ptr [ESP]=>local_a0,s_passcode_length_sh   = "passcode length should be 20 
08048547                 CALL       puts                                            
0804854c                 MOV        EAX,0x0
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.

  1. We move the saved value of local_84 into EAX (in the first code block we saw that the char array pointer was saved into local_84)
  2. We add 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)
  3. We move the value pointed to by the address in EAX (argv[1]) into EAX
  4. We move the value of 0xffffffff into local_88
  5. We move the value of EAX ( argv[1] ) into EDX
  6. We set EAX to 0x0
  7. We move the value of local_88 into ECX ( which was 0xffffffff )
  8. We move the value of EDX ( argv[1] ) into EDI
  9. SCASB.REPNE which will scan a string and set flags accordingly to what it finds. The value it tries to find is found in AL (EAX Low byte). As EAX is 0x0 (see step 6) this scans for the value of zero. When I see in a couple of lines further a CMP 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)
  10. The result of ECX is moved to EAX (so remember, ECX was 0xffffffff and went -1 every iteration of REPNE so if our string was 20 chars long it would be 0xffffffea
  11. Suddenly a NOT operator on EAX which would make 0xffffffea to 0x00000015 (which is… decimal 21)
  12. SUB 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
  13. And then we have a CMP and JZ to complete our if(strlen(argv[1]) == 20)
  14. If 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
0804855a                 MOV        EAX,dword ptr [EAX]
0804855c                 MOV        dword ptr [ESP]=>local_a0,EAX
0804855f                 CALL       check_password                                  

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
0804849a                 MOV        EAX,dword ptr [EBP + param_1]
0804849d                 MOV        dword ptr [EBP + local_8],EAX
080484a0                 MOV        dword ptr [EBP + local_c],0x0
080484a7                 MOV        dword ptr [EBP + local_10],0x0
080484ae                 JMP        LAB_080484c2
080484b0                 MOV        EAX,dword ptr [EBP + local_10]
080484b3                 SHL        EAX,0x2
080484b6                 ADD        EAX,dword ptr [EBP + local_8]
080484b9                 MOV        EAX,dword ptr [EAX]
080484bb                 ADD        dword ptr [EBP + local_c],EAX
080484be                 ADD        dword ptr [EBP + local_10],0x1
080484c2                 CMP        dword ptr [EBP + local_10],0x4
080484c6                 JLE        LAB_080484b0
080484c8                 MOV        EAX,dword ptr [EBP + local_c]
080484cb                 LEAVE
080484cc                 RET

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)

  1. We storage the pointer to the char[] we’ve received from the user
  2. Then we move the pointer to local_8
  3. We move the value of 0x0 into local_c and in local_10
  4. Suddenly a jmp to a CMP of local_10 against 0x4, have we detected a while loop?
  5. Jump Less or EQUAL so we go back up to LAB_080484b0
  6. We move local_10 inside EAX
  7. We shl EAX with 0x2, can be seen as EAX times 4
  8. We add local_8 to it, reminder this is a pointer address so we have a ptr address and we add a value to it that is always x * 4. It seems we are accessing an array!
  9. Now we move the value EAX is pointing to into EAX
  10. We add that value to local_c
  11. We increase local_10 with one
char* local_8 = param1
local_c, local_10 = 0
while(local_10 <= 4) {
    local_c += *(local_8 + local_10*4) // or maybe local_8[local_10]

So now we return to our main function were immediately after we see

08048564                 MOV        EDX,dword ptr [hashcode]  = 21DD09ECh
0804856a                 CMP        EAX,EDX
0804856c                 JNZ        LAB_08048581

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