1. 개요
•
이전부터 임베디드 장비에 Doom을 올려보고 싶었다.
•
단순 취약점을 찾는게 아닌 게임을 올리려면 수정 해야 할 여러가지들이 존재했으며, 특히 펌웨어를 수정하여 게임을 올리는 과정에서 펌웨어 구조를 파악하는데 좋은 계기가 될 것 같았다.
•
여러가지 자료를 찾아보는 중 IPPhone에 둠을 올리는 자료가 존재했으며, 동일 제품을 구매하여 Doom을 올려보기로 하였다. 설명이 매우 친절하게 나와있기 때문에 따라 해보면서 설명되어 있지 않은 부분은 추가적으로 작성해가며 정리하였다.
•
성공을 한다면 이제 응용을 들어가는 걸로 ..
2. 펌웨어 추출
•
우측 하단에 보이는 5pin → UART 핀
◦
좌측부터 : GND, X, X, RX, TX
•
UART 연결 시 부팅 로그가 출력되며 마지막에 로그인 창이 뜸
•
전체 부팅 로그
Bonanza U-Boot > printenv
...
loadaddr=0x10900000
rd_loadaddr=(0x10900000 + 0x300000)
bootargs=console=ttymxc1,115200 video=mxcfb0:dev=ldb,1024x600@60,if=RGB666 fbmem=28M vt.global_cursor_default=0 eth=${ethaddr} bt=${btaddr} NoReset hardware_revision=5 ip=off
bootargs_mmc=set bootargs console=${console} video=${video} eth=${ethaddr} bt=${btaddr} ${resetbutton} boot_image=${boot_image} hardware_revision=${hardware_revision} ip=off
bootcmd=run bootargs_mmc; run auto_bootcmd
bootfile=bonanza.combo
bootflash1=set boot_image 1; run bootargs_mmc; mmc dev 3; mmc read ${loadaddr} ${kernel1blockstart} ${kernel1blockcount}; bootm; || run bootflash2;
bootflash2=set boot_image 2; run bootargs_mmc; mmc dev 3; mmc read ${loadaddr} ${kernel2blockstart} ${kernel2blockcount}; bootm; || run bootflash1;
...
kernel1blockcount=10000
kernel1blockstart=1200
kernel2blockcount=10000
kernel2blockstart=11400
...
reset_bootargs=set bootargs console=${console} video=${video} eth=${ethaddr} bt=${btaddr} hardware_revision=${hardware_revision} ip=off; saveenv
splashimage=10800000
splashpos=0,0
ubootblockcount=1000
ubootblockstart=0
video=mxcfb0:dev=ldb,1024x600@60,if=RGB666 fbmem=28M vt.global_cursor_default=0
..
auto_bootcmd=set boot_image 1; run bootargs_mmc; mmc dev 3; mmc read ${loadaddr} ${kernel1blockstart} ${kernel1blockcount}; bootm; || run bootflash2;
Environment size: 1808/8188 bytes
Shell
복사
•
부팅 중 esc 를 연타하면 uboot 콘솔에 진입가능
•
printenv 명령 결과를 통해 해당 장비는 두 개의 커널 이미지를 통해 에러 발생시 swap을 통하여 정상 부팅이 가능한 구조로 되어있음
mmc read ${loadaddr} ${kernel1blockstart} ${kernel1blockcount}
# loadaddr : 0x10900000
# kernel1blockstart : 0x1200
# kernel1blockcount : 0x10000
# kernel2blockstart : 0x11400
# kernel2blockcount : 0x10000
# ramdisk 위치 : loadaddr + 0x300000
Shell
복사
•
•
따라서 eMMC에 있는 데이터를 위 mmc read ~ 명령을 수행하여 메모리에 올린 뒤, md 명령어로 32MB의 펌웨어 일부를 추출하는 코드를 작성하면 됨
•
정상적으로 동작하는지 Uboot 콘솔에서 먼저 mmc read 명령어로 확인
•
eMMC에서 정상적으로 데이터가 읽어지며, md.b 명령어로 확인해보면 커널 헤더가 보임
•
총 32MB 크기를 시리얼 통신 → 파이선 코드로 포팅하여 짜야 함
•
시리얼 통신의 물리적 연결 및 115200 의 baudrate 특징으로 인해 read 시 오랜 시간이 걸리며 중간에 오류가 날 수 도 있는 점을 주의하여 코드를 작성해야 함
•
시리얼 통신 코드 설계 아이디어
◦
“주소 : data : ascii” 형식의 텍스트 형태로 0x2000 크기만큼 짤라서 파일로 저장
extract_kernel.py
◦
각 파일의 주소 정렬이 0x10 크기로 잘 증가되는지, 데이터는 16개가 하나의 라인에 잘 나오는지, hex 데이터의 ascii가 일치하는지 확인 후 전체 텍스트 파일로 저장 및 최종 바이트 형태로 저장
hexdump_parser.py
•
메모리 레이아웃(클로드가 그려줌..)
•
binwalk로 추출한 이미지를 돌려보면 ramdisk로 추정되는 파일시스템 영역이 잘 추출됨
$ ls
bcm4330b2.hcd bin dev etc lib mnt proc sbin sys tmp usr
Shell
복사
3. 펌웨어 분석
# This is run first except when booting in single-user mode.
::sysinit:/etc/rc.sh
::restart:/sbin/init
::respawn:/etc/scripts/system_data.sh
::respawn:/usr/bin/bonanza 2>/tmp/stderr >/tmp/stdout
# Start a shell on the terminal [Must be last 'respawn' entry to avoid ^C problem]
::respawn:/sbin/getty 115200 ttymxc1 vt100 -w -n -i
# Stuff to do before rebooting
::shutdown:/etc/shutdown.sh
Shell
복사
/etc/inittab
•
핵심 코드
◦
초기 rc.sh를 실행 → 각종 초기화 스크립트 존재
◦
system_data.sh 실행 → 시스템 초기화
◦
/usr/bin/bonanza 실행 → 메인 바이너리. respawn 설정으로 인해 죽어도 재실행
root:6fBoITKfBdSto:0:0:root:/:/bin/sh
bin:*:1:1:bin:/bin:
daemon:*:2:2:daemon:/sbin:
halt:*:7:0:halt:/sbin:/sbin/halt
ftp:*:14:50:FTP User:/
nobody:*:99:99:Nobody:/:
Shell
복사
/etc/passwd
•
shadow 파일은 없으며 root 계정의 해시값 13바이트(DES) 만 설정되어 있음
•
DES 암호화이기 때문에 비교적 크랙에 성공할 가능성이 높음. 따라서 존더리퍼를 사용하여 크랙시도
$ john --format=descrypt passwd.txt 1 ↵
Loaded 1 password hash (descrypt, traditional crypt(3) [DES 128/128 SSE2-16])
Will run 18 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
Warning: MaxLen = 13 is too large for the current hash type, reduced to 8
0g 0:00:05:09 3/3 0g/s 3725Kp/s 3725Kc/s 3725KC/s b3sg9f..b3hkau
0g 0:00:05:24 3/3 0g/s 3825Kp/s 3825Kc/s 3825KC/s cigary66..cyrsepas
0g 0:00:05:34 3/3 0g/s 3898Kp/s 3898Kc/s 3898KC/s ric2cba..ribldie
0g 0:00:05:55 3/3 0g/s 3995Kp/s 3995Kc/s 3995KC/s jlwhy06..j16kbkk
0g 0:00:09:17 3/3 0g/s 5156Kp/s 5156Kc/s 5156KC/s hgc35aw..hgcl047
0g 0:00:09:19 3/3 0g/s 5168Kp/s 5168Kc/s 5168KC/s sp1nkl8r..spcasig2
0g 0:00:09:20 3/3 0g/s 5175Kp/s 5175Kc/s 5175KC/s plythy4l..plzemugs
0g 0:00:09:22 3/3 0g/s 5186Kp/s 5186Kc/s 5186KC/s anit19me..anjj12jr
0g 0:00:09:23 3/3 0g/s 5192Kp/s 5192Kc/s 5192KC/s l0nellly..l0cchow3
y4u8it (root)
1g 0:00:09:47 3/3 0.001700g/s 5385Kp/s 5385Kc/s 5385KC/s y4hf1t..y488fx
Shell
복사
•
y4u8it root 비밀번호 크랙에 성공하였으며 해당 정보로 로그인 시도
1432 root 2160 S udhcpc -b -i eth0 -s /etc/scripts/wired_dns.sh
1442 root 0 SW< [hci0]
1445 root 2096 S hciattach /dev/ttymxc0 any 115200 noflow
1451 root 2160 S /bin/sh /etc/scripts/system_data.sh
1452 root 128m S /usr/bin/bonanza
1453 root 2240 S -sh
2770 root 2240 S -sh
3697 root 0 SW [kworker/0:1]
4279 root 4904 S /sbin/wpa_supplicant -i eth1 -c /mnt/data/scripts/wp
4280 root 2160 S udhcpc -b -T 6 -i eth1 -s /etc/scripts/wireless_dns.
4312 root 2028 S sleep 1
4321 root 2028 S sleep 2
4322 root 2240 R ps
[192.168.50.8]~# id
Shell
복사
•
로그인 성공
•
ps로 실행되는 프로세스들은 inittab에서 확인한 프로세스들
•
/usr/bin/bonanza 가 메인 프로세스
#!/bin/sh
PARTITION_uboot=/dev/mmcblk0p1
PARTITION_uboot_env=/dev/mmcblk0p2
PARTITION_kernel1=/dev/mmcblk0p3
PARTITION_extension=/dev/mmcblk0p4
PARTITION_kernel2=/dev/mmcblk0p5
PARTITION_data=/dev/mmcblk0p6
LED_REF=/sys/class/backlight/pwm-backlight.0
DATA=/mnt/data
MAIN_APP=/usr/bin/bonanza
BT_CONFIG=${DATA}/scripts/BT.cfg
...
#--- Prep and mount filesystems -------------------------------------------
# echo "!!!Prep and mount filesystems" > /dev/console
if (sfdisk -l /dev/mmcblk0 2>&1 | grep -q "unrecognized partition table"); then
echo "!!!ERROR: File system corrupted... reinitializing" > /dev/console
/etc/scripts/partition_disk.sh /etc/scripts/partition.map
fi
e2fsck -y /dev/mmcblk0p6
usleep 250000
mount -a
#--- Set Backlight to full brightness -------------------------------------
# echo "!!!Set Backlight to full brightness" > /dev/console
cat ${LED_REF}/max_brightness > ${LED_REF}/brightness
#--- Mount physical file systems --------------------------------(portable)
# echo "!!!Remount physical file systems" > /dev/console
mount -a
# echo "================================================================ BOOT ================================================================" >> /mnt/data/bonanza.log
mkdir -p ${DATA}/database ${DATA}/scripts ${DATA}/helpfiles ${DATA}/saved/demo ${DATA}/photos/demo ${DATA}/voice/demo
...
Shell
복사
/etc/rc.sh
•
rc.sh 파일을 보면 emmc 파티션과 관련된 구조 확인 가능
•
/dev/mmcblk0p1 ~ /dev/mmcblk0p6 까지 순서대로 다음의 구조로 되어 있음
◦
PARTITION_uboot=/dev/mmcblk0p1
◦
PARTITION_uboot_env=/dev/mmcblk0p2
◦
PARTITION_kernel1=/dev/mmcblk0p3
◦
PARTITION_extension=/dev/mmcblk0p4
◦
PARTITION_kernel2=/dev/mmcblk0p5
◦
PARTITION_data=/dev/mmcblk0p6
•
총 6개의 emmc 파티션으로 구성되어 있으며 해당 파티션 마운트와 관련된 레이아웃은 /etc/scripts/partition.map 파일에서 확인 가능
# partition table of /dev/mmcblk0
unit: sectors
#---- U-Boot: 16 blocks for partition table -------------------------------------------------------
#/dev/mmcblk0 : start=0, size=16 <---Primary partition table [dummy line]
/dev/mmcblk0p1 : start=16, size=4080, Id=DA
#---- U-Boot env ----------------------------------------------------------------------------------
/dev/mmcblk0p2 : start=4096, size=512, Id=DA
#---- Kernel 1 ------------------------------------------------------------------------------------
/dev/mmcblk0p3 : start=4608, size=65536, Id=F0
#---- extended container --------------------------------------------------------------------------
/dev/mmcblk0p4 : start=70144, size=7663100, Id=85
#---- Kernel 2 ------------------------------------------------------------------------------------
#/dev/mmcblk0 : start=70144, size=16 <---Extended partition table [dummy line]
#/dev/mmcblk0 : start=70160, size=496 <---Erase block (128K) alignment [dummy line]
/dev/mmcblk0p5 : start=70656, size=65536, Id=F0
#---- Data ----------------------------------------------------------------------------------------
/dev/mmcblk0p6 : start=136192, size=7597052, Id=83
Shell
복사
/etc/scripts/partition.map
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
devpts /dev/pts devpts mode=0620,gid=5 0 0
/dev/mmcblk0p6 /mnt/data ext4 data=journal,noatime 0 0
Shell
복사
/etc/fstab
•
partition.map을 보면 파티션 3번이 첫 번째 커널(uart로 추출한 영역)을 뜻하며 파티션 5번이 백업용 두 번째 커널인걸 확인 가능
•
또한 6번파티션은 fstab 파일을 보면 /mnt/data에 마운트되며 rc.sh를 보면 사용자 정보를 주로 저장하는 파티션인 것을 알 수 있음
mkdir -p ${DATA}/database ${DATA}/scripts ${DATA}/helpfiles ${DATA}/saved/demo ${DATA}/photos/demo ${DATA}/voice/demo
Shell
복사
# mount
rootfs on / type rootfs (rw)
sysfs on /sys type sysfs (rw,relatime)
proc on /proc type proc (rw,relatime)
/dev on /dev type tmpfs (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,gid=5,mode=620)
/dev/mmcblk0p6 on /mnt/data type ext4 (rw,noatime,user_xattr,barrier=1,nodelalloc,data=journal)
Shell
복사
•
uart를 통해 내부 쉘에 접근 가능하기 때문에 mount 명령어로 실제 마운트되어있는 정보를 확인하면 /mnt/data에 6번째 emmc 파티션이 마운트가 실제 되어있음
•
rootfs 은 rw 가능한 상태
3.1 분석 요약
•
해당 CaptionCall 장비는 4G 크기의 eMMC에 rootfs이 마운트되는것이 아닌 실제 커널 내부에 존재하는 ramdisk를 main rootfs로 사용
•
eMMC는 /mnt/data에 저장되어 유저 정보와 관련된 데이터가 주로 저장됨
/mnt/data# ls
bin bonanza.log.0 helpfiles photos scripts
bonanza.log database lost+found saved voice
Shell
복사
•
따라서 재부팅을 시키지 않는다면 inittab을 수정하여 Doom 관련 작업을 진행하면 되지만, 재부팅이 된다면 다 사라진다
•
어짜피 커널이 현재 2개이므로 첫 번째 커널 이미지는 Doom 작업을 위한 이미지로 수정하여 기본 부팅 이미지를 Doom 이미지로 변경해보자(정상부팅은 Uboot 콘솔에서 run bootflash2 명령으로 부팅 가능하기 때문)
4. ramdisk 수정
•
binwalk를 통해 ramdisk 추출이 가능하지만, ramdisk에서 inittab을 수정하려면 추출한 ramdisk를 압축하여 uImage로 만드는 과정이 필요하다
•
따라서 binwalk가 아닌 uImage의 구조를 파악하여 직접 추출할 것이다
•
uImage는 커널 이미지에서 uboot를 위한 64바이트의 헤더가 추가된 것으로 u-boot에서 사용하는 압축된 커널을 의미한다.
•
실제 추출한 uImage 를 보면 위 0x40 바이트의 영역이 uImage 헤더 영역이다
•
uImage 헤더 구조는 다음과 같다
•
헤더에서 전체 커널 이미지 크기를 확인할 수 있다
Ramdisk 추출 과정
•
uImage 헤더에 전체 커널 이미지 크기가 나와있기 때문에 해당 size를 기준으로 위 흐름대로 Ramdisk 추출이 가능하다
•
참고로 추출한 uImage의 헤더+0x1f 오프셋이 커널 이미지의 압축 방식을 의미하는데, 해당 필드의 경우 현재 0으로 압축되어 있지 않다고 표시된다
•
하지만 이는 실제로 ramdisk가 압축되어 있지 않다는 의미가 아니며 uboot에서는 해당 필드를 확인하여 압축 필드가 0일경우 커널 이미지를 메모리에 그대로 로드한다
•
그 후 커널에서 piggy 코드를 활용하여 GZIP 압축 데이터를 해제하여 Ramdisk를 뽑아 낸다
•
일반적으로 커널 이미지와 Ramdisk를 따로 분류하여 구성되지만 이런식의 커널 이미지 안에 Ramdisk가 통합되어 있는 경우도 임베디드 시스템에서는 흔히 볼 수 있다
4.1 uImage 헤더 제거
def extract_kernel_from_uimage(uimage_path):
"""
Remove uImage header
"""
with open(uimage_path, 'rb') as f:
# uImage header is 64 bytes
header = f.read(64)
# Extract image size from header (4 bytes, big endian)
image_size = struct.unpack('>I', header[12:16])[0]
print_debug(f"Extracted image size: {image_size:x} bytes")
# Read image size from header
kernel_image = f.read(image_size)
return kernel_image
Python
복사
•
2에서 추출한 전체 커널 이미지에서 64바이트의 헤더를 제거한다
•
이때 +12 offset에 존재하는 size 필드를 파싱하여 헤더를 제외한 전체 커널 이미지 크기 만큼 데이터를 읽는다
4.2 gzip으로 압축된 데이터 검색 후 압축 해제(gzip)
def find_and_decompress_kernel(kernel_image, output_path):
"""
Find gzip magic bytes and try to decompress
"""
# gzip magic bytes (1F 8B 08)
GZIP_MAGIC = b'\x1f\x8b\x08'
# Find all gzip magic bytes in kernel image
offsets = []
pos = 0
while True:
pos = kernel_image.find(GZIP_MAGIC, pos)
if pos == -1:
break
offsets.append(pos)
pos += 1
print_debug(f"Found {len(offsets)} gzip headers")
# Try to decompress at each offset
for offset in offsets:
print_debug(f"Try to decompress at offset 0x{offset:x}...")
try:
# Extract data from offset to end of file
compressed_data = kernel_image[offset:]
f_tmp = open(output_path,'wb')
f_tmp.write(compressed_data)
f_tmp.close()
# Run gunzip command and capture output
result = run_command(['gunzip', '-f', output_path])
if 'decompression OK' not in result:
print_debug(f"Failed to decompress at offset 0x{offset:x}.. Try another offset..")
f_tmp.close()
continue
else:
print_debug(f"Successfully decompressed kernel! (offset: 0x{offset:x})")
f_tmp.close()
return True
except Exception as e:
print_debug(f"Failed to decompress at offset 0x{offset:x}: {e}")
# f_tmp.close()
sys.exit(1)
return None
Python
복사
•
헤더를 제거한 이미지에서 gzip magic 바이트를 찾는다(\x1f\x8b\x08)
•
해당 시그니처가 한 개 이상 나올 수 있으므로 검색된 offset 기준으로 파일 끝까지 압축 해제를 시도한다
•
통상 guzip 압축 해제가 성공하면 “decompression OK” 라는 문자열이 나오므로 guzip 압축 해제 후 해당 문자열이 출력되는 오프셋을 찾아 이미지를 압축 해제한다
4.3 ramdisk 추출
.section .init.ramfs,"a"
__irf_start:
.incbin "usr/initramfs_inc_data"
__irf_end:
.section .init.ramfs.info,"a"
.globl __initramfs_size
__initramfs_size:
#ifdef CONFIG_64BIT
.quad __irf_end - __irf_start
#else
.long __irf_end - __irf_start
#endif
Shell
복사
•
ramdisk는 커널에 올라갈 때 메모리에 올라가는 시작과 끝 주소를 의미하는 심볼이다
◦
__irf_start : ramdisk 이미지가 메모리에 로드되는 가상주소
◦
__irf_end : ramdisk 이미지가 메모리에 로드되는 끝 가상주소
•
해당 주소는 메모리에 로드될 때의 가상 주소이며 base addr를 기준으로 빼야 실제 시작-끝 offset을 계산 할 수 있다
•
gzip으로 압축 해제한 이미지를 ‘vmlinux-to-elf’ 툴을 사용하여 elf 형식으로 변환하면 해당 정보를 확인 가능하다
./kernel/decompressed_kernel.bin.elf: file format elf32-little
./kernel/decompressed_kernel.bin.elf
architecture: UNKNOWN!, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x800085a8
Program Header:
# Kernel Code .. data .. etc ..
LOAD off 0x00000124 vaddr 0x80008000 paddr 0x80008000 align 2**0
filesz 0x0101cb84 memsz 0x0101cb84 flags rwx
# For BSS
LOAD off 0x0101cca8 vaddr 0x81024b84 paddr 0x81024b84 align 2**0
filesz 0x00000000 memsz 0x00100000 flags rwx
Sections:
Idx Name Size VMA LMA File off Algn
0 .kernel 0101cb84 80008000 80008000 00000124 2**0
CONTENTS, ALLOC, LOAD, CODE
1 .bss 00100000 81024b84 81024b84 0101cca8 2**0
ALLOC, CODE
SYMBOL TABLE:
80008100 l F .kernel 00000000 __enable_mmu_loc
8000810c l F .kernel 00000000 __vet_atags
80008144 l F .kernel 00000000 __mmap_switched
...
80027af0 l F .kernel 00000000 __irf_start
80b14960 l F .kernel 00000000 __irf_end
Shell
복사
objdump -x “vmlinux-to-elf 로 변환한 이미지” 결과
•
vmlinux-to-elf 툴로 변환한 이미지를 objdump로 심볼 정보를 확인 가능하다
•
vaddr 인 0x80008000이 가상 주소의 base addr 이며, __irf_start, __irf_end가 각각 0x80027af0, 0x80b14960 이다. 따라서 실제 ramdisk 영역의 크기와 실제 ramdisk 시작 offset은 다음과 같다
rmadisk size: __irf_end(0x80b14960) - __irf_start(0x80027af0) = 0xAECE70
ramdisk start offset : __irf_start(0x80027af0) - vaddr(0x80008000)
Shell
복사
def find_ramdisk_from_decompressed_kernel(decompressed_kernel):
global ramdisk_start, ramdisk_len
result = run_command(['vmlinux-to-elf', decompressed_kernel, decompressed_kernel + '.elf'])
if result is None:
print_debug(f"Error: vmlinux-to-elf failed")
sys.exit(1)
result = run_command(['objdump', '-x', decompressed_kernel + '.elf'])
if result is None:
print_debug(f"Error: objdump failed")
sys.exit(1)
# Find base address. vaddr is virtual address base.
vaddr_pattern = 'vaddr '
paddr_pattern = ' paddr'
vaddr_pos = result.find(vaddr_pattern, 1)
paddr_pos = result.find(paddr_pattern, 1)
if vaddr_pos != -1 or paddr_pos != -1:
vaddr = result[vaddr_pos+6:paddr_pos]
print_debug(f"Found vaddr: {vaddr}")
else:
print_debug(f"Cannot find vaddr")
sys.exit(1)
# Find __irf_stat, __irf_end -> ramdisk area
start_pattern = '__irf_start'
end_pattern = '__irf_end'
start_pos = result.find(start_pattern, 1)
end_pos = result.find(end_pattern, 1)
if start_pos != -1 and end_pos != -1:
start_addr = result[result[:start_pos].rfind('\n')+1:result[:start_pos].rfind('\n')+1+8]
end_addr = result[result[:end_pos].rfind('\n')+1:result[:end_pos].rfind('\n')+1+8]
print_debug(f"Found __irf_stat: 0x{start_addr}, __irf_end: 0x{end_addr}")
else:
print_debug(f"Cannot find __irf_stat or __irf_end")
sys.exit(1)
# Calculate ramdisk size
ramdisk_start = int(start_addr, 16) - int(vaddr, 16)
ramdisk_len = int(end_addr, 16) - int(start_addr, 16)
print_debug(f"Ramdisk start: 0x{ramdisk_start:x}, size: 0x{ramdisk_len:x}")
...
Python
복사
•
위에서 설명한 과정을 코드로 구현하면 다음과 같다
•
이 후 실제 gzip 압축 해제한 이미지에서 해당 오프셋 ~ 사이즈 만큼 짤라서 ramdisk 추출 작업을 진행한다
...
result = run_command(['xz', '-d', decompressed_kernel + '-ramdisk.xz'])
if result is None:
print_debug(f"Error: xz failed")
sys.exit(1)
os.system("mkdir -p cpio.out 1>/dev/null")
result = run_command(f'cpio -idmv --directory=./cpio.out <{decompressed_kernel}-ramdisk', True)
...
Python
복사
•
ramdisk는 xz로 또 압축되어 있으며 cpio 아카이브 형식이기 때문에 이에 맞춰 압축 해제 과정을 거치면 최종적으로 binwalk에서 추출 가능한 initramfs를 추출 할 수 있다
cd cpio.out
$ ls
SoftFrame.cfg bin etc lib proc sys usr bcm4330b2.hcd dev init mnt sbin tmp
Shell
복사
•
최종적으로 추출한 initramfs 를 확인할 수 있다
...
result = run_command(f"cd cpio.out; find . 2>/dev/null | cpio -H newc -R root:root -o | xz --check=crc32 --lzma2=dict=1MiB > {decompressed_kernel}-ramdisk_repacked.xz",True)
...
comment = "Linux-BSP10.14-CC2.1.8.13"
result = run_command(f"mkimage -A arm -T kernel -C none -a 0x10008000 -e 0x10008000 -n '{comment}' -d {decompressed_kernel} {decompressed_kernel}.uimg", True)
...
Python
복사
•
이제 inittab을 수정하고, 반대의 작업을 수행하면 된다
•
cpio 아카이브로 xz 방식으로 압축 후, mkimage 를 활용하여 최종적으로 uImage를 만들면 된다
•
이때 start addr와 entry addr는 부팅 로그에서 확인한 0x10008000 주소로 설정하였다
•
ramdisk에 수정해야할 내용은 5절에서 설명하겠다
5. Doom 포팅
•
현재 동작 중인 장비의 커널 버전은 Linux 3.0.35-2213-g7e8c89c arm 32bit 아키텍처이다
•
debootstrap jessie(다른 버전은 호환성이 안맞음)를 활용하여 chroot 환경을 구성한 뒤, 장비에 올려 Doom을 실행할 예정
•
필요 패키지 설치
# debootstrap pkg
sudo apt install -y debootstrap qemu-user-static binfmt-support
Shell
복사
•
Debian Jessie armel chroot 생성 및 chroot 실행
# create directory
$ mkdir debian-jessie
$ cd debian-jessie
# run debootstrap (with --foreign)
$ sudo debootstrap --arch=armel --foreign jessie debian-jessie http://archive.debian.org/debian
# cp qemu-arm-static
$ sudo cp /usr/bin/qemu-arm-static debian-jessie/usr/bin/
# Access chroot and run second-stage
$ sudo chroot debian-jessie /bin/bash
$ /debootstrap/debootstrap --second-stage
Shell
복사
•
chroot 내부에서 Doom 동작을 위한 필수 패키지 설치(+ 디버깅을 위한 ssh 설치)
$ apt install locales dialog icewm
$ apt install xserver-xorg-input-evdev mesa-utils
$ apt install xorg xterm xinit xserver-xorg-video-fbdev
$ apt install prboom-plus doom-wad-shareware
$ apt install openssh-server
$ apt install xdotool # for custom keyboad mapping -> dialpad
Shell
복사
•
장비의 화면에 게임을 띄우기 위해선 크게 3가지 작업이 필요하다
◦
그래픽 출력 처리 → fbdev 사용
◦
창 관리, 그래픽 표시, 입력 처리 장치 등의 기본 기능 제공하며 fbdev 같은 그래픽 드라이버를 사용하여 화면에 출력 → X11 사용
◦
X11 위에서 동작하는 경량 윈도우 매니저로 사용자 인터페이스 제공 → IceWM 사용
5.1 X11 설정
•
X11 설정 파일 추가
Section "Device" # Device section: Defines graphics hardware
Identifier "BonanzaLCD" # Unique name for this device
Driver "fbdev" # Uses framebuffer device driver
Option "fbdev" "/dev/fb0" # Path to framebuffer device file
EndSection
Section "InputDevice"
Identifier "BonanzaTS"
Driver "evdev"
Option "Device" "/dev/input/event2" # event2 -> touch event
Option "SendCoreEvents" "yes"
Option "SwapAxes" "1"
Option "Calibration" "0 1100 50 350"
Option "Absolute" "true"
EndSection
Section "Monitor"
Identifier "BonanzaMonitor"
VendorName "CaptionCall"
ModelName "Bonanza"
Mode "1024x600"
# D: 64.998 MHz, H: 48.362 kHz, V: 75.802 Hz
DotClock 64.999
HTimings 1024 1064 1124 1344 # for mouse x,y mapping
VTimings 600 603 610 620
Flags "-HSync" "-VSync"
EndMode
EdnSection
Section "Screen" # Screen section: Links device and monitor
Identifier "Screen0" # Unique name for this screen configuration
Device "BonanzaLCD" # References the Device section
Monitor "BonanzaMonitor" # References the Monitor section
DefaultDepth 16 # Default color depth (16-bit)
SubSection "Display" # Display subsection for specific depth
Depth 16 # Color depth (16-bit)
Modes "1024x600" # Available resolution modes
EndSubSection
EndSection
Section "ServerLayout" # Server layout: Puts everything together
Identifier "Layout0" # Unique name for this layout
Screen "Screen0" # References the Screen section
InputDevice "BonanzaTS" "CorePointer" # Sets the touchscreen as main pointing device
EndSection
Section "ServerFlags" # X server flags: Global server options
Option "AllowEmptyInput" "false" # Disables automatic input device addition
Option "AutoAddDevices" "false" # Prevents automatic device configuration
Option "AutoEnableDevices" "false" # Prevents automatic device enabling
EndSection
Shell
복사
/etc/X11/xorg.conf
5.2 ssh 접속 설정(디버깅 목적)
# In chroot. Setting root passwd
$ passwd
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
# Permit root login ssh -> /etc/ssh/sshd_config
...
PermitRootLogin yes
...
Python
복사
•
장비 내부에서 chroot로 Doom 동작 시 발생 가능한 에러를 잡기 위해 디버깅 환경 세팅(ssh로 접속 가능하게)
6. 구동 초기화 스크립트
•
Doom을 돌리기 위한 debootstrap 내부 설정은 완료하였다
•
이제 수정한 커널에서 inittab을 수정하여 3절에서 확인했던 /usr/bin/bonanza 메인 바이너리가 동작하지 않고 설정한 chroot 환경에서의 doom이 돌아가게 수정하면 된다
•
initramfs 수정사항은 다음과 같다
# This is run first except when booting in single-user mode.
::sysinit:/etc/rc.sh
::restart:/sbin/init
::respawn:/etc/scripts/system_data.sh
# delete bonaza script
# Start a shell on the terminal [Must be last 'respawn' entry to avoid ^C problem]
::respawn:/sbin/getty 115200 ttymxc1 vt100 -w -n -i
# Stuff to do before rebooting
::shutdown:/etc/shutdown.sh
Shell
복사
/etc/inittab
•
/usr/bin/bonaza 바이너리 실행 부분 제거
...
#--- Do specific actions based on dev/stable app versions -------(portable)
# echo "!!!Start additional services" > /dev/console
if [ -f "${DATA}/scripts/run-telnetd" ] || [ $(( ${build_type} % 2 )) -eq 1 ]; then
telnetd
if $(grep -q '^[0-9. ]*workstation.corp.srelay.com' /etc/hosts); then
(
sleep 5
mkdir -p /mnt/nfs
if ! $(mount -o nolock -t nfs workstation.corp.srelay.com:/srv/nfs /mnt/nfs); then
echo "!!!ERROR: NFS mount failed" > /dev/console
fi
)&
fi
fi
/bin/busybox telnetd &
/mnt/data/etc/init.sh
Shell
복사
/etc/rc.sh
•
기존의 /etc/rc.sh 스크립트에 telnetd과 chroot 환경에서 doom을 돌리기 위한 스크립트 추가
•
/mnt/data이 현재 emmc의 /dev/mmcblk0p6 파티션에 마운트되어 있기 때문에 /mnt/data/etc/init.sh 를 넣어둔다
#!/bin/bash
# Chroot init
mount --bind /dev/ /mnt/data/debian-jessie/dev
mount --bind /dev/pts /mnt/data/debian-jessie/dev/pts
mount --bind /dev/shm /mnt/data/debian-jessie/dev/shm
mount -t sysfs sysfs /mnt/data/debian-jessie/sys
mount -t proc proc /mnt/data/debian-jessie/proc
cp /etc/resolv.conf /mnt/data/debian-jessie/etc/resolv.conf
chroot /mnt/data/debian-jessie /etc/rc.local
Shell
복사
/mnt/data/etc/init.sh
•
/mnt/data/etc/init.sh 내부에서 chroot 구동을 위한 마운트 작업과 실제 chroot 환경에서도 초기에 실행시킬 rc.local 파일을 명시한다
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
# Run X and other utils at chroot start
export HOME=/root
export DISPLAY=:0
# Clean out old /tmp
rm -rf /tmp
mkdir /tmp
chmod 1777 /tmp
startx >/dev/null &
/usr/local/bin/keypad2keyboard &
sleep 2
/usr/local/bin/keypad2shutgun &
/etc/init.d/ssh start
sleep infinity
exit 0
Shell
복사
/mnt/data/debian-jessie/etc/rc.local
•
startx를 실행하여 X를 실행시킨다. 이때 X에 대한 초기 설정파일에 icewm과 prdoom-plus를 구동시키다
#!/bin/sh
icewm &
sleep 3
exec /usr/games/prboom-plus /usr/share/games/doom/freedoom2.wad -warp 1 1 -width 1024 -height 600 -fullscreen
Shell
복사
/mnt/data/debian-jessie/root/.xinitrc
•
startx 실행시 X가 실행되면서 .xinitrc 설정파일을 읽어 해당 스크립트를 실행시킨다
•
이때 X 위에서 구도될 icewm 윈도우 매니저를 실행시키고 그 위에서 doom 엔진을 실행시킨다. 인자는 실행시킴 wad 형식의 doom 게임 파일이다
•
warp → 게임 난이도 지정. 이걸 줘야 데모 모드로 동작하지 않고 바로 게임이 실행된다
•
가로 세로 크기는 현재 장비 화면에 맞는 크기를 입력한다
•
마지막으로 fullscreen 옵션을 줘야 전체 화면으로 동작한다
7. Keyboad 매핑 스크립트 추가
•
ipphone의 키패드를 표준 키보드 입력으로 변환하기 위해선 추가적인 작업이 필요하다
•
이는 xdotool 를 활용하여 키 이벤트를 수동으로 변환하면 된다
•
장비 내부에서 evtest 명령을 사용하여 실시간으로 입력 장치의 이벤트를 확인 가능하다
cd /proc/bus/input
# evtest
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0: gpio-keys
/dev/input/event1: imx-keypad
/dev/input/event2: ft5x06
/dev/input/event3: rotary-encoder
/dev/input/event4: bonanza-ringer
Select the device event number [0-4]:
Shell
복사
•
키보드 이벤트에 대한 값을 확인 해야 하기 때문에 1번을 입력한다
Select the device event number [0-4]: 1
Input driver version is 1.0.1
Input device ID: bus 0x19 vendor 0x0 product 0x0 version 0x0
Input device name: "imx-keypad"
Supported events:
Event type 0 (EV_SYN)
Event type 1 (EV_KEY)
Event code 512 (KEY_NUMERIC_0)
Event code 513 (KEY_NUMERIC_1)
Event code 514 (KEY_NUMERIC_2)
Event code 515 (KEY_NUMERIC_3)
Event code 516 (KEY_NUMERIC_4)
Event code 517 (KEY_NUMERIC_5)
Event code 518 (KEY_NUMERIC_6)
Event code 519 (KEY_NUMERIC_7)
Event code 520 (KEY_NUMERIC_8)
Event code 521 (KEY_NUMERIC_9)
Event code 522 (KEY_NUMERIC_STAR)
Event code 523 (KEY_NUMERIC_POUND)
Event type 4 (EV_MSC)
Event code 4 (MSC_SCAN)
Properties:
Testing ... (interrupt to exit)
# ipphone "0" key press ..
Event: time 19682.620050, type 4 (EV_MSC), code 4 (MSC_SCAN), value 21
Event: time 19682.620054, type 1 (EV_KEY), code 512 (KEY_NUMERIC_0), value 1
Event: time 19682.620056, -------------- SYN_REPORT ------------
# ipphone "0" key press off ..
Event: time 19683.966049, type 4 (EV_MSC), code 4 (MSC_SCAN), value 21
Event: time 19683.966053, type 1 (EV_KEY), code 512 (KEY_NUMERIC_0), value 0
Event: time 19683.966054, -------------- SYN_REPORT ------------
Shell
복사
•
다이얼 패드에서 ‘0’ 을 눌렀을 때
◦
type 1 (EV_KEY), code 512 (KEY_NUMERIC_0), value 1
•
누른 손을 땠을 때
◦
type 1 (EV_KEY), code 512 (KEY_NUMERIC_0), value 0
•
결론적으로 ‘0’ 은 code 512로 매핑되어 있으며 눌렀을 때의 값을 value=1 땠을 때의 값을 value=0으로 처리하면 된다.
•
이런식으로 1 ~ 9, * , # 에 대한 이벤트 code값을 식별하고, xdotool 스크립트를 작성 후 실행시키면 ipphone에서의 다이얼 패드를 표준 키보드 입력으로 매핑하여 게임에서 사용할 수 있다
◦
수화기 버튼은 event0으로 code 169를 가진다.(총 쏘는 키로 지정)
#!/bin/bash
# Yes, it's a hack; but evdev is buggy and we need this...
export HOME=/root
export DISPLAY=:0
device='/dev/input/event1'
key_0d='*type 1 (EV_KEY), code 512 (KEY_NUMERIC_0), value 1*'
key_1d='*type 1 (EV_KEY), code 513 (KEY_NUMERIC_1), value 1*'
key_2d='*type 1 (EV_KEY), code 514 (KEY_NUMERIC_2), value 1*'
key_3d='*type 1 (EV_KEY), code 515 (KEY_NUMERIC_3), value 1*'
key_4d='*type 1 (EV_KEY), code 516 (KEY_NUMERIC_4), value 1*'
key_5d='*type 1 (EV_KEY), code 517 (KEY_NUMERIC_5), value 1*'
key_6d='*type 1 (EV_KEY), code 518 (KEY_NUMERIC_6), value 1*'
key_7d='*type 1 (EV_KEY), code 519 (KEY_NUMERIC_7), value 1*'
key_8d='*type 1 (EV_KEY), code 520 (KEY_NUMERIC_8), value 1*'
key_9d='*type 1 (EV_KEY), code 521 (KEY_NUMERIC_9), value 1*'
key_sd='*type 1 (EV_KEY), code 522 (KEY_NUMERIC_STAR), value 1*'
key_pd='*type 1 (EV_KEY), code 523 (KEY_NUMERIC_POUND), value 1*'
key_0u='*type 1 (EV_KEY), code 512 (KEY_NUMERIC_0), value 0*'
key_1u='*type 1 (EV_KEY), code 513 (KEY_NUMERIC_1), value 0*'
key_2u='*type 1 (EV_KEY), code 514 (KEY_NUMERIC_2), value 0*'
key_3u='*type 1 (EV_KEY), code 515 (KEY_NUMERIC_3), value 0*'
key_4u='*type 1 (EV_KEY), code 516 (KEY_NUMERIC_4), value 0*'
key_5u='*type 1 (EV_KEY), code 517 (KEY_NUMERIC_5), value 0*'
key_6u='*type 1 (EV_KEY), code 518 (KEY_NUMERIC_6), value 0*'
key_7u='*type 1 (EV_KEY), code 519 (KEY_NUMERIC_7), value 0*'
key_8u='*type 1 (EV_KEY), code 520 (KEY_NUMERIC_8), value 0*'
key_9u='*type 1 (EV_KEY), code 521 (KEY_NUMERIC_9), value 0*'
key_su='*type 1 (EV_KEY), code 522 (KEY_NUMERIC_STAR), value 0*'
key_pu='*type 1 (EV_KEY), code 523 (KEY_NUMERIC_POUND), value 0*'
evtest "$device" | while read line; do
case $line in
($key_0d) xdotool keydown 0 ;;
($key_0u) xdotool keyup 0 ;;
($key_1d) xdotool keydown Left ;;
($key_1u) xdotool keyup Left ;;
($key_2d) xdotool keydown 2 ;;
($key_2u) xdotool keyup 2 ;;
($key_3d) xdotool keydown Right ;;
($key_3u) xdotool keyup Right ;;
($key_4d) xdotool keydown 4 ;;
($key_4u) xdotool keyup 4 ;;
($key_5d) xdotool keydown w ;;
($key_5u) xdotool keyup w ;;
($key_6d) xdotool keydown 6;;
($key_6u) xdotool keyup 6 ;;
($key_7d) xdotool keydown a;;
($key_7u) xdotool keyup a;;
($key_8d) xdotool keydown s;;
($key_8u) xdotool keyup s;;
($key_9d) xdotool keydown d ;;
($key_9u) xdotool keyup d ;;
($key_sd) xdotool keydown shift+8;;
($key_su) xdotool keyup shift+8 ;;
($key_pd) xdotool keydown shift+3 ;;
($key_pu) xdotool keyup shift+3 ;;
esac
done
Shell
복사
/usr/local/bin/keypad2keyboard
export HOME=/root
export DISPLAY=:0
device='/dev/input/event0'
key_0d='*type 1 (EV_KEY), code 169 (KEY_PHONE), value 1*'
key_0u='*type 1 (EV_KEY), code 169 (KEY_PHONE), value 0*'
evtest "$device" | while read line; do
case $line in
($key_0d) xdotool keydown Control_L ;;
($key_0u) xdotool keyup Control_L ;;
esac
done
Shell
복사
/usr/local/bin/keypad2shutgun
•
xdotool을 활용하여 다음의 키 매핑을 수행하였다
◦
전진(5), 후진(8), 좌측 이동(7), 우측 이동(9)
◦
좌측 방향 전환(1), 우측 방향 전환(2)
◦
총 쏘기(수화기 버튼)
8. 마무리
•
최종적으로 실행되는 흐름은 다음과 같다
1.
inittab → bonanza 실행 X
2.
/etc/rc.sh 실행
...
- /bin/busybox telnetd &
- /mnt/data/etc/init.sh
Shell
복사
3.
/mnt/data/etc/init.sh 실행
mount --bind /dev/ /mnt/data/debian-jessie/dev
mount --bind /dev/pts /mnt/data/debian-jessie/dev/pts
mount --bind /dev/shm /mnt/data/debian-jessie/dev/shm
mount -t sysfs sysfs /mnt/data/debian-jessie/sys
mount -t proc proc /mnt/data/debian-jessie/proc
cp /etc/resolv.conf /mnt/data/debian-jessie/etc/resolv.conf
chroot /mnt/data/debian-jessie /etc/rc.local
Shell
복사
4.
chroot 환경의 /etc/rc.local 실행
...
startx >/dev/null &
/usr/local/bin/keypad2keyboard &
sleep 2
/usr/local/bin/keypad2shutgun &
/etc/init.d/ssh start # for debugging
sleep infinity
exit 0
Shell
복사
5.
startx가 실행될 때 초기화 스크립트인 /root/.xinitrc 실행(자동
icewm &
sleep 3
/usr/games/prboom-plus -iwad /usr/share/games/doom/freedoom1.wad -warp 1 1 -width 1024 -height 600 -fullscreen
Shell
복사
6.
게임 실행 시작
•
(한손으로 촬영하느라 한 손으로 플레이 하는 ..)