12558网页游戏私服论坛

 找回密码
 立即注册
游戏开服表 申请开服
游戏名称 游戏描述 开服状态 游戏福利 运营商 游戏链接
攻城掠地-仿官 全新玩法,觉醒武将,觉醒技能 每周新区 经典复古版本,长久稳定 进入游戏
巅峰新版攻 攻城掠地公益服 攻城掠地SF 新兵种、新武将(兵种) 进入游戏
攻城掠地公 散人玩家的天堂 新开 进入游戏
改版攻城掠 上线即可国战PK 稳定新区 全新改版,功能强大 进入游戏
少年江山 高福利高爆率 刚开一秒 江湖水落潜蛟龙 进入游戏
太古封魔录 开服送10亿钻石 福利多多 不用充钱也可升级 进入游戏
神魔之道 签到送元宝 稳定开新区 送豪华签到奖励 进入游戏
神奇三国 统帅三军,招揽名将 免费玩新区 激情国战,征战四方 进入游戏
龙符 三日豪礼领到爽 天天开新区 助你征战无双 进入游戏
王者之师 免费领豪华奖励 免费玩新区 6元送6888元宝 进入游戏
查看: 419|回复: 0

task_t指针重大风险预报-修复建议篇

[复制链接]

282

主题

282

帖子

574

积分

实习版主

Rank: 7Rank: 7Rank: 7

积分
574
发表于 2019-8-20 11:24:22 | 显示全部楼层 |阅读模式
引言:大家都知道知名意大利天才少年Luca放出来的针对Head.msgh_request_port);  // (1)
RetCode = task_threads(
             target_task,
             (thread_act_array_t  *)&(OutP->act_list.address),
             &OutP->act_listCnt);
