[Ext4 File System] 3. image 파일 분석 해보기 ( ext4_image1.001 )

cryptosecurity.tistory.com/15?category=841651

 

[ Ext4 File System ] 3. image 파일 분석 해보기 ( ext4_image1.001 )

ext4_image1.001 이미지를 HxD 프로그램으로 분석하고 파일을 추출해보자. 1. HxD 프로그램 열기 [도구] - [디스크 이미지 열기] - [ext4_image1.001 파일 선택] 디스크 이미지 열기를 하면 섹터 (512byte) 단위..

cryptosecurity.tistory.com

전에 ext4_image1.001,  ext4_image2.001 이미지 파일을 분석해 보았다.

분석한 것을 토대로 파이썬으로 파싱해서 파일 목록 리스트 구해보자.

 

 

1. 모듈 설정

1
2
3
4
import binascii
import struct
 
sector = 512
cs

binascii  -> 바이너리와 ASCII 간의 변환 모듈

struct -> little endian 모듈

 

 

2. 이미지 파일 받아오기

1
2
image_name = input('Input the ext4 image name (ex. ext4_image1.txt) : ')
= open(image_name, 'rb+')
 
cs

 

 

3. little endian 함수

1
2
3
4
# little endian
def little4(hex4): return struct.unpack('<L', hex4)[0# 4byte
def little2(hex2): return struct.unpack('<H', hex2)[0# 2byte
def little1(hex1): return struct.unpack('<B', hex1)[0# 1byte
cs

little4() : 4byte Hex 값을 little endian해서 10진수로 변환하는 함수

little2() : 2byte Hex 값을 little endian해서 10진수로 변환하는 함수

little1() : 1byte Hex 값을 little endian해서 10진수로 변환하는 함수

 

 

4. MBR 함수

1
2
3
4
5
6
7
# MBR
def MBR():
    f.seek(454)
    LBA_str = f.read(4# b'\x00\x08\x00\x00'
    LBA = little4(LBA_str) # 2048
 
    return LBA
 
 

MBR 영역에서 LBA 주소 구하는 함수

LBA = 오프셋 454부터 4byte 값을 little endian한 값

 

1
2
LBA = MBR()
Superblock_addr = LBA + 2
cs

Super Block 주소는 LBA 시작 주소 + 2 이다.

 

 

5. Super Block 함수

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
# Super Block
def Superblock(Superblock_addr):  
    f.seek(Superblock_addr * sector) # 2050 * 512
    sb = f.read(sector)
 
    # 4byte씩 나누기
    sb_str = binascii.b2a_hex(sb).decode()
    sb_list = [sb_str[i:i+8for i in range(0, sector*28)]
    
    # log block size
    log_block_hex = binascii.unhexlify(sb_list[7])
    log_block = little4(log_block_hex)
    
    if log_block == 0:
        log_block_size = 1 # 1kb
    elif log_block == 1:
        log_block_size = 2 # 2kb
    elif log_block == 2:
        log_block_size = 4 # 4kb
 
    # Inode_Per_Group
    Inode_Per_Group_hex = binascii.unhexlify(sb_list[10])
    Inode_Per_Group = little4(Inode_Per_Group_hex)
    
    return log_block_size, Inode_Per_Group
cs

Super block 주소는 섹터단위이므로 Superblock_addr에 sector값을 곱해준다.

sb = 섹터 Super block 주소부터 512byte 값 저장

sb_list = 512byte를 4byte씩 나눠서 리스트에 저장

 

log_block = sb_list 중 7번째 값을 little endian한 값

log_block이 0이면 log_block_size는 1KB

log_block이 1이면 log_block_size는 2KB

log_block이 2이면 log_block_size는 4KB

 

Inode_Per_Group = sb_list 중 10번째 값을 little endian한 값

 

log_block_size와 Inode_Per_Group 값을 리턴하는 함수

 

1
2
log_block_size, Inode_Per_Group = Superblock(Superblock_addr )
GDT_addr = Superblock_addr - 2 + log_block_size * 2
cs

GDT 주소는 Superblock 주소 - 2 + log_block_size x 2 이다.

GDT_addr = 2056

 

 

6. GDT (Group Descriptor Table) 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Group Descriptor Table
def GDT(GDT_addr, bg_num):
    bg_size = 32
 
    f.seek(GDT_addr * sector + bg_size * bg_num)
    GDT = f.read(bg_size)
 
    GDT_str = binascii.b2a_hex(GDT).decode() 
    GDT_list = [GDT_str[i:i+8for i in range(0, sector*28)]
 
    # Inode Table Address
    Inode_Table_hex = binascii.unhexlify(GDT_list[2])
    Inode_Table_dec = little4(Inode_Table_hex) 
    
    return Inode_Table_dec
 
cs

GDT 주소는 섹터단위이므로 GDT_addr에 sector값을 곱해준다.

GDT에는 Block Group 정보들이 32byte씩 여러개 저장되어 있다.

root_directory를 먼저 살펴봐야 하기에 이때는 Block Group 0번을 살펴봐야 한다. ( bg_num = 0 )

 

나중에 root_directory에 저장되어 있는 파일을 살펴 본 후 root_directory 안에 Directory 파일이 있을 경우에,

0이 아닌 bg_num 값을 받아오게 된다.

 

따라서 GDT_addr x sector 값에 bg_size x bg_num을 더해준다.

 

GDT  = 섹터 GDT 주소 + (bg_size x bg_num)byte부터 32byte 값 저장

GDT_list = 32byte를 4byte씩 나눠서 리스트에 저장

 

Inode_Table_dec = GDT_list 중 2번째 값을 little endian한 값

 

1
2
Inode_Table_dec = GDT(GDT_addr, 0)
Inode_Table_addr = (Inode_Table_dec * log_block_size * 2+ LBA
cs

root_directory를 살펴봐야 하기에 bg_num = 0을 넣는다.

Inode_Table 주소는 Inode_Table_dec x log_block_size x 2 + LBA 주소 이다.

 

 

7. Inode Table 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Inode Table
def Inode_Table(Inode_Table_addr, Inode_num):
    Inode_size = 256
    f.seek(Inode_Table_addr * sector + Inode_size * (Inode_num - 1))
    Inode = f.read(Inode_size)
 
    Inode_str = binascii.b2a_hex(Inode).decode() 
    Inode_list = [Inode_str[i:i+8for i in range(0, Inode_size*28)]
 
    # Directory Pointer
    DP_list = Inode_list[10:22]
 
    for a in reversed(DP_list):
        if a != '00000000':
            DP = a
            break
    
    DP_hex = binascii.unhexlify(DP)
   DP_dec = little4(DP_hex)
 
    return DP_dec
 
cs

Inode Table 주소는 섹터단위이므로 Inode_Table_addr에 sector값을 곱해준다.

Inode Table에는 Inode 들이 여러개 저장되어 있다.그 중에 우리는 Root Directory 정보가 저장되어 있는 Inode 2번을 살펴볼 것이기에 Inode_num = 2 이다.따라서 Inode_Table_addr x sector 값에 Inode_size x (Inode_num - 1)을 더해준다.

 

Inode  = (섹터 Inode Table 주소+ 256byte)부터 256byte 값 저장

Inode_list = 256byte를 4byte씩 나눠서 리스트에 저장

 

Directory Poiner는 Inode_list 중 10번부터 22번째 값 중 0이 아닌 제일 마지막 값이다.

DP_dec = DP_hex 값을 little endian한 값

 

1
2
DP_dec = Inode_Table(Inode_Table_addr, 2# root directory
DP_addr = (DP_dec * log_block_size * 2+ LBA
cs

Directory Pointer 주소는 DP x log_block_size x 2 + LBA 주소 이다.

 

 

 

8. Directory Entry 함수

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Directory Entry
def Directory_Entry(DP_addr):
    DE_size = 256
    f.seek(DP_addr * sector)
    DE = f.read(DE_size)
    
    # 1byte씩 나누기
    DE_str = binascii.b2a_hex(DE).decode()
    DE_list = [DE_str[i:i+2for i in range(0, DE_size*22)]
 
   File_name, Dir_name, Block_Group_num = [], [], []
    i = 24
    while(i < DE_size):
        # Inode_num
        I_num_hex = binascii.unhexlify(DE_list[i] + DE_list[i+1+ DE_list[i+2+ DE_list[i+3])
        Inode_num = little4(I_num_hex)
        
        # record length
        r_len_hex = binascii.unhexlify(DE_list[i+4+ DE_list[i+5])
        record_len = little2(r_len_hex)
        
        # file name length
        n_len_hex = binascii.unhexlify(DE_list[i+6])
        name_len = little1(n_len_hex)
 
        # file type
        FT_hex = binascii.unhexlify(DE_list[i+7])
        FT_dec = little1(FT_hex)
 
        if FT_dec == 0:
            file_type = 'unknown'
        elif FT_dec == 1:
            file_type = 'Regular'
        elif FT_dec == 2:
            file_type = 'Directory'
 
        # file name
        name = []
        for k in range(88 + name_len):
            name.append(DE_list[i+k])
 
        name_hex = binascii.unhexlify(''.join(name))
        name = name_hex.decode()
        File_name.append(name)
 
 
        if file_type == 'Directory' and name != 'lost+found':
           Dir_name.append(name_hex.decode())
            Block_Group_num.append(int((Inode_num-1)/Inode_Per_Group))
        
        i += record_len
 
    return File_name, Dir_name, Block_Group_num
 
cs

Directory Entry 주소는 섹터단위이므로 DE_addr에 sector값을 곱해준다.

 

DE  = 섹터 75056 부터 256byte 값 저장

DE_list = 256byte를 1byte씩 나눠서 리스트에 저장

 

하나의 레코드 당

      Inode num은 4byte

      record length는 2byte

      file name length는 1byte

      file type은 1byte

      file name은 file name length byte

로 구성되어 있다.

 

i = 24

Inode_num은 DE_list에 i번째 부터 4byte 값을 little endian한 값

record_len은 DE_list에 i+4번째 부터 2byte 값을 little endian한 값

name_len은 DE_list에 i+6번째 부터 1byte 값을 little endian한 값

FT_dec은 DE_list에 i+7번째 부터 1byte 값을 little endian한 값

name은 DE_list에 i+8번째 부터 file name length byte 값을 little endian한 값

 

i에 record length 값을 더하고 다시 반복

 

File_name에 name을 리스트로 저장

만약에 'lost+found' 제외하고 file_type이 Directory라면

                Folder_name에 그때의 name을 리스트로 저장

                Block_Group_num에 그때의 ( ( inode_num - 1 ) / Inode_Per_Group ) 값을 리스트 저장

 

File_name, Folder_name, Block_Group_num 반환

 

1
2
3
4
File_name, Dir_name, Block_Group_num = [], [], []
File_name, Dir_name, Block_Group_num = Directory_Entry(DP_addr)
 
print('root -->', File_name)
 
cs

root Directory에 들어있는 File_name을 출력한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while(len(Dir_name) != 0):
    i=0
    for bg_num in Block_Group_num:
        f_Inode_Table_dec = GDT(GDT_addr, bg_num)
        f_Inode_Table_addr = (f_Inode_Table_dec * log_block_size * 2+ LBA
 
        f_DP_dec = Inode_Table(f_Inode_Table_addr, 1)
        f_DP_addr = (f_DP_dec * log_block_size * 2+ LBA
 
       f_File_name, f_Dir_name, f_Block_Group_num = [], [], []
       f_File_name, f_Dir_name, f_Block_Group_num = Directory_Entry(f_DP_addr)
 
        print(Dir_name[i], '-->', f_File_name)
        i+=1
 
   Block_Group_num, Dir_name= f_Block_Group_num, f_Dir_name
cs

Dir_name 길이가 0이 아닐 때

              Block_Group_num 값 하나하나를 bg_num으로 놓는다.

               GDT(), Inode_Table(), Directory_Entry() 함수를 다시 통과해서 Directory안에 들어있는 Regular 파일을 구한다.

              Dir_name 파일에 들어있는 f_File_name을 출력한다.

              Directory 파일이 없을 때 까지 반복한다.

 

 

== 실행결과 ==

  • ext4_image1.txt 이미지 실행 결과

 

  • ext4_image2.txt 이미지 실행결과

ext4_image1.001 이미지를 HxD 프로그램으로 분석하고 파일을 추출해보자.

 

 

1. HxD 프로그램 열기

[도구] - [디스크 이미지 열기] - [ext4_image1.001 파일 선택]

디스크 이미지 열기를 하면 섹터 (512byte) 단위로 분석할 수 있다.

 

 

2. MBR 분석

MBR이란 저장 매체(물리 디스크)의 첫 번째 섹터에 위치하며 512Byte의 크기이다.

맨 마지막 2byte 가 MBR signature 부분이다. 

MBR signature = 0x55AA

 

연두색 네모 부분이 다음과 같은 구조를 가진다.

 

• Part type

    -  0x83 --> Linux native file system (ex2fs/xiafs)

 

• LBA 시작주소 (Starting LBA Addr)

   - 실제 파티션이 시작되는 섹터의 위치

   - 주소계산 할 때 사용

   - 0x00000800 = 2048 (Little Endian)

 

 

3. Super Block

LBA 시작주소인 섹터 2048이 Superblock의 주소이다.

 

[ 섹터 2048, 2049 ]

섹터 2048, 2049는 다음과 같이 0패딩이 되어있어 0으로 채워져 있다.

 

 

[ 섹터 2050 ~ ] 

 

superblock은 총 1024byte로 실제 사용하는 영역은 상위 264byte이고 나머지 하위 영역은 예약된 영역이다.

아래와 같은 구조를 가진다.

•  Log Block Size

  - 블록의 크기 지정

  - 0 : 1KB, 1 : 2KB, 2 : 4KB

  - 0x02 = 2 --> 블록 크기는 4KB이다.

 

•  Block Per Group

  - 각 블록 그룹에 속한 블록 개수

  - 0x8000 = 32768

 

•  Inode Per Group

  - 각 블록 그룹에 속한 아이노드 개수

  - 0x1FF0 = 8176

 

•  Magic Signature

  - 슈퍼블록의 시그니처

  - 0x53EF

 

Super Block은 총 1block의 크기를 지닌다. 

블록의 크기가 4KB이므로 SuperBlock은 8sector의 크기이다. (1KB = 1024byte) 

 

• Boot Sector 영역 (0패딩) = 2sector

• Superblock의 실제 영역 = 2sector

• unused 영역 = 4sector

따라서 Group Descriptor Table 영역 시작 주소는 섹터 2056이다.

 

 

4. Group Descriptor Table

Group Descriptor Table 시작 주소인 섹터 2056으로 이동하자.

연두색 네모 부분이 다음과 같은 구조를 가진다.

맨 처음 32byte 부분이 0번 블록그룹 정보이다.

그 다음도 32byte씩 1번, 2번 등등의 블록그룹 정보가 저장되어 있다.

0번 블록그룹 정보를 자세히 살펴보자.

 

 Block Bitmap 시작 주소 ( Starting Block Address of Block Bitmap )

  - 0x0396 = 918

  - 섹터단위 주소계산

                 = Block Bitmap 시작 주소 x 1 block 크기 + LBA 시작주소

                 = ( 918 x 8 ) + 2048 = 9392

 

 Inode Bitmap 시작주소

  - 0x03A6 = 934

  - 섹터단위 주소계산 = ( 934 x 8 ) + 2048 = 9520

 

 Inode Table 시작주소

  - 0x03B6 = 950

  - 섹터단위 주소계산 = ( 950 x 8 ) + 2048 = 9648

 

 

5. Block Bitmap

Block Bitmap 시작 주소인 섹터 9392로 이동하자.

Block Bitmap은 블록의 사용 현황을 Bit로 표현하여 나타낸 것이다.

 

ex) 0xFF = 0b1111 1111

            --> 1의 개수 = 8

            --> 8개 블록 사용중

 

 오프셋 496000 ~ 496474 : 0xFF로 채워져 있다.

                          --> 8 * ( 16 * 47 + 5 ) = 5056

 오프셋 496475 : 0x0F

                          --> 4

   

                          --> 5056 + 4 = 6060

 

 블록현황 : 6060개 블록 사용 중

 

 

6. Inode Bitmap

Inode Bitmap 시작 주소인 섹터 9520으로 이동하자.

Inode Bitmap은 아이노드의 사용 현황을 Bit로 표현하여 나타낸 것이다.

 0xFFFF = 0b 1111 1111 1111 1111

 아이노드 현황 : 16개 아이노드 사용 중

 

 

7. Inode Table

Inode Table 시작 주소인 섹터 9648으로 이동하자.

Inode 크기 = 256byte

한 섹터 당 두개의 Inode를 지니고 있다.

Inode 2번이 Root Directory이다. 

따라서 Inode 2번을 자세히 살펴보자.

 

 

[ Inode 2번 ]

Inode의 구조는 다음과 같다.

 

 Direct Pointer

   - 파일의 내용이 들어있는 블록의 번호 기록

   - 루트 디렉토리의 포인터

   - 0x23A6 = 9126

   - 섹터주소 계산 : ( 9126 * 8 ) + 2048 = 75056

 

 

8. Directory Entry

Director Entry 구조는 다음과 같다.

 

다음과 같이 lost+found 디렉토리 1개와 KAKAO.jpeg, TEST_FILE, apeach.png, kakao_apeach.png, toystory.jpeg 파일 5개가 이미지 안에 저장되어 있는 것을 알 수 있다.

 

먼저 KAKAO.jpeg 파일을 추출하기 위해 아이노드 12번을 자세히 살펴보자.

 

 

8.1. KAKAO.jpeg

Inode 1번이 섹터 9648이므로 Inode 11번은 섹터 9653이다.

 

[ 아이노드 12번 ] 

 Direct Pointer : 0x8396 = 33686

 주소 계산 : ( 33686 * 8 ) + 2048 = 271536

 

 

KAKAO.jpeg 정보가 저장되어 있는 섹터 271536로 이동하자.

 

 JPEG 파일

   - 헤더 시그니처 : FF D8 FF E0 --> 오프셋 : 8496000

   - 푸터 시그니처 : FF D9 --> 오프셋 : 849CC99

 

1) [편집] - [블록단위 선택]  :  오프셋 8496000 부터 오프셋 849CC99 까지 블록선택을 하고 복사를 한다.

2) [파일] - [새로 만들기]  :  새로운 창에 복사한 값을 붙여넣기 삽입한다.

3) KAKAO.jpeg로 저장한다.

 

KAKAO.jpeg 파일을 열어보면 다음과 같은 사진을 볼 수 있다.

 

 

8.2. TEST_FILE

[ 아이노드 13번 ]

 Direct Pointer : 0x839D = 33693

 섹터 주소 계산 : ( 33693 * 8 ) + 2048 = 271592

 

TEST_FILE 정보가 저장되어 있는 섹터 271592로 이동하자.

2020 FaS filesystem!!!! 라고 저장되어 있는 것을 볼 수 있다.

 

 

8.3. apeach.png

[ 아이노드 14번 ]

파일 사이즈가 0으로 파일을 찾을 수 없다.

 

 

8.4. kakao_apeach.png

[ 아이노드 15번 ]

 Direct Pointer : 0x839E = 33694

 섹터 주소 계산 : ( 33694 * 8 ) + 2048 = 271600

 

kakao_apeach.png 정보가 저장되어 있는 섹터 271600으로 이동하자.

 PNG 파일

   - 헤더 시그니처 : 89 50 4E 47 0D 0A 1A 0A --> 오프셋 : 849E000

   - 푸터 시그니처 : 49 45 4E 44 AE 42 60 82 --> 오프셋 : 855B23F

 

1) [편집] - [블록단위 선택]  :  오프셋 849E000 부터 오프셋 855B23F 까지 블록선택을 하고 복사를 한다.

2) [파일] - [새로 만들기]  :  새로운 창에 복사한 값을 붙여넣기 삽입한다.

3) kakao_apeach.png로 저장한다.

 

kakao_apeach.png 파일을 열어보면 다음과 같은 사진을 볼 수 있다.

 

 

8.5. toystory.jpeg

[ 아이노드 16번 ]

 Direct Pointer : 0x845C = 33884

 주소 계산 : ( 33884 * 8 ) + 2048 = 273120

 

toystory.jpeg 정보가 저장되어 있는 섹터 273120로 이동하자.

 JPEG 파일

   - 헤더 시그니처 : FF D8 FF E0 --> 오프셋 : 8496000

   - 푸터 시그니처 : FF D9 --> 오프셋 : 849CC99

 

1) [편집] - [블록단위 선택]  :  오프셋 849E000 부터 오프셋 855B23F 까지 블록선택을 하고 복사를 한다.

2) [파일] - [새로 만들기]  :  새로운 창에 복사한 값을 붙여넣기 삽입한다.

3) toystory.jpeg로 저장한다.

 

toystory.jpeg 파일을 열어보면 다음과 같은 사진을 볼 수 있다.

 

 

9. 총 정리

따라서 ext4_image1.001의 디렉터리 구조는 다음과 같다.

 

[ 1탄 ] JEB로 tigerconnect.apk 분석

cryptosecurity.tistory.com/13?category=841778

 

[ Reversing ] JEB로 tigerconnect.apk 분석

1. JEB 실행하기 1) JEB, APK 설치 2) JAVA 설치 3) JEB 실행 4) 파일 - 열기 - 앱 선택 5) Project Explore에서 앱 더블클릭 -> bytecode 더블클릭 6) classese.dex 파일 보여줌 2. JEB로 apk 분석 4) Q..

