In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-01-24 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Network Security >
Share
Shulou(Shulou.com)05/31 Report--
Today, I would like to talk to you about how to conduct Ubuntu kernel eBPF 0day analysis, many people may not know much about it. In order to make you understand better, the editor has summarized the following contents for you. I hope you can get something according to this article.
0x01 background
Chinese Wushu is extensive and profound, among which Taiji is highly respected as a kung fu that does not beat people with clumsy strength. Similarly, if you peep at the field of loopholes from the perspective of attack, it is not difficult to see that there is no lack of "tai chi" in the game between attack and defense, and lightweight, stable and easy-to-use loopholes and tools are often more attractive to hackers. Today, the author wants to analyze such a 0day loophole that is good at "four or two strokes".
You must have heard of the attack power of 0day vulnerabilities. Kernel 0day is favored by attackers because of its wide range of influence and long repair cycle. Recently, Vitaly Nikolenko, a foreign security researcher, published a kernel 0day of Ubuntu 16.04 on twitter [1]. Attackers can directly use this code to get the highest authority (root) of Ubuntu without a threshold. Although it only affects specific versions, since Ubuntu has a large number of users around the world, especially public cloud users, this vulnerability still poses a lot of risks to enterprises and individual users.
The author makes a technical analysis of the vulnerability, no matter from the cause of the vulnerability or the utilization technology, it is quite representative, and it is a typical application of Data-Oriented Attacks in the linux kernel. Only using the incoming carefully constructed data can control the program flow, achieve the purpose of attack, completely bypass some existing memory protection measures, and have the effect of "two or four digging thousands of pounds".
Reasons for 0x02 vulnerabilities
This vulnerability lies in the eBPF module of the Linux kernel. Let's take a brief look at eBPF.
EBPF (extended Berkeley Packet Filter) is a set of packet filtering mechanism derived from BPF. Strictly speaking, the function of eBPF is not limited to network packet filtering, but it can be used to achieve kernel tracing,tracfic control, application performance monitoring and other powerful functions. In order to achieve such a powerful function, eBPF provides a set of RISC-like instructions and implements the virtual machine of the instruction set. Users submit instruction codes to eBPF through the kernel API to complete specific functions.
Seeing this, experienced security researchers may think that the ability to submit controllable instruction code to the kernel for execution is likely to cause security problems. As a matter of fact, there are a lot of loopholes in BPF in history. For more details on eBPF, please refer to [4] [5] here.
Of course, eBPF is designed with security in mind, and it implements a verifier mechanism in the kernel to filter non-compliant eBPF code. However, the loophole this time lies in eBPF's verifier mechanism.
From the screenshot of the patch originally released by Vitaly Nikolenko, we initially judge that this vulnerability is likely to be the same hole as CVE-2017-16995 [6], but then there are two questions:
1.CVE-2017-16995 kernels 4.9 and 4.14 and later versions were fixed in December last year, so why is the version 4.4 used by Ubuntu not fixed?
2.CVE-2017-16995 is an eBPF vulnerability discovered by Jann Horn of the Google Project Zero team, which exists in kernel versions 4.9 and 4.14 [7]. The author only gives a brief description of the cause of the vulnerability in the vulnerability report, is it exactly the same as this one?
Note: all of the author's code analysis and debugging are based on Ubuntu 14.04, and the kernel version is 4.4.0-31-generic # 50~14.04.1-Ubuntu [8].
Let's answer the second question first. the debugging and analysis process is not shown here.
Referring to the following code, the ALU directive is checked (check_alu_op) in eBPF's verifer code (kernel/bpf/verifier.c). The last else branch of this code checks instructions:
1.BPF_ALU64 | BPF_MOV | BPF_K, which assigns 64-bit immediate numbers to the destination register
2.BPF_ALU | BPF_MOV | BPF_K, which assigns a 32-bit immediate number to the destination register
However, there is no distinction between the two instructions, directly assigning the immediate number insn- > imm in the user instruction to the destination register, insn- > imm and the type of destination register is integer. What will be the impact of this operation?
Let's take a look at how the eBPF runtime code (kernel/bpf/core.c) interprets these two instructions (bpf_prog_run).
Referring to the following code, the above two ALU instructions correspond to ALU_MOV_K and ALU64_MOV_K, respectively. You can see that verifier and eBPF runtime codes have different semantic interpretations of the two instructions. DST is a 64bit register, so ALU_MOV_K gets a 32bit unsigned integer, while ALU64_MOV_K sign extension imm to get a signed 64bit integer. At this point, we probably know the cause of the vulnerability, and this logic is basically the same as that of CVE-2017-16995, although there are some differences in the code details (kernels 4.9 and 4.14 make significant adjustments to verifier). But what is the impact of semantic inconsistency here?
Let's take a look at the following code (check_cond_jmp_op) in vefier, which checks the BPF_JMP | BPF_JNE | BPF_IMM instruction. The semantics of this instruction is: if the immediate number of the destination register = = the immediate number of instructions (insn- > imm), the program continues to execute, otherwise the instruction at pc+off will be executed. Note that the condition for immediate equality is judged, because the previous ALU instruction makes no distinction between 32bit and 64bit integer, regardless of whether imm is signed or not. Take a look at the eBPF runtime interpretation of the BPF_JMP | BPF_JNE | BPF_IMM instruction (bpf_prog_run), obviously when imm is matched and unsigned, because the result of the sign extension is different. Note that this is a jump instruction, and the consequence of semantic inconsistency here is more intuitive, which is equivalent to the logic that we can control the jump instruction through the immediate number of ALU instructions. This imagination is relatively large, and it is also the basis for later vulnerability exploitation. for example, you can control eBPF programs to completely bypass the verifier mechanism and execute malicious code directly at run time.
It is worth mentioning that although the cause of this vulnerability is basically the same as that of CVE-2017-16995, the idea of controlling the jump instruction is not the same as the POC idea given by Jann Horn in CVE-2017-16995. Interested readers can analyze the POC in CVE-2017-16995, because of the defect of ALU sign extension, the operation of the pointer in eBPF is calculated incorrectly, thus bypassing the pointer check of verifier, and finally reading and writing arbitrary kernel memory. However, this method of utilization does not work in the 4. 4 kernel because the eBPF of the 4. 4 kernel does not allow ALU operations on pointer types.
At this point, let's go back to the first question: since the cause of the vulnerability is the same, why didn't the kernel of Ubuntu 4.4 fix the vulnerability? It is related to the development mode of Linux kernel.
Linux kernel is divided into three versions of mainline,stable,longterm [9], and general security problems will be fixed in mainline, but for longterm, only important security patches are selected for backport, so it may occur that a vulnerability is ignored or misjudged, resulting in the vulnerability still exists in the version of longterm. For example, in this version of 4.4longterm, Jann Horn did not mention the version below 4.9in the initial report.
There has been a long debate about Linux kernel's maintenance of the longterm version [10], and the mainstream opinion in the community is to advise users to use the latest version. However, for the sake of stability and development cost, each distribution (such as Ubuntu) generally chooses longterm as the base and maintains a set of kernel on its own.
For embedded systems, this problem is even more serious. The risk and cost of kernel upgrade caused by a large number of vendor code is much higher than that of backport security patches, so most embedded systems are still using the older version of longterm. For example, when Google Android was released by Pixel / Pixel XL 2 last year, the kernel version was upgraded from 3.18 to 4.4, perhaps because 3.18 has already entered EOL (End of Life), that is, the community is going to announce that 3.18 is dead, and there will be no subsequent backport security patches to 3.18, while the latest mainline version has reached 4.16. The author also found an unfixed historical vulnerability in Android kernel last year (reported to google and fixed), but upstream fixed it 2 years ago.
Vitaly Nikolenko, on the other hand, may be based on the CVE-2017-16995 report, which found a similar vulnerability in version 4.4 and found a more general way to exploit it (control jump instructions).
0x03 vulnerability exploitation
According to the analysis of the cause of the vulnerability in the previous section, after we take advantage of the vulnerability to bypass the eBPF verifier mechanism, we can execute any instructions supported by eBPF, of course, the most direct thing is to read and write arbitrary memory. The steps for exploitation are as follows:
1. Construct eBPF instruction, take advantage of ALU instruction defect, bypass eBPF verifier mechanism
two。 Construct eBPF instructions to read the kernel stack base address
3. According to the leaked SP address, continue to construct the eBPF instruction, read the task_struct address, and then get the task_struct- > cred address.
4. Construct the eBPF instruction, override cred- > uid, and cred- > gid is 0 to complete the lifting.
The core of vulnerability exploitation lies in the well-constructed malicious eBPF instruction, which is a 16-mechanism string (char * _ prog) in Vitaly Nikolenko exp, which is not intuitive. For convenience, the author wrote a gadget to restore these instructions to a more friendly form. Of course, we can also use the debugging mechanism of eBPF to print out the readable form of eBPF instructions in the kernel log. Let's take a look at this eBPF program, with a total of 41 instructions (the output of the gadget written by the author):
Parsing eBPF prog, size 328, len 41ins 0: code (b4) alu | = | imm, dst_reg 9, src_reg 0, off 0, imm ffffffffins 1: code (55) jmp |! = | imm, dst_reg 9, src_reg 0, off 2, imm ffffffffins 2: code (b7) alu64 | = | imm, dst_reg 0, src_reg 0, off 0, imm 0ins 3: code (95) jmp | exit | imm, dst_reg 0, src_reg 0, off 0, imm 0ins 4: code (18) ld | BPF_IMM | u64 Dst_reg 9, src_reg 1, off 0, imm 3ins 5: code (00) ld | BPF_IMM | U32, dst_reg 0, src_reg 0, off 0, imm 0ins 6: code (bf) alu64 | = | src_reg, dst_reg 1, src_reg 9, off 0, imm 0ins 7: code (bf) alu64 | = | src_reg, dst_reg 2, src_reg a, off 0, imm 0ins 8: code (07) alu64 | + = | imm, dst_reg 2, src_reg 0, off 0 Imm fffffffcins 9: code (62) st | BPF_MEM | U32, dst_reg a, src_reg 0, off fffffffc, imm 0ins 10: code (85) jmp | call | imm, dst_reg 0, src_reg 0, off 0, imm 1ins 11: code (55) jmp |! = | imm, dst_reg 0, src_reg 0, off 1, imm 0ins 12: code (95) jmp | exit | imm, dst_reg 0, src_reg 0, off 0, imm 0ins 13: code (79) ldx | BPF_MEM | u64, dst_reg 6 Src_reg 0, off 0, imm 0ins 14: code (bf) alu64 | = | src_reg, dst_reg 1, src_reg 9, off 0, imm 0ins 15: code (bf) alu64 | = | src_reg, dst_reg 2, src_reg a, off 0, imm 0ins 16: code (07) alu64 | + = | imm, dst_reg 2, src_reg 0, off 0, imm fffffffcins 17: code (62) st | BPF_MEM | U32, dst_reg a, src_reg 0, off fffffffc Imm 1ins 18: code (85) jmp | call | imm, dst_reg 0, src_reg 0, off 0, imm 1ins 19: code (55) jmp |! = | imm, dst_reg 0, src_reg 0, off 1, imm 0ins 20: code (95) jmp | exit | imm, dst_reg 0, src_reg 0, off 0, imm 0ins 21: code (79) ldx | BPF_MEM | u64, dst_reg 7, src_reg 0, off 0, imm 0ins 22: code (bf) alu64 | src_reg, dst_reg 1 Src_reg 9, off 0, imm 0ins 23: code (bf) alu64 | = | src_reg, dst_reg 2, src_reg a, off 0, imm 0ins 24: code (07) alu64 | + = | imm, dst_reg 2, src_reg 0, off 0, imm fffffffcins 25: code (62) st | BPF_MEM | U32, dst_reg a, src_reg 0, off fffffffc, imm 2ins 26: code (85) jmp | call | imm, dst_reg 0, src_reg 0, off 0 Imm 1ins 27: code (55) jmp |! = | imm, dst_reg 0, src_reg 0, off 1, imm 0ins 28: code (95) jmp | exit | imm, dst_reg 0, src_reg 0, off 0, imm 0ins 29: code (79) ldx | BPF_MEM | u64, dst_reg 8, src_reg 0, off 0, imm 0ins 30: code (bf) alu64 | = | src_reg, dst_reg 2, src_reg 0, off 0, imm 0ins 31: code (b7) alu64 | = | imm, dst_reg 0 Src_reg 0, off 0, imm 0ins 32: code (55) jmp |! = | imm, dst_reg 6, src_reg 0, off 3, imm 0ins 33: code (79) ldx | BPF_MEM | u64, dst_reg 3, src_reg 7, off 0, imm 0ins 34: code (7b) stx | BPF_MEM | U64, dst_reg 2, src_reg 3, off 0, imm 0ins 35: code (95) jmp | exit | imm, dst_reg 0, src_reg 0, off 0 Imm 0ins 36: code (55) jmp |! = | imm, dst_reg 6, src_reg 0, off 2, imm 1ins 37: code (7b) stx | BPF_MEM | u64, dst_reg 2, src_reg a, off 0, imm 0ins 38: code (95) jmp | exit | imm, dst_reg 0, src_reg 0, off 0, imm 0ins 39: code (7b) stx | BPF_MEM | u64, dst_reg 7, src_reg 8, off 0, imm 0ins 40: code (95) jmp | exit | imm, dst_reg 0 Src_reg 0, off 0, imm 0parsed 41 ins, total 41
To explain a little, ins 0 and ins 1 work together to bypass the eBPF verifier mechanism. After ins 0 instruction, regs [9] = 0xffffffff, but in verifier, regs [9] .imm =-1. When ins 1 is executed, jmp instruction determines that regs [9] = = 0xffffffff, note that regs [9] is 64bit integer, because sign extension,regs [9] = = 0xffffffff results in false,eBPF skipping 2 (off) instructions and continues to execute. In verifier, the regs [9] .imm = = insn- > imm of the jmp instruction results in true, and the program goes to another branch and executes the ins 3 jmp | exit instruction, causing verifier to think that the program is finished and will not check the rest of the dead code.
So because the detection logic of eBPF is inconsistent with the runtime logic, we bypass verifier. The subsequent instruction is to read and write the kernel memory with the user-mode exp.
We also need to know the map mechanism of eBPF. In order to interact with kernel state more efficiently, eBPF has designed a set of map mechanism. Both user-mode programs and eBPF programs can read and write memory in map area and exchange data. In the code, the interaction between the user mode program and the eBPF program is completed by using the map mechanism.
Ins4-ins5: regs [9] = struct bpf_map * map, get the address of the map applied for by the user mode program. Note that the author's static analysis of these two instructions is not accurate. Get the instruction of the map pointer. In eBPF verifier, the content of the instruction will be modified to replace the value of the map pointer.
Ins6-ins12: call bpf_map_lookup_elem (map, & key), and the returned value is regs [0] = & map- > value [0]
Ins13: regs [6] = * regs [0], regs [6] gets the value of key=0 in map
Ins14-ins20: continue to call bpf_map_lookup_elem (map, & key), regs [0] = & map- > value [1]
Ins21: regs [7] = * regs [0], regs [7] gets the value of key=1 in map
Ins22-ins28: continue to call bpf_map_lookup_elem (map, & key), regs [0] = & map- > value [2]
Ins29: regs [8] = * regs [0], regs [8] gets the value of key=2 in map
Ins30: regs [2] = regs [0]
Ins32: if (regs [6]! = 0) jmp ins32 + 3, perform different operations depending on the key value passed in the user mode
Ins33: regs [3] = * regs [7]. Read the contents of the address in regs [7]. This is where the read primitive in user mode is completed. The address in regs [7] is any kernel address passed in user mode.
Ins34: * regs [2] = regs [3] to return the value read by the call instruction to the user state
Ins36: if (regs [6]! = 1) jmp ins36 + 2
Ins37: * regs [2] = regs [FP], read the runtime stack pointer of eBPF and return it to the user state. Note that the stack pointer of this eBPF actually points to a local uint64 array in the bpf_prog_run function. On the kernel stack, the base address of the kernel stack can be obtained from this value, and this instruction corresponds to the get_fp of the user state.
Ins39: * regs [7] = regs [8], write regs [8] to the address in regs [7]. For write primitive in application state, the address in regs [7] is any kernel address passed in user mode.
Understand this eBPF program, and then look at the user mode exp is easy to understand. It should be noted that the three key points in the eBPF instruction: leaking FP, reading any kernel address, and writing any kernel address, are checked in verifier, but because the first two instructions completely bypass verifier, subsequent instructions drive straight in.
The author successfully promoted the power on Ubuntu 14.04. this attack mode is different from the traditional memory destruction loophole, which does not need to do a complex memory layout, but only needs to modify the incoming data in the user mode to control the program instruction flow, making use of the normal function of the original program, which will completely bypass the existing memory defense mechanisms (SMEP/SMAP, etc.). This is also a popular Data-Oriented Attacks in the past two years, and it seems to be rare in linux kernel.
Scope of influence of 0x04 vulnerability & repair
Because there are many kernel versions of linux kernel, the scope of impact on security vulnerabilities is often not easy to identify. The most accurate way is to find out the root cause of the vulnerability and judge it from the code level, but it also brings high-cost problems. In a quick emergency, we often need to confirm the scope of vulnerability as soon as possible. From the principle of the previous vulnerability, the author roughly gives a comprehensive affected version of linux kernel:
3.18-4.4 all versions (including longterm 3.18, 4.1 and 4.4)
/ etc/apt/sources.list & & apt update & & apt install linux-image-4.4.0-117-generic
For the informal kernel version of Ubuntu, we can take a look at the key patches (note that this is the kernel version of Ubuntu, not upstream):
Git diff Ubuntu-lts-4.4.0-116.14014014.04.1 Ubuntu-lts-4.4.0-117.141_14.04.1ALU instruction distinguishes between 32bit and 64bit immediate numbers, while regs [] .imm is changed to 64bit integer
An interesting check has also been added to replace all dead_code with nop instructions, which is obviously aimed at exp, and mitigation,upstream kernel, which is a bit similar to exp, may not necessarily like this repair style:)
With regard to this vulnerability, Ubuntu also has some related fix code, which can be discovered by interested readers.
Let's take a look at the fix of upstream kernel 4.4.123, which is much more concise, with only three lines of code change [12]:
When processing 32bit ALU instructions, if imm is negative, ignore it and think it is UNKNOWN_VALUE, which avoids the semantic inconsistency between verifer and runtime mentioned earlier.
In addition, bpf sycall is not enabled on Android kernel, so it is not affected by this vulnerability.
Thinking caused by 0x05
We review the following entire vulnerability analysis process, and there are several points worth paying attention to and thinking about:
1.eBPF as a powerful mechanism provided by the kernel, because of its complex filtering mechanism, a little carelessly, will introduce fatal security problems, the author speculated that the subsequent eBPF may have similar security vulnerabilities.
two。 Limited to the development mode and many versions of linux kernel, the confirmation and repair of security vulnerabilities may be ignored, and there is a scenario in which N day changes to 0 day.
After 3.Vitaly Nikolenko announced the vulnerability exp, some netizens criticized that the details should not be released before the manufacturer released the formal patch. Let's not discuss the motivation of Vitaly Nikolenko for the time being. As a security practitioner, responsible disclosure of vulnerabilities is the basic rule.
After reading the above, do you have any further understanding of how to conduct Ubuntu kernel eBPF 0day analysis? If you want to know more knowledge or related content, please follow the industry information channel, thank you for your support.
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.