Post

Pwnable.kr 「leg (Toddler's Bottle)」Writeup

ARMにおけるアセンブリの理解 (ARM/Thumb モード)

Pwnable.kr 「leg (Toddler's Bottle)」Writeup

pwnable_kr-leg

Summary

本問は,ARMにおけるアセンブリの理解 (ARM/Thumb モード) を試す問題です.

  • Category: Pwn
  • Description: Daddy told me I should study ARM architecture.
    • But I know Intel architecture and it should be similar.
    • Why bother to study ARM?
  • Tools & TechStack:
    • C
    • asm
  • Release: N/A

階層構造

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── Dockerfile
├── ld
├── ld.c
├── leg.asm
├── leg.c
├── readme
├── rootfs.img
├── run.sh
├── super.pl
└── zImage

1 directory, 10 files

ソースコードの解析

key1() + key2() + key(3) の結果が分かれば,key にその値を入れてif文内に入れるため,配布されている leg.asm と対応させながらアセンブリを読みます.

leg.c

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
#include <stdio.h>
#include <fcntl.h>

int key1()
{
	asm("mov r3, pc\n");
}

int key2()
{
	asm(
		"push	{r6}\n"
		"add	r6, pc, $1\n"
		"bx	r6\n"
		".code   16\n"
		"mov	r3, pc\n"
		"add	r3, $0x4\n"
		"push	{r3}\n"
		"pop	{pc}\n"
		".code	32\n"
		"pop	{r6}\n");
}

int key3()
{
	asm("mov r3, lr\n");
}

int main()
{
	int key = 0;
	printf("Daddy has very strong arm! : ");
	scanf("%d", &key);

	if ((key1() + key2() + key3()) == key)
	{
		printf("Congratz!\n");
		int fd = open("flag", O_RDONLY);
		char buf[100];
		int r = read(fd, buf, 100);
		write(0, buf, r);
	}
	else
	{
		printf("I have strong leg :P\n");
	}

	return 0;
}

ARMのアセンブリの読み方

ARMのアセンブリを読んだことがないため,最低限調べました.

  • ARM (GNU表記) では 左側が書き込み先
  • # 付きの値は 即値 (定数)

頻出レジスタ:

レジスタ役割
r0関数の返り値 (と第1引数)
r11 (fp)フレームポインタ
sp (r13)スタックポインタ
lr (r14)リターンアドレス(呼び出し元に戻る場所)
pc (r15)プログラムカウンタ

key1() の返り値

1
2
3
4
int key1()
{
	asm("mov r3, pc\n");
}

leg.asm

1
2
3
4
5
6
7
8
9
10
11
12
;...
   ; プロローグ
   0x00008cd4 <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00008cd8 <+4>:	add	r11, sp, #0
   ; インライン asm
   0x00008cdc <+8>:	mov	r3, pc
   0x00008ce0 <+12>:	mov	r0, r3
   0x00008ce4 <+16>:	sub	sp, r11, #0
   ; エピローグ
   0x00008ce8 <+20>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00008cec <+24>:	bx	lr
;...

関数の返り値は r0 に入るため,r0 = r3 であり,r3 は mov, r0, r3 で決まります.
また,命令が4byte固定で進んでいるため,これはARMモードであると推測できます.
この命令のアドレスは 0x8cdc であり,ARMモードの場合 pc は 現在の命令 + 8 を指し示すため,mov r3, pc から2命令先の sub sp, r11, #0 の命令アドレス (0x00008ce4) が返り値になります.

  • key1() return 0x00008ce4
  • ARM: pc = 実行中命令アドレス + 8
  • Thumb: pc = 実行中命令アドレス + 4

key2() の返り値

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//...
int key2()
{
	asm(
		"push	{r6}\n"
		"add	r6, pc, $1\n"
		"bx	r6\n"
		".code   16\n"
		"mov	r3, pc\n"
		"add	r3, $0x4\n"
		"push	{r3}\n"
		"pop	{pc}\n"
		".code	32\n"
		"pop	{r6}\n");
}
//...

この問題も同様に,pop {r6} に至るまでの処理をトレースし,返り値を求めます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
;...
   ; プロローグ
   0x00008cf0 <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00008cf4 <+4>:	add	r11, sp, #0
   ; インラインasm
   0x00008cf8 <+8>:	push	{r6}		; (str r6, [sp, #-4]!)
   0x00008cfc <+12>:	add	r6, pc, #1
   0x00008d00 <+16>:	bx	r6
   ; bx により Thumbモード (2byte) に入る
   0x00008d04 <+20>:	mov	r3, pc
   0x00008d06 <+22>:	adds	r3, #4
   0x00008d08 <+24>:	push	{r3}
   0x00008d0a <+26>:	pop	{pc}
   0x00008d0c <+28>:	pop	{r6}		; (ldr r6, [sp], #4)
   ; ここで ARMモード (4byte) に戻る
   0x00008d10 <+32>:	mov	r0, r3 ; 戻り値をセット
   ; エピローグ
   0x00008d14 <+36>:	sub	sp, r11, #0
   0x00008d18 <+40>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00008d1c <+44>:	bx	lr
;...

bx 命令: Branch and Exchange
指定したレジスタのアドレスへジャンプし,必要に応じて命令セットモードを切り替える 命令です.

  • ジャンプ先アドレスの最下位ビットが 1 の場合:
    • CPUは Thumbモード に切り替えてジャンプ先を実行します.(ただし,最下位ビットを 0 にしたアドレスにジャンプする)
  • 最下位ビットが 0 の場合:
    • CPUは ARMモード に切り替えてジャンプ先を実行します.

      adds 命令: Add and Set Flags 通常の加算命令にプラスして,APSR (アプリケーションプログラム状態レジスタ) の条件フラグを変更する命令.

  • 条件フラグの概要は以下の表に示します.
フラグ名前意味・条件
N (Negative)負フラグ結果が負の数 (最上位ビットが 1) の場合に 1
Z (Zero)ゼロフラグ計算結果がちょうど 0 になった場合に 1
C (Carry)キャリーフラグ加算によって桁あふれ (キャリー) が発生した場合に 1
V (oVerflow)符号付きオーバーフロー符号付き整数として計算結果がオーバーフローした場合に 1

返り値を保持するレジスタ r0 に関連する処理を追っていきます.

  1. push {r6}: r6 を bx に入れる一時的な処理
  2. add r6, pc, #1:
    • pc = 0x00008cfc + 8 = 0x00008d04
    • r6 = 0x00008d04 + 1 = 0x00008d05
  3. bx r6: r6 = 0x00008d05 = 1000 1101 0000 0101 の最下位ビットは 1 なため,Thumb モードに切り替わり,最下位ビットを 0 にした 0x00008d04 アドレスに飛びます.
  4. mov r3, pc: pc = 0x00008d04 + 4 = 0x00008d08 より, r3 = 0x00008d08
  5. adds r3, #4: r3 = 0x00008d08 + 4 = 0x00008d0c になり,1000 1101 0000 1100 より,上位16ビットはすべて 0 なため,フラグは無し.
  6. mov r0, r3: r0 = 0x00008d0c
  • key2() return 0x00008d0c

key3() の返り値

1
2
3
4
5
6
//...
int key3()
{
	asm("mov r3, lr\n");
}
//...
1
2
3
4
5
6
7
8
9
10
11
12
;...
   ; プロローグ
   0x00008d20 <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00008d24 <+4>:	add	r11, sp, #0
   ; インラインasm
   0x00008d28 <+8>:	mov	r3, lr
   0x00008d2c <+12>:	mov	r0, r3
   ; エピローグ
   0x00008d30 <+16>:	sub	sp, r11, #0
   0x00008d34 <+20>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00008d38 <+24>:	bx	lr
;...

lr (リンクレジスタ) には,bl 命令で関数を呼んだときに リターンアドレス が自動で入ります.
また,key3() の中では lr を書き換えていないので,値は呼び出し元 main() が決めています.

1
2
3
4
5
; disas main
;...
   0x00008d7c <+64>:	bl	0x8d20 <key3>
   0x00008d80 <+68>:	mov	r3, r0
;...

bl が呼ばれた次のアドレスが保存されるので,0x00008d80 がリターンアドレスになります.

  • key3() return 0x00008d80

Exploit

0x00008ce4 + 0x00008d0c + 0x00008d80 = 108400 となるため,108400 を入力すればいいことがわかりました.

1
2
3
4
/ $ ./leg
Daddy has very strong arm! : 108400
Congratz!
<REDACTED>

Post-Mortem & Dead ends

N/A

References

N/A

This post is licensed under CC BY 4.0 by the author.