cryptosecurity.tistory.com

 

 

[ tigerconnect.apk의 decrypt 구조화 ]

JEB 분석을 통해 찾은 tigerconnect.apk의 decrypt 구조화를 이용해서 데이터베이스를 복호화 해보자.

 

 

1. DB 복호화 소스코드 구현

-> 파이썬으로 AuthToken 구하기

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
from hashlib import pbkdf2_hmac
from Crypto.Cipher import AES
import base64
 
def decrypt(key, enc):
    cipher = AES.new(key, AES.MODE_ECB)
    dec = cipher.decrypt(enc)
    result = dec[:-1*dec[-1]]
    return result
 
salt = 'jQx+pvuo3TJ7aN9zPJnGPVTfIfrDG9uEkIHEVOZ6ez8='
ttkey01 = 'oum53H3VwmQoECodZfek7ckgo2qRrtta'
ttkey02 = 'USDNyIf90wXg4ifLTVFb//qRobxlJjqOaAPAn/HHxBV2AmCeLiBi2R4+RZvZjhzaHhFa+HQDdVnw z+/gzSU9Ug=='
 
salt2 = base64.b64decode(salt)
ttkey01_byte = ttkey01.encode()
ttkey02_decode = base64.b64decode(ttkey02)
 
secretkey = pbkdf2_hmac('sha1', password=ttkey01_byte, salt=salt2, iterations=1000, dklen=32)
print('secretkey :', base64.b64encode(secretkey).decode('utf-8'))
 
restsecret = decrypt(secretkey, ttkey02_decode)
print('restsecret :', restsecret)
 
authtoken = ttkey01_byte + bytes(b':'+ restsecret
print('authtoken :', authtoken)
cs

 

[ 실행 결과 ]

 

2. DB Browser for SQLCipher 

2.1 ) DB Brower for SQLCipher.exe 실행

 

2.2 ) 파일 - 데이터베이스 열기 - ttandroid.db

암호화가 걸려있는 것을 볼 수 있다.

 

2.3) 앞에서 구했던 AuthToken 값 입력

AuthToken = oum53H3VwmQoECodZfek7ckgo2qRrtta:UNql9sXWri3hSSt95vfhXbgoQNFaKLiMMGfcBx1p4YszBhCj

 

tigerconnect 데이터베이스 복호화 성공

 

 

 

[ Github 소스코드 파일 ]

github.com/K-subin/tigerconnect_DB_decrypt

'Digital Forensic > Reversing' 카테고리의 다른 글

[ Reversing ] JEB로 tigerconnect.apk 분석  (0) 2020.09.12
[ Reversing ] 리버싱이란?  (0) 2020.09.10

1. JEB 실행하기

1) JEB, APK 설치

 

2) JAVA 설치

 

3) JEB 실행

 

4) 파일 - 열기 - 앱 선택

 

5) Project Explore에서 앱 더블클릭 -> bytecode 더블클릭

 

6) classese.dex 파일 보여줌

 

 

2. JEB로 apk 분석

 

4) Q 눌러서 JAVA 코드로 변환

 

 

3. 의미있는 함수 찾기

3.1) AuthToken 구하기

  • decryptCipher()

getAuthToken() 반환

 

 

  • getAuthToken()

getAuthToken  =  [ RestKey : RestSecret ]

 

 

  • getString()

ttkey01의 String 얻는 함수

RestKey  =  ttkey01의 String

 

 

  • getSecureString()

RestSecret =  decrypt( ttkey02의 String )

 

 

  • decrypt()

v0 = RestKey

 

 

  • decrypt().a()

RestKey를 평문으로 받아 Secretkey 구하는 함수

 

 

  • decrypt().a().c()

 

Secretkey = PBKDF2-SHA1 ( restkey, salt, iteration=1000, keylen = 256 )

 

  • decrypt().a()

 

 

  • decrypt().a().a()

AES, ECB, PKCS5패딩, KEY=secretkey로 복호화를 정의하는 함수

 

 

  • decrypt()

decrypt(ttkey02)

1. ttkey02로 restkey 생성

2. restkey로 pbkdf-sha1 이용해서 secretkey 생성