task_deallocate(target_task);
我们可以看到任务端口被转换成了任务结构体指针,它接下来被存储在局部变量 target_task中,这个局部变量的生存周期是这个函数调用的生存周期。
下面是来自task_threads的相关代码:
task_threads(
   task_t task,
   thread_act_array_t  *threads_out,
   mach_msg_type_number_t  *count)
{
   ...
   for (thread =  (thread_t)queue_first(&task->threads);
        i  < actual;
        ++i,  thread = (thread_t)queue_next(&thread->task_threads)) {
     thread_reference_internal(thread);
     thread_list[j++]  = thread;
   }
   ...
     for (i = 0; i  < actual; ++i)
       ((ipc_port_t  *) thread_list)<i> = convert_thread_to_port(thread_list<i>); // (2)
     }
   ...
}
这段代码在线程列表中不断循环迭代地收集struct thread指针,然后把那些结构体线程转化为线程端口,并返回。代码中有少量的锁,但是它们是不相关的。
如果任务同时正在执行一个带有suid标志的程序,会发生什么?
相关的exec代码部分有两点,在ipc_task_reset和ipc_thread_reset中:
void
ipc_task_reset(
   task_t     task)
{
   ipc_port_t old_kport,  new_kport;
   ipc_port_t old_sself;
   ipc_port_t  old_exc_actions[EXC_TYPES_COUNT];
   int i;
   new_kport =  ipc_port_alloc_kernel();
   if (new_kport == IP_NULL)
     panic(&quot;ipc_task_reset&quot;);
   itk_lock(task);
   old_kport =  task->itk_self;
   if (old_kport == IP_NULL) {
     itk_unlock(task);
     ipc_port_dealloc_kernel(new_kport);
     return;
   }
   task->itk_self =  new_kport;
   old_sself =  task->itk_sself;
   task->itk_sself =  ipc_port_make_send(new_kport);
   ipc_kobject_set(old_kport,  IKO_NULL, IKOT_NONE); // (3)
紧跟着的是对ipc_thread_reset的调用:
ipc_thread_reset(
   thread_t  thread)
{
   ipc_port_t old_kport,  new_kport;
   ipc_port_t old_sself;
   ipc_port_t  old_exc_actions[EXC_TYPES_COUNT];
   boolean_t   has_old_exc_actions = FALSE;
   int       i;
   new_kport =  ipc_port_alloc_kernel();
   if (new_kport == IP_NULL)
     panic(&quot;ipc_task_reset&quot;);
   thread_mtx_lock(thread);
   old_kport =  thread->ith_self;
   if (old_kport == IP_NULL) {
     thread_mtx_unlock(thread);
     ipc_port_dealloc_kernel(new_kport);
     return;
   }
   thread->ith_self =  new_kport; // (4)
我们把执行exec的进程命名为B,调用task_threads()的进程命名为A,想象下面的交叉执行过程:
A:
target_task = convert_port_to_task(
   In0P->Head.msgh_request_port);  // (1)
A从栈上获得了指向进程B的任务结构体的指针。
B:
ipc_kobject_set(old_kport, IKO_NULL,  IKOT_NONE); // (3)
B执行了带有suid权限的程序,并且使旧任务端口无效,因此它不再拥有任务结构体指针。
B:
thread->ith_self = new_kport; // (4)
B分配了新的线程端口并启动
A:
((ipc_port_t *) thread_list)<i> =  convert_thread_to_port(thread_list<i>); // (2)
A为B的特权线程读入并转换新的线程端口对象,这给了A一个特权线程端口。
一个线程端口的发送权限会给你完整的寄存器控制权限。这个exploit和之前的两个执行模式有些类似,不同的是一旦它获得线程端口,它可以直接把RIP指向我们的gadget地址,而不必覆盖一个函数指针。竞态窗口非常小,所以需要一个很特别的交叉执行才可以,但是这是可以实现的。查看exploit(链接:https://bugs.chromium.org/p/project-zero/issues/attachment?aid=237182)和最初的bug报告(链接:837 - task_t considered harmful - many XNU EoPs - project-zero - Monorail)。
第二轮缓解策略
iOS 10/MacOS 10.12 引入了另外的缓解策略,同样可以绕过。
首先,在IOKit方面,userclient的生命周期现在直接与创建的任务绑定。其次,ipc_kobject服务有一处缓解措施来检测如果MIG内核方法因为竞态导致了execve调用,就强制使这个方法执行失败:
/*
* Check if the port is a task port, if its a  task port then
* snapshot the task exec token before the mig  routine call.
*/
ipc_port_t port =  request->ikm_header->msgh_remote_port;
if (IP_VALID(port) && ip_kotype(port)  == IKOT_TASK) {
task =  convert_port_to_task_with_exec_token(port, &exec_token);
}
(*ptr->routine)(request->ikm_header,  reply->ikm_header);
/* Check if the exec token changed during the  mig routine */
if (task != TASK_NULL) {
if (exec_token != task->exec_token) {
   exec_token_changed = TRUE;
}
task_deallocate(task);
}
缓解策略中有三处缺陷:
1. 它仅仅审查了第一个参数,但是有的内核MIG方法会在其他位置接受一个任务端口。
2. 它仅仅检查任务端口,然而thread_ports也受到了相似的影响。
3. 它仅仅缓解了那些我们需要获得MIG调用返回资源(比如端口)的bug。但是还有大量的其他方法是直接修改进程状态,而非返回新端口。
绕过第二轮缓解策略
虽然我们不再能够直接通过task_threads获得新的线程端口,还是有一些绕弯子的途径来达到目的。我们仅仅需要一个能够修改状态,而不是直接返回一些有用的东西(比如任务端口)的API。
task_set_exception_port允许我们为一个任务设置一个异常端口。当异常抛出时(比如非法访问内存)内核会发送一个异常消息给注册过的异常处理例程。对于我们很重要的是,这个异常消息包含了任务以及造成异常的线程的线程端口。
与内核中绝大多数地方带有一个task_t在栈上一样,这个API也存在有漏洞的竞态条件。在进程A中我们持续调用task_set_exception_ports()来传递进程B的任务端口,同时B execve执行一个带有suid权限的程序:
mig_internal novalue _Xtask_set_exception_ports(
mach_msg_header_t *InHeadP,
mach_msg_header_t *OutHeadP) {
...
task =  convert_port_to_task(In0P->Head.msgh_request_port); // (1)
OutP->RetCode =
   task_set_exception_ports(task,
                            In0P->exception_mask,
                            In0P->new_port.name,
                            In0P->behavior,
                            In0P->new_flavor);
task_deallocate(task);
...
kern_return_t
task_set_exception_ports(
task_t                 task,
exception_mask_t       exception_mask,
ipc_port_t             new_port,
exception_behavior_t  new_behavior,
thread_state_flavor_t new_flavor)
{
...
itk_lock(task); // (2)
for (i = FIRST_EXCEPTION; i <  EXC_TYPES_COUNT; ++i) {
   if ((exception_mask & (1  exc_actions<i>.port;
     task->exc_actions<i>.port  = ipc_port_copy_send(new_port); // (3)
     task->exc_actions<i>.behavior  = new_behavior;
     task->exc_actions<i>.flavor  = new_flavor;
     task->exc_actions<i>.privileged  = privileged;
   }
...
itk_unlock(task);
...
进程B调用execve来执行一个特权suid程序:
ipc_task_reset(
task_t  task)
{
...
itk_lock(task); // (4)
...
ip_lock(old_kport);
ipc_kobject_set_atomically(old_kport,  IKO_NULL, IKOT_NONE); // (5)
task->exec_token += 1;
ip_unlock(old_kport);
ipc_kobject_set(new_kport,  (ipc_kobject_t) task, IKOT_TASK);
for (i = FIRST_EXCEPTION; i <  EXC_TYPES_COUNT; i++) {
...
   if  (!task->exc_actions<i>.privileged) {
     old_exc_actions<i>  = task->exc_actions<i>.port;
     task->exc_actions<i>.port  = IP_NULL; // (6)
   }
}
itk_unlock(task); //(7)
我们寻找下面这样的交叉执行情景:
A:
task =  convert_port_to_task(In0P->Head.msgh_request_port); // (1)
B:
itk_lock(task); // (4)
ipc_kobject_set_atomically(old_kport,  IKO_NULL, IKOT_NONE); // (5)
task->exc_actions<i>.port = IP_NULL; // (6)
itk_unlock(task); //(7)
A:
itk_lock(task); // (2)
task->exc_actions<i>.port =  ipc_port_copy_send(new_port); // (3)
我们很容易在竞态条件取得对task_threads的先机,因为锁保证了所有对我们有利的事情。我们要做的仅仅是循环调用task_set_exception_ports并且希望(4)处的B接过任务锁之前,(1)能够被A调用。实践中Exp在几微秒内就可以赢得竞态条件。
最后的工作实际上是确保当赢得竞态条件时,我们强制子进程引发一个异常,把它的任务和线程端口发给我们。我们可以通过在执行suid目标前带一个非常小的值调用setrlimit(RLIMIT_STACK)
来实现。这意味着我们将要执行的二进制程序的栈空间很小,很快就会导致段错误。
在父进程中,一旦task_set_exception_port调用失败,我们就尝试从异常端口接收消息,设置一个短的timeout。如果接收到消息,我们在竞态中取得先机,这个消息中包含euid为0的进程的任务和线程端口。这种情况下,Exp在任务中分配了一些RWX内存,并把一个shellcode拷贝到这个地方。shellcode做的事情如下:
struct rlimit lim = {0x1000000,  0x1000000};
setrlimit(RLIMIT_STACK, lim);
setuid(0);
char* argv[2] = {&quot;/bin/bash&quot;,  0};
execve(&quot;/bin/bash&quot;, argv, 0);
shellcode把栈长度设置回一个很大的值,用setuid(0)来避免bash丢失权限,最后打开一个shell。
这个Exp应该会在MacOS/OS X 版本
楼主热帖
回复

使用道具 举报

*滑块验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|12558网页游戏私服论坛 |网站地图

GMT+8, 2024-5-14 15:28 , Processed in 0.125000 second(s), 30 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表