Search

임베디드 장비에 Doom 올리기

Category
임베디드
Column
2025/04/22 14:17
Tags
study

1. 개요

이전부터 임베디드 장비에 Doom을 올려보고 싶었다.
단순 취약점을 찾는게 아닌 게임을 올리려면 수정 해야 할 여러가지들이 존재했으며, 특히 펌웨어를 수정하여 게임을 올리는 과정에서 펌웨어 구조를 파악하는데 좋은 계기가 될 것 같았다.
여러가지 자료를 찾아보는 중 IPPhone에 둠을 올리는 자료가 존재했으며, 동일 제품을 구매하여 Doom을 올려보기로 하였다. 설명이 매우 친절하게 나와있기 때문에 따라 해보면서 설명되어 있지 않은 부분은 추가적으로 작성해가며 정리하였다.
성공을 한다면 이제 응용을 들어가는 걸로 ..

2. 펌웨어 추출

우측 하단에 보이는 5pin → UART 핀
좌측부터 : GND, X, X, RX, TX
UART 연결 시 부팅 로그가 출력되며 마지막에 로그인 창이 뜸
전체 부팅 로그
captioncall_2025-04-08_오후 10_49_37.log
39.6KB
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는 SDIN7DP2-4G 를 사용하며 4G의 크기를 갖음.[데이터시트]
따라서 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.
게임 실행 시작
(한손으로 촬영하느라 한 손으로 플레이 하는 ..)

참고