3. 복호화  ->  AES, ECB, PKCS5, secretkey

4.  UTF-8로 디코딩해서 출력

 

 

 

 

  • getAuthToken()

Authtoken = [RestKey : RestSecret] = [ ttkey01의 String  : decrypt ( ttkey02의 String) ]

 

 

 

3.2 ) AuthToken을 이용해서 Secretkey 2 얻기

  • decryptCipher()

 

 

  • decryptCipher().c()

 

Secretkey2 = PBKDF2-SHA1 ( AuthToken, salt, iteration = 1000, keylen = 256 )

 

 

 

3.3) decryptCipher 구하기

  • decryptCipher()

 

 

  • decryptCipher().a()

 

 

  • decryptCipher()

 

 

4. tigerconnect.apk의 decrypt 구조화하기

1. PBKDF2의 이해

  • 의사 난수 기능을 적용하여 키 파생
  • 파생된 키 길이는 제한 없음
    • 그러나 파생된 키의 최대 유효 검색 공간은 기본 의사 난수 함수의 구조에 의해 제한 될 수 있다.

 

2. PBKDF2의 동작과정

 

 

 

 

 

INT(i) : i를 4byte로 인코딩

 

 

 

 

 

 

 

 

3. PBKDF2 소스코드 구현

[ 모듈 이용 ] 

