Pwnable.kr 「leg (Toddler's Bottle)」Writeup
ARMにおけるアセンブリの理解 (ARM/Thumb モード)
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
指定したレジスタのアドレスへジャンプし,必要に応じて命令セットモードを切り替える 命令です.
| フラグ | 名前 | 意味・条件 |
|---|---|---|
| N (Negative) | 負フラグ | 結果が負の数 (最上位ビットが 1) の場合に 1 |
| Z (Zero) | ゼロフラグ | 計算結果がちょうど 0 になった場合に 1 |
| C (Carry) | キャリーフラグ | 加算によって桁あふれ (キャリー) が発生した場合に 1 |
| V (oVerflow) | 符号付きオーバーフロー | 符号付き整数として計算結果がオーバーフローした場合に 1 |
返り値を保持するレジスタ r0 に関連する処理を追っていきます.
push {r6}:r6をbxに入れる一時的な処理add r6, pc, #1:pc = 0x00008cfc + 8 = 0x00008d04r6 = 0x00008d04 + 1 = 0x00008d05
bx r6:r6 = 0x00008d05 = 1000 1101 0000 0101の最下位ビットは1なため,Thumb モードに切り替わり,最下位ビットを0にした0x00008d04アドレスに飛びます.mov r3, pc:pc = 0x00008d04 + 4 = 0x00008d08より,r3 = 0x00008d08adds r3, #4:r3 = 0x00008d08 + 4 = 0x00008d0cになり,1000 1101 0000 1100より,上位16ビットはすべて0なため,フラグは無し.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