1
2
3
4
5
6
7
### 모듈 이용 ###
from hashlib import pbkdf2_hmac
import base64
 
= pbkdf2_hmac('sha1', password='password'.encode(), salt='salt'.encode(), iterations=1000, dklen=32)
print('[ 모듈 이용 ] DK =', base64.b64encode(K).decode())
 
cs

 

 

[ 동작원리 이용 ]

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
### 동작원리 이용 ###
import hashlib
import hmac
import base64
import math
import operator
 
def pbkdf2(password, salt, iteration, dklen):
    hlen = hmac.new(password, msg=None, digestmod=hashlib.sha1).digest_size  
    len = math.ceil(dklen/hlen)
    r = dklen-(len-1)*hlen
    DK = b''
    for i in range(1len+1):
        T = bytes(hlen)
        INT = i.to_bytes(4, byteorder='big')
        U_0 = salt + INT
        for j in range(iteration):
            U_N = hmac.new(password, U_0, hashlib.sha1).digest()
            T = bytes(map(operator.xor, T, U_N))
            U_0 = U_N
        if (i == len):
            DK += T[:r]
            return DK
        DK += T
    return DK
    
DK = pbkdf2('password'.encode(), 'salt'.encode(), 100032)
print('[ 동작원리 이용 ] DK =', base64.b64encode(DK).decode())
cs

 

 

=== 실행결과 ===

 

 

== Github 소스코드 파일 ==

github.com/K-subin/PBKDF

1. PBKDF1의 이해

  • MD2, MD5, SHA-1인 해시함수 적용
  • 파생된 키 길이는 제한
    • MD2, MD5 – 16byte
    • SHA-1 – 20byte

 

 

 

 

Salt와 iteration은 공격자의 사전공격을

어렵게 하는 중요한 요소

 

 

 

 

 

2. PBKDF1의 동작과정

 

3. PBKDF1 소스코드 구현

1
2
3
4
5
6
7
8
9
10
11
12
import hashlib
import base64
 
def pbkdf1(password, salt, iteration, dklen):
    T_0 = password + salt
    for i in range(iteration):
        T_N = hashlib.sha1(T_0).digest()
        T_0 = T_N
    return T_0[:dklen]
 
DK = pbkdf1('password'.encode(), 'salt'.encode(), 100020)
print(base64.b64encode(DK).decode('utf-8'))
cs

 

 

==실행결과==

 

 

== Github 소스코드 파일 ==

github.com/K-subin/PBKDF

[ FBE(File-Based Encryption)의 동작 과정 ]

  • 파일 시스템 수준에서 수행
  • ext4 파일 시스템 암호화

 

1. FDE와 유사하게 User key와 salt를 이용해서 KDF를 이용해 KEK, IV를 생성

 

 

 

 

 

 

 

 KEK는 TEE에 안전하게 보관된다.

 

 

 

 

 


2. KEK와 IV를 사용하여 AES-256-GCM으로 DEK를 암호화

  

     - DEK : dev/urandom에서 읽은 64byte 난수

     - 상위 32byte는 파일 내용 암호화하는데 사용

     - 하위 32byte는 파일 이름 암호화하는데 사용

 

 

      TEE에서 KEK를 가져오려면 3가지 요구사항을 충족해야 한다.

      (1) 확장된 사용자 자격증명

               :  KDF(scrypt)로 사용자 인증을 확장

      (2) Secdiscardable hash (사용자 SID)

               :  임의의 16KB 파일로 이루어진 512bit 해시

      (3) 인증 토큰

               :  사용자가 정상적으로 로그인했을 때 게이트키퍼에 의해

                  생성되는 암호화방식으로 인증된 토큰


3. DEK와 nonce를 사용하여 AES-ECB-128로 암호화해서 각 파일별 키 생성 

 

 

 

  • 각 파일에는 연결된 16byte nonce가 dentry에 저장
  • dentry는 ext4 파일 시스템 구조에 inode항목에 연결하는 데이터 구조

 

  • DEK 상위 32byte -> AES-ECB-128 -> 파일 이름 암호화 키 생성
  • DEK 하위 32byte -> AES-ECB-128 -> 파일 내용 암호화 키 생성


4.  파일 이름은 각 파일별 키와 IV를 사용하여                                                                                                          AES-256-CBC-CTS로 암호화 된다.

  • IV는 0으로 채워진다.
  • 결과 암호문은 base62 인코딩을 사용하여 인코딩 된다.
  • 암호화된 파일 이름은 dentry에 저장된다.

 

5. 파일 내용은 각 파일별 키를 사용하여                                                                                                                    AES-256-XTS로 암호화된다.

  • 암호화된 파일 내용은 inode에 저장된다.

6.  Key Store

‘crypto footer’ 유추  ->  /data/misc/vold/user_keys 디렉터리

‘de’와 ‘ce’ 두 개의 하위 디렉터리로 나뉜다.

 

=================================================

 

“encrypted_key”   :  암호화된 DEK 포함

“keymaster_key_blob   :  암호화된 RSA-2048 개인키 포함

“salt”   :  DEK를 복호화할 때 사용자 인증과 결합하여 사용

                   -> de 디렉터리는 사용자 입력이 필요하지 않으므로 salt 없음

“secdiscardable   :  임의의 16KB 파일의 512bit 해시 값

“stretching”   :   KDF에 사용되는 매개변수 포함

                   -> “scrypt X : Y : Z” 형식

                   ->  N = 2^X,  r = 2^Y,  p = 2^Z

1. FBE(File-Based Encryption)란?

  • 여러 키를 사용하여 여러 파일을 암호화 가능
  • 안드로이드 7.0부터 FBE 추가
  • 직접 부팅(Direct Boot) 기능 추가
    • 인증하지 않은 상태에서 바로 부팅 가능
    • 그러나 중요한 데이터는 복호화되지 않음
  • DE와 CE로 나눠서 각각 암호화하고 각각의 접근 범위를 설정한다.
    • CE(Credential Encrypted) storage  :  기본 저장공간, 사용자 인증 이후로만 사용 가능한 저장소
    • DE(Device Encrypted) storage  :  사용자 인증과 상관없이 사용 가능한 저장소(직접 부팅, 인증 이후 모두 가능)
  • 접근성이 설정된다면?
    • DE영역만 접근하도록 하여 데이터 보호 -> 암호화 무력화 문제 해결

 

2. FBE Decryption

• 파일 수준에서 암,복호화

• 기본 ext4 파일 시스템이 지원하는 fscrypt 암호화를 사용.

• 별도의 암호화된 두 가지 스토리지 영역

    - DE : 사용자 인증 전에 사용 가능.

    - CE : 사용자 인증 후에만 사용 가능.

1. 먼저 KEK를 생성하는데, FDE와 비슷한 방식으로 생성.

2. AES-256-GCM을 이용하여 CE, DE 별도의 마스터키가 생성.

3. AES-128-ECB와 128bit nonce를 이용하여 file key를 생성.

4. AES-256-CTS를 사용하여 파일 이름 복호화, AES-256-XTS를 사용하여 파일 내용 복호화.

 

 

+ Recent